You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

447 lines
14 KiB

6 years ago
5 years ago
5 years ago
5 years ago
6 years ago
  1. <template lang='pug'>
  2. v-dialog(
  3. v-model='isShown'
  4. persistent
  5. width='1000'
  6. :fullscreen='$vuetify.breakpoint.smAndDown'
  7. )
  8. .dialog-header
  9. v-icon(color='white') mdi-tag-text-outline
  10. .subtitle-1.white--text.ml-3 {{$t('editor:props.pageProperties')}}
  11. v-spacer
  12. v-btn.mx-0(
  13. outlined
  14. dark
  15. @click.native='close'
  16. )
  17. v-icon(left) mdi-check
  18. span {{ $t('common:actions.ok') }}
  19. v-card(tile)
  20. v-tabs(color='white', background-color='blue darken-1', dark, centered, v-model='currentTab')
  21. v-tab {{$t('editor:props.info')}}
  22. v-tab {{$t('editor:props.scheduling')}}
  23. v-tab(:disabled='!hasScriptPermission') {{$t('editor:props.scripts')}}
  24. v-tab(disabled) {{$t('editor:props.social')}}
  25. v-tab(:disabled='!hasStylePermission') {{$t('editor:props.styles')}}
  26. v-tab-item(transition='fade-transition', reverse-transition='fade-transition')
  27. v-card-text.pt-5
  28. .overline.pb-5 {{$t('editor:props.pageInfo')}}
  29. v-text-field(
  30. ref='iptTitle'
  31. outlined
  32. :label='$t(`editor:props.title`)'
  33. counter='255'
  34. v-model='title'
  35. )
  36. v-text-field(
  37. outlined
  38. :label='$t(`editor:props.shortDescription`)'
  39. counter='255'
  40. v-model='description'
  41. persistent-hint
  42. :hint='$t(`editor:props.shortDescriptionHint`)'
  43. )
  44. v-divider
  45. v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d3` : `lighten-5`')
  46. .overline.pb-5 {{$t('editor:props.path')}}
  47. v-container.pa-0(fluid, grid-list-lg)
  48. v-layout(row, wrap)
  49. v-flex(xs12, md2)
  50. v-select(
  51. outlined
  52. :label='$t(`editor:props.locale`)'
  53. suffix='/'
  54. :items='namespaces'
  55. v-model='locale'
  56. hide-details
  57. )
  58. v-flex(xs12, md10)
  59. v-text-field(
  60. outlined
  61. :label='$t(`editor:props.path`)'
  62. append-icon='mdi-folder-search'
  63. v-model='path'
  64. :hint='$t(`editor:props.pathHint`)'
  65. persistent-hint
  66. @click:append='showPathSelector'
  67. :rules='[rules.required, rules.path]'
  68. )
  69. v-divider
  70. v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-4`')
  71. .overline.pb-5 {{$t('editor:props.categorization')}}
  72. v-chip-group.radius-5.mb-5(column, v-if='tags && tags.length > 0')
  73. v-chip(
  74. v-for='tag of tags'
  75. :key='`tag-` + tag'
  76. close
  77. label
  78. color='teal'
  79. text-color='teal lighten-5'
  80. @click:close='removeTag(tag)'
  81. ) {{tag}}
  82. v-combobox(
  83. :label='$t(`editor:props.tags`)'
  84. outlined
  85. v-model='newTag'
  86. :hint='$t(`editor:props.tagsHint`)'
  87. :items='newTagSuggestions'
  88. :loading='$apollo.queries.newTagSuggestions.loading'
  89. persistent-hint
  90. hide-no-data
  91. :search-input.sync='newTagSearch'
  92. )
  93. v-tab-item(transition='fade-transition', reverse-transition='fade-transition')
  94. v-card-text
  95. .overline {{$t('editor:props.publishState')}}
  96. v-switch(
  97. :label='$t(`editor:props.publishToggle`)'
  98. v-model='isPublished'
  99. color='primary'
  100. :hint='$t(`editor:props.publishToggleHint`)'
  101. persistent-hint
  102. inset
  103. )
  104. v-divider
  105. v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d3` : `lighten-5`')
  106. v-container.pa-0(fluid, grid-list-lg)
  107. v-row
  108. v-col(cols='6')
  109. v-dialog(
  110. ref='menuPublishStart'
  111. :close-on-content-click='false'
  112. v-model='isPublishStartShown'
  113. :return-value.sync='publishStartDate'
  114. width='460px'
  115. :disabled='!isPublished'
  116. )
  117. template(v-slot:activator='{ on }')
  118. v-text-field(
  119. v-on='on'
  120. :label='$t(`editor:props.publishStart`)'
  121. v-model='publishStartDate'
  122. prepend-icon='mdi-calendar-check'
  123. readonly
  124. outlined
  125. clearable
  126. :hint='$t(`editor:props.publishStartHint`)'
  127. persistent-hint
  128. :disabled='!isPublished'
  129. )
  130. v-date-picker(
  131. v-model='publishStartDate'
  132. :min='(new Date()).toISOString().substring(0, 10)'
  133. color='primary'
  134. reactive
  135. scrollable
  136. landscape
  137. )
  138. v-spacer
  139. v-btn(
  140. text
  141. color='primary'
  142. @click='isPublishStartShown = false'
  143. ) {{$t('common:actions.cancel')}}
  144. v-btn(
  145. text
  146. color='primary'
  147. @click='$refs.menuPublishStart.save(publishStartDate)'
  148. ) {{$t('common:actions.ok')}}
  149. v-col(cols='6')
  150. v-dialog(
  151. ref='menuPublishEnd'
  152. :close-on-content-click='false'
  153. v-model='isPublishEndShown'
  154. :return-value.sync='publishEndDate'
  155. width='460px'
  156. :disabled='!isPublished'
  157. )
  158. template(v-slot:activator='{ on }')
  159. v-text-field(
  160. v-on='on'
  161. :label='$t(`editor:props.publishEnd`)'
  162. v-model='publishEndDate'
  163. prepend-icon='mdi-calendar-remove'
  164. readonly
  165. outlined
  166. clearable
  167. :hint='$t(`editor:props.publishEndHint`)'
  168. persistent-hint
  169. :disabled='!isPublished'
  170. )
  171. v-date-picker(
  172. v-model='publishEndDate'
  173. :min='(new Date()).toISOString().substring(0, 10)'
  174. color='primary'
  175. reactive
  176. scrollable
  177. landscape
  178. )
  179. v-spacer
  180. v-btn(
  181. text
  182. color='primary'
  183. @click='isPublishEndShown = false'
  184. ) {{$t('common:actions.cancel')}}
  185. v-btn(
  186. text
  187. color='primary'
  188. @click='$refs.menuPublishEnd.save(publishEndDate)'
  189. ) {{$t('common:actions.ok')}}
  190. v-tab-item(:transition='false', :reverse-transition='false')
  191. .editor-props-codeeditor-title
  192. .overline {{$t('editor:props.html')}}
  193. .editor-props-codeeditor
  194. textarea(ref='codejs')
  195. .editor-props-codeeditor-hint
  196. .caption {{$t('editor:props.htmlHint')}}
  197. v-tab-item(transition='fade-transition', reverse-transition='fade-transition')
  198. v-card-text
  199. .overline {{$t('editor:props.socialFeatures')}}
  200. v-switch(
  201. :label='$t(`editor:props.allowComments`)'
  202. v-model='isPublished'
  203. color='primary'
  204. :hint='$t(`editor:props.allowCommentsHint`)'
  205. persistent-hint
  206. inset
  207. )
  208. v-switch(
  209. :label='$t(`editor:props.allowRatings`)'
  210. v-model='isPublished'
  211. color='primary'
  212. :hint='$t(`editor:props.allowRatingsHint`)'
  213. persistent-hint
  214. disabled
  215. inset
  216. )
  217. v-switch(
  218. :label='$t(`editor:props.displayAuthor`)'
  219. v-model='isPublished'
  220. color='primary'
  221. :hint='$t(`editor:props.displayAuthorHint`)'
  222. persistent-hint
  223. inset
  224. )
  225. v-switch(
  226. :label='$t(`editor:props.displaySharingBar`)'
  227. v-model='isPublished'
  228. color='primary'
  229. :hint='$t(`editor:props.displaySharingBarHint`)'
  230. persistent-hint
  231. inset
  232. )
  233. v-tab-item(:transition='false', :reverse-transition='false')
  234. .editor-props-codeeditor-title
  235. .overline {{$t('editor:props.css')}}
  236. .editor-props-codeeditor
  237. textarea(ref='codecss')
  238. .editor-props-codeeditor-hint
  239. .caption {{$t('editor:props.cssHint')}}
  240. page-selector(:mode='pageSelectorMode', v-model='pageSelectorShown', :path='path', :locale='locale', :open-handler='setPath')
  241. </template>
  242. <script>
  243. import _ from 'lodash'
  244. import { sync, get } from 'vuex-pathify'
  245. import gql from 'graphql-tag'
  246. import CodeMirror from 'codemirror'
  247. import 'codemirror/lib/codemirror.css'
  248. import 'codemirror/mode/htmlmixed/htmlmixed.js'
  249. import 'codemirror/mode/css/css.js'
  250. /* global siteLangs, siteConfig */
  251. const filenamePattern = /^(?![\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s])(?!.*[\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]$)[^\#\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]*$/
  252. export default {
  253. props: {
  254. value: {
  255. type: Boolean,
  256. default: false
  257. }
  258. },
  259. data () {
  260. return {
  261. isPublishStartShown: false,
  262. isPublishEndShown: false,
  263. pageSelectorShown: false,
  264. namespaces: siteLangs.length ? siteLangs.map(ns => ns.code) : [siteConfig.lang],
  265. newTag: '',
  266. newTagSuggestions: [],
  267. newTagSearch: '',
  268. currentTab: 0,
  269. cm: null,
  270. rules: {
  271. required: value => !!value || 'This field is required.',
  272. path: value => {
  273. return filenamePattern.test(value) || 'Invalid path. Please ensure it does not contain special characters, or begin/end in a slash or hashtag string.'
  274. }
  275. }
  276. }
  277. },
  278. computed: {
  279. isShown: {
  280. get() { return this.value },
  281. set(val) { this.$emit('input', val) }
  282. },
  283. mode: get('editor/mode'),
  284. title: sync('page/title'),
  285. description: sync('page/description'),
  286. locale: sync('page/locale'),
  287. tags: sync('page/tags'),
  288. path: sync('page/path'),
  289. isPublished: sync('page/isPublished'),
  290. publishStartDate: sync('page/publishStartDate'),
  291. publishEndDate: sync('page/publishEndDate'),
  292. scriptJs: sync('page/scriptJs'),
  293. scriptCss: sync('page/scriptCss'),
  294. hasScriptPermission: get('page/effectivePermissions@pages.script'),
  295. hasStylePermission: get('page/effectivePermissions@pages.style'),
  296. pageSelectorMode () {
  297. return (this.mode === 'create') ? 'create' : 'move'
  298. }
  299. },
  300. watch: {
  301. value (newValue, oldValue) {
  302. if (newValue) {
  303. _.delay(() => {
  304. this.$refs.iptTitle.focus()
  305. }, 500)
  306. }
  307. },
  308. newTag (newValue, oldValue) {
  309. const tagClean = _.trim(newValue || '').toLowerCase()
  310. if (tagClean && tagClean.length > 0) {
  311. if (!_.includes(this.tags, tagClean)) {
  312. this.tags = [...this.tags, tagClean]
  313. }
  314. this.$nextTick(() => {
  315. this.newTag = null
  316. })
  317. }
  318. },
  319. currentTab (newValue, oldValue) {
  320. if (this.cm) {
  321. this.cm.toTextArea()
  322. }
  323. if (newValue === 2) {
  324. this.$nextTick(() => {
  325. setTimeout(() => {
  326. this.loadEditor(this.$refs.codejs, 'html')
  327. }, 100)
  328. })
  329. } else if (newValue === 4) {
  330. this.$nextTick(() => {
  331. setTimeout(() => {
  332. this.loadEditor(this.$refs.codecss, 'css')
  333. }, 100)
  334. })
  335. }
  336. }
  337. },
  338. methods: {
  339. removeTag (tag) {
  340. this.tags = _.without(this.tags, tag)
  341. },
  342. close() {
  343. this.isShown = false
  344. },
  345. showPathSelector() {
  346. this.pageSelectorShown = true
  347. },
  348. setPath({ path, locale }) {
  349. this.locale = locale
  350. this.path = path
  351. },
  352. loadEditor(ref, mode) {
  353. this.cm = CodeMirror.fromTextArea(ref, {
  354. tabSize: 2,
  355. mode: `text/${mode}`,
  356. theme: 'wikijs-dark',
  357. lineNumbers: true,
  358. lineWrapping: true,
  359. line: true,
  360. styleActiveLine: true,
  361. viewportMargin: 50,
  362. inputStyle: 'contenteditable',
  363. direction: 'ltr'
  364. })
  365. switch (mode) {
  366. case 'html':
  367. this.cm.setValue(this.scriptJs)
  368. this.cm.on('change', c => {
  369. this.scriptJs = c.getValue()
  370. })
  371. break
  372. case 'css':
  373. this.cm.setValue(this.scriptCss)
  374. this.cm.on('change', c => {
  375. this.scriptCss = c.getValue()
  376. })
  377. break
  378. default:
  379. console.warn('Invalid Editor Mode')
  380. break
  381. }
  382. this.cm.setSize(null, '500px')
  383. this.$nextTick(() => {
  384. this.cm.refresh()
  385. this.cm.focus()
  386. })
  387. }
  388. },
  389. apollo: {
  390. newTagSuggestions: {
  391. query: gql`
  392. query ($query: String!) {
  393. pages {
  394. searchTags (query: $query)
  395. }
  396. }
  397. `,
  398. variables () {
  399. return {
  400. query: this.newTagSearch
  401. }
  402. },
  403. fetchPolicy: 'cache-first',
  404. update: (data) => _.get(data, 'pages.searchTags', []),
  405. skip () {
  406. return !this.value || _.isEmpty(this.newTagSearch)
  407. },
  408. throttle: 500
  409. }
  410. }
  411. }
  412. </script>
  413. <style lang='scss'>
  414. .editor-props-codeeditor {
  415. background-color: mc('grey', '900');
  416. min-height: 500px;
  417. > textarea {
  418. visibility: hidden;
  419. }
  420. &-title {
  421. background-color: mc('grey', '900');
  422. border-bottom: 1px solid lighten(mc('grey', '900'), 10%);
  423. color: #FFF;
  424. padding: 10px;
  425. }
  426. &-hint {
  427. background-color: mc('grey', '900');
  428. border-top: 1px solid lighten(mc('grey', '900'), 5%);
  429. color: mc('grey', '500');
  430. padding: 5px 10px;
  431. }
  432. }
  433. </style>