editor-modal-properties.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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.toc')}}
  23. v-tab {{$t('editor:props.scheduling')}}
  24. v-tab(:disabled='!hasScriptPermission') {{$t('editor:props.scripts')}}
  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.tocTitle')}}
  96. v-switch(
  97. :label='$t(`editor:props.tocUseDefault`)'
  98. v-model='useDefaultTocDepth'
  99. )
  100. v-range-slider(
  101. :disabled='useDefaultTocDepth'
  102. prepend-icon='mdi-menu-open'
  103. :label='$t(`editor:props.tocHeadingLevels`)'
  104. v-model='tocDepth'
  105. :min='1'
  106. :max='6'
  107. :tick-labels='["H1", "H2", "H3", "H4", "H5", "H6"]'
  108. )
  109. .text-caption.pl-8.grey--text {{$t('editor:props.tocHeadingLevelsHint')}}
  110. v-tab-item(transition='fade-transition', reverse-transition='fade-transition')
  111. v-card-text
  112. .overline {{$t('editor:props.publishState')}}
  113. v-switch(
  114. :label='$t(`editor:props.publishToggle`)'
  115. v-model='isPublished'
  116. color='primary'
  117. :hint='$t(`editor:props.publishToggleHint`)'
  118. persistent-hint
  119. inset
  120. )
  121. v-divider
  122. v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d3` : `lighten-5`')
  123. v-container.pa-0(fluid, grid-list-lg)
  124. v-row
  125. v-col(cols='6')
  126. v-dialog(
  127. ref='menuPublishStart'
  128. :close-on-content-click='false'
  129. v-model='isPublishStartShown'
  130. :return-value.sync='publishStartDate'
  131. width='460px'
  132. :disabled='!isPublished'
  133. )
  134. template(v-slot:activator='{ on }')
  135. v-text-field(
  136. v-on='on'
  137. :label='$t(`editor:props.publishStart`)'
  138. v-model='publishStartDate'
  139. prepend-icon='mdi-calendar-check'
  140. readonly
  141. outlined
  142. clearable
  143. :hint='$t(`editor:props.publishStartHint`)'
  144. persistent-hint
  145. :disabled='!isPublished'
  146. )
  147. v-date-picker(
  148. v-model='publishStartDate'
  149. :min='(new Date()).toISOString().substring(0, 10)'
  150. color='primary'
  151. reactive
  152. scrollable
  153. landscape
  154. )
  155. v-spacer
  156. v-btn(
  157. text
  158. color='primary'
  159. @click='isPublishStartShown = false'
  160. ) {{$t('common:actions.cancel')}}
  161. v-btn(
  162. text
  163. color='primary'
  164. @click='$refs.menuPublishStart.save(publishStartDate)'
  165. ) {{$t('common:actions.ok')}}
  166. v-col(cols='6')
  167. v-dialog(
  168. ref='menuPublishEnd'
  169. :close-on-content-click='false'
  170. v-model='isPublishEndShown'
  171. :return-value.sync='publishEndDate'
  172. width='460px'
  173. :disabled='!isPublished'
  174. )
  175. template(v-slot:activator='{ on }')
  176. v-text-field(
  177. v-on='on'
  178. :label='$t(`editor:props.publishEnd`)'
  179. v-model='publishEndDate'
  180. prepend-icon='mdi-calendar-remove'
  181. readonly
  182. outlined
  183. clearable
  184. :hint='$t(`editor:props.publishEndHint`)'
  185. persistent-hint
  186. :disabled='!isPublished'
  187. )
  188. v-date-picker(
  189. v-model='publishEndDate'
  190. :min='(new Date()).toISOString().substring(0, 10)'
  191. color='primary'
  192. reactive
  193. scrollable
  194. landscape
  195. )
  196. v-spacer
  197. v-btn(
  198. text
  199. color='primary'
  200. @click='isPublishEndShown = false'
  201. ) {{$t('common:actions.cancel')}}
  202. v-btn(
  203. text
  204. color='primary'
  205. @click='$refs.menuPublishEnd.save(publishEndDate)'
  206. ) {{$t('common:actions.ok')}}
  207. v-tab-item(:transition='false', :reverse-transition='false')
  208. .editor-props-codeeditor-title
  209. .overline {{$t('editor:props.html')}}
  210. .editor-props-codeeditor
  211. textarea(ref='codejs')
  212. .editor-props-codeeditor-hint
  213. .caption {{$t('editor:props.htmlHint')}}
  214. v-tab-item(:transition='false', :reverse-transition='false')
  215. .editor-props-codeeditor-title
  216. .overline {{$t('editor:props.css')}}
  217. .editor-props-codeeditor
  218. textarea(ref='codecss')
  219. .editor-props-codeeditor-hint
  220. .caption {{$t('editor:props.cssHint')}}
  221. page-selector(:mode='pageSelectorMode', v-model='pageSelectorShown', :path='path', :locale='locale', :open-handler='setPath')
  222. </template>
  223. <script>
  224. import _ from 'lodash'
  225. import { sync, get } from 'vuex-pathify'
  226. import gql from 'graphql-tag'
  227. import CodeMirror from 'codemirror'
  228. import 'codemirror/lib/codemirror.css'
  229. import 'codemirror/mode/htmlmixed/htmlmixed.js'
  230. import 'codemirror/mode/css/css.js'
  231. /* global siteLangs, siteConfig */
  232. // eslint-disable-next-line no-useless-escape
  233. const filenamePattern = /^(?![\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s])(?!.*[\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]$)[^\#\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]*$/
  234. export default {
  235. props: {
  236. value: {
  237. type: Boolean,
  238. default: false
  239. }
  240. },
  241. data () {
  242. return {
  243. isPublishStartShown: false,
  244. isPublishEndShown: false,
  245. pageSelectorShown: false,
  246. namespaces: siteLangs.length ? siteLangs.map(ns => ns.code) : [siteConfig.lang],
  247. newTag: '',
  248. newTagSuggestions: [],
  249. newTagSearch: '',
  250. currentTab: 0,
  251. cm: null,
  252. rules: {
  253. required: value => !!value || 'This field is required.',
  254. path: value => {
  255. return filenamePattern.test(value) || 'Invalid path. Please ensure it does not contain special characters, or begin/end in a slash or hashtag string.'
  256. }
  257. }
  258. }
  259. },
  260. computed: {
  261. isShown: {
  262. get() { return this.value },
  263. set(val) { this.$emit('input', val) }
  264. },
  265. mode: get('editor/mode'),
  266. title: sync('page/title'),
  267. description: sync('page/description'),
  268. locale: sync('page/locale'),
  269. tags: sync('page/tags'),
  270. path: sync('page/path'),
  271. isPublished: sync('page/isPublished'),
  272. publishStartDate: sync('page/publishStartDate'),
  273. publishEndDate: sync('page/publishEndDate'),
  274. tocDepth: {
  275. get() {
  276. const tocDepth = this.$store.get('page/tocDepth')
  277. return [tocDepth.min, tocDepth.max]
  278. },
  279. set(value) {
  280. this.$store.set('page/tocDepth', {
  281. min: parseInt(value[0]),
  282. max: parseInt(value[1])
  283. })
  284. }
  285. },
  286. useDefaultTocDepth: sync('page/useDefaultTocDepth'),
  287. scriptJs: sync('page/scriptJs'),
  288. scriptCss: sync('page/scriptCss'),
  289. hasScriptPermission: get('page/effectivePermissions@pages.script'),
  290. hasStylePermission: get('page/effectivePermissions@pages.style'),
  291. pageSelectorMode () {
  292. return (this.mode === 'create') ? 'create' : 'move'
  293. }
  294. },
  295. watch: {
  296. value (newValue, oldValue) {
  297. if (newValue) {
  298. _.delay(() => {
  299. this.$refs.iptTitle.focus()
  300. }, 500)
  301. }
  302. },
  303. newTag (newValue, oldValue) {
  304. const tagClean = _.trim(newValue || '').toLowerCase()
  305. if (tagClean && tagClean.length > 0) {
  306. if (!_.includes(this.tags, tagClean)) {
  307. this.tags = [...this.tags, tagClean]
  308. }
  309. this.$nextTick(() => {
  310. this.newTag = null
  311. })
  312. }
  313. },
  314. currentTab (newValue, oldValue) {
  315. if (this.cm) {
  316. this.cm.toTextArea()
  317. }
  318. if (newValue === 3) {
  319. this.$nextTick(() => {
  320. setTimeout(() => {
  321. this.loadEditor(this.$refs.codejs, 'html')
  322. }, 100)
  323. })
  324. } else if (newValue === 4) {
  325. this.$nextTick(() => {
  326. setTimeout(() => {
  327. this.loadEditor(this.$refs.codecss, 'css')
  328. }, 100)
  329. })
  330. }
  331. }
  332. },
  333. methods: {
  334. removeTag (tag) {
  335. this.tags = _.without(this.tags, tag)
  336. },
  337. close() {
  338. this.isShown = false
  339. },
  340. showPathSelector() {
  341. this.pageSelectorShown = true
  342. },
  343. setPath({ path, locale }) {
  344. this.locale = locale
  345. this.path = path
  346. },
  347. loadEditor(ref, mode) {
  348. this.cm = CodeMirror.fromTextArea(ref, {
  349. tabSize: 2,
  350. mode: `text/${mode}`,
  351. theme: 'wikijs-dark',
  352. lineNumbers: true,
  353. lineWrapping: true,
  354. line: true,
  355. styleActiveLine: true,
  356. viewportMargin: 50,
  357. inputStyle: 'contenteditable',
  358. direction: 'ltr'
  359. })
  360. switch (mode) {
  361. case 'html':
  362. this.cm.setValue(this.scriptJs)
  363. this.cm.on('change', c => {
  364. this.scriptJs = c.getValue()
  365. })
  366. break
  367. case 'css':
  368. this.cm.setValue(this.scriptCss)
  369. this.cm.on('change', c => {
  370. this.scriptCss = c.getValue()
  371. })
  372. break
  373. default:
  374. console.warn('Invalid Editor Mode')
  375. break
  376. }
  377. this.cm.setSize(null, '500px')
  378. this.$nextTick(() => {
  379. this.cm.refresh()
  380. this.cm.focus()
  381. })
  382. }
  383. },
  384. apollo: {
  385. newTagSuggestions: {
  386. query: gql`
  387. query ($query: String!) {
  388. pages {
  389. searchTags (query: $query)
  390. }
  391. }
  392. `,
  393. variables () {
  394. return {
  395. query: this.newTagSearch
  396. }
  397. },
  398. fetchPolicy: 'cache-first',
  399. update: (data) => _.get(data, 'pages.searchTags', []),
  400. skip () {
  401. return !this.value || _.isEmpty(this.newTagSearch)
  402. },
  403. throttle: 500
  404. }
  405. }
  406. }
  407. </script>
  408. <style lang='scss'>
  409. .editor-props-codeeditor {
  410. background-color: mc('grey', '900');
  411. min-height: 500px;
  412. > textarea {
  413. visibility: hidden;
  414. }
  415. &-title {
  416. background-color: mc('grey', '900');
  417. border-bottom: 1px solid lighten(mc('grey', '900'), 10%);
  418. color: #FFF;
  419. padding: 10px;
  420. }
  421. &-hint {
  422. background-color: mc('grey', '900');
  423. border-top: 1px solid lighten(mc('grey', '900'), 5%);
  424. color: mc('grey', '500');
  425. padding: 5px 10px;
  426. }
  427. }
  428. </style>