editor-modal-properties.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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-d3` : `lighten-5`')
  71. .overline.pb-5 Theme Options
  72. v-switch(
  73. label='Use Site Defaults'
  74. v-model='doUseTocDefault'
  75. )
  76. v-range-slider(
  77. :disabled='doUseTocDefault'
  78. prepend-icon='mdi-serial-port'
  79. label='Heading Levels in ToC'
  80. hint='The table of contents will show headings from and up to the selected levels.'
  81. v-model='tocRange'
  82. :min='1'
  83. :max='6'
  84. :tick-labels='["H1", "H2", "H3", "H4", "H5", "H6"]'
  85. )
  86. v-divider
  87. v-card-text.grey.pt-5(:class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-4`')
  88. .overline.pb-5 {{$t('editor:props.categorization')}}
  89. v-chip-group.radius-5.mb-5(column, v-if='tags && tags.length > 0')
  90. v-chip(
  91. v-for='tag of tags'
  92. :key='`tag-` + tag'
  93. close
  94. label
  95. color='teal'
  96. text-color='teal lighten-5'
  97. @click:close='removeTag(tag)'
  98. ) {{tag}}
  99. v-combobox(
  100. :label='$t(`editor:props.tags`)'
  101. outlined
  102. v-model='newTag'
  103. :hint='$t(`editor:props.tagsHint`)'
  104. :items='newTagSuggestions'
  105. :loading='$apollo.queries.newTagSuggestions.loading'
  106. persistent-hint
  107. hide-no-data
  108. :search-input.sync='newTagSearch'
  109. )
  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='fade-transition', reverse-transition='fade-transition')
  215. v-card-text
  216. .overline {{$t('editor:props.socialFeatures')}}
  217. v-switch(
  218. :label='$t(`editor:props.allowComments`)'
  219. v-model='isPublished'
  220. color='primary'
  221. :hint='$t(`editor:props.allowCommentsHint`)'
  222. persistent-hint
  223. inset
  224. )
  225. v-switch(
  226. :label='$t(`editor:props.allowRatings`)'
  227. v-model='isPublished'
  228. color='primary'
  229. :hint='$t(`editor:props.allowRatingsHint`)'
  230. persistent-hint
  231. disabled
  232. inset
  233. )
  234. v-switch(
  235. :label='$t(`editor:props.displayAuthor`)'
  236. v-model='isPublished'
  237. color='primary'
  238. :hint='$t(`editor:props.displayAuthorHint`)'
  239. persistent-hint
  240. inset
  241. )
  242. v-switch(
  243. :label='$t(`editor:props.displaySharingBar`)'
  244. v-model='isPublished'
  245. color='primary'
  246. :hint='$t(`editor:props.displaySharingBarHint`)'
  247. persistent-hint
  248. inset
  249. )
  250. v-tab-item(:transition='false', :reverse-transition='false')
  251. .editor-props-codeeditor-title
  252. .overline {{$t('editor:props.css')}}
  253. .editor-props-codeeditor
  254. textarea(ref='codecss')
  255. .editor-props-codeeditor-hint
  256. .caption {{$t('editor:props.cssHint')}}
  257. page-selector(:mode='pageSelectorMode', v-model='pageSelectorShown', :path='path', :locale='locale', :open-handler='setPath')
  258. </template>
  259. <script>
  260. import _ from 'lodash'
  261. import { sync, get } from 'vuex-pathify'
  262. import gql from 'graphql-tag'
  263. import CodeMirror from 'codemirror'
  264. import 'codemirror/lib/codemirror.css'
  265. import 'codemirror/mode/htmlmixed/htmlmixed.js'
  266. import 'codemirror/mode/css/css.js'
  267. /* global siteLangs, siteConfig */
  268. // eslint-disable-next-line no-useless-escape
  269. const filenamePattern = /^(?![\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s])(?!.*[\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]$)[^\#\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]*$/
  270. export default {
  271. props: {
  272. value: {
  273. type: Boolean,
  274. default: false
  275. }
  276. },
  277. data () {
  278. return {
  279. isPublishStartShown: false,
  280. isPublishEndShown: false,
  281. pageSelectorShown: false,
  282. namespaces: siteLangs.length ? siteLangs.map(ns => ns.code) : [siteConfig.lang],
  283. newTag: '',
  284. newTagSuggestions: [],
  285. newTagSearch: '',
  286. currentTab: 0,
  287. cm: null,
  288. rules: {
  289. required: value => !!value || 'This field is required.',
  290. path: value => {
  291. return filenamePattern.test(value) || 'Invalid path. Please ensure it does not contain special characters, or begin/end in a slash or hashtag string.'
  292. }
  293. }
  294. }
  295. },
  296. computed: {
  297. isShown: {
  298. get() { return this.value },
  299. set(val) { this.$emit('input', val) }
  300. },
  301. mode: get('editor/mode'),
  302. title: sync('page/title'),
  303. description: sync('page/description'),
  304. locale: sync('page/locale'),
  305. tags: sync('page/tags'),
  306. path: sync('page/path'),
  307. isPublished: sync('page/isPublished'),
  308. publishStartDate: sync('page/publishStartDate'),
  309. publishEndDate: sync('page/publishEndDate'),
  310. tocRange: {
  311. get() {
  312. var range = [this.$store.get('page/minTocLevel'), this.$store.get('page/tocLevel')]
  313. return range
  314. // return [get('page/minTocLevel'), get('page/tocLevel')]
  315. },
  316. set(value) {
  317. this.$store.set('page/minTocLevel', value[0])
  318. this.$store.set('page/tocLevel', value[1])
  319. this.$store.set('page/tocCollapseLevel', value[1])
  320. }
  321. },
  322. doUseTocDefault: sync('page/doUseTocDefault'),
  323. scriptJs: sync('page/scriptJs'),
  324. scriptCss: sync('page/scriptCss'),
  325. hasScriptPermission: get('page/effectivePermissions@pages.script'),
  326. hasStylePermission: get('page/effectivePermissions@pages.style'),
  327. pageSelectorMode () {
  328. return (this.mode === 'create') ? 'create' : 'move'
  329. }
  330. },
  331. watch: {
  332. value (newValue, oldValue) {
  333. if (newValue) {
  334. _.delay(() => {
  335. this.$refs.iptTitle.focus()
  336. }, 500)
  337. }
  338. },
  339. newTag (newValue, oldValue) {
  340. const tagClean = _.trim(newValue || '').toLowerCase()
  341. if (tagClean && tagClean.length > 0) {
  342. if (!_.includes(this.tags, tagClean)) {
  343. this.tags = [...this.tags, tagClean]
  344. }
  345. this.$nextTick(() => {
  346. this.newTag = null
  347. })
  348. }
  349. },
  350. currentTab (newValue, oldValue) {
  351. if (this.cm) {
  352. this.cm.toTextArea()
  353. }
  354. if (newValue === 2) {
  355. this.$nextTick(() => {
  356. setTimeout(() => {
  357. this.loadEditor(this.$refs.codejs, 'html')
  358. }, 100)
  359. })
  360. } else if (newValue === 4) {
  361. this.$nextTick(() => {
  362. setTimeout(() => {
  363. this.loadEditor(this.$refs.codecss, 'css')
  364. }, 100)
  365. })
  366. }
  367. }
  368. },
  369. methods: {
  370. removeTag (tag) {
  371. this.tags = _.without(this.tags, tag)
  372. },
  373. close() {
  374. this.isShown = false
  375. },
  376. showPathSelector() {
  377. this.pageSelectorShown = true
  378. },
  379. setPath({ path, locale }) {
  380. this.locale = locale
  381. this.path = path
  382. },
  383. loadEditor(ref, mode) {
  384. this.cm = CodeMirror.fromTextArea(ref, {
  385. tabSize: 2,
  386. mode: `text/${mode}`,
  387. theme: 'wikijs-dark',
  388. lineNumbers: true,
  389. lineWrapping: true,
  390. line: true,
  391. styleActiveLine: true,
  392. viewportMargin: 50,
  393. inputStyle: 'contenteditable',
  394. direction: 'ltr'
  395. })
  396. switch (mode) {
  397. case 'html':
  398. this.cm.setValue(this.scriptJs)
  399. this.cm.on('change', c => {
  400. this.scriptJs = c.getValue()
  401. })
  402. break
  403. case 'css':
  404. this.cm.setValue(this.scriptCss)
  405. this.cm.on('change', c => {
  406. this.scriptCss = c.getValue()
  407. })
  408. break
  409. default:
  410. console.warn('Invalid Editor Mode')
  411. break
  412. }
  413. this.cm.setSize(null, '500px')
  414. this.$nextTick(() => {
  415. this.cm.refresh()
  416. this.cm.focus()
  417. })
  418. }
  419. },
  420. apollo: {
  421. newTagSuggestions: {
  422. query: gql`
  423. query ($query: String!) {
  424. pages {
  425. searchTags (query: $query)
  426. }
  427. }
  428. `,
  429. variables () {
  430. return {
  431. query: this.newTagSearch
  432. }
  433. },
  434. fetchPolicy: 'cache-first',
  435. update: (data) => _.get(data, 'pages.searchTags', []),
  436. skip () {
  437. return !this.value || _.isEmpty(this.newTagSearch)
  438. },
  439. throttle: 500
  440. }
  441. }
  442. }
  443. </script>
  444. <style lang='scss'>
  445. .editor-props-codeeditor {
  446. background-color: mc('grey', '900');
  447. min-height: 500px;
  448. > textarea {
  449. visibility: hidden;
  450. }
  451. &-title {
  452. background-color: mc('grey', '900');
  453. border-bottom: 1px solid lighten(mc('grey', '900'), 10%);
  454. color: #FFF;
  455. padding: 10px;
  456. }
  457. &-hint {
  458. background-color: mc('grey', '900');
  459. border-top: 1px solid lighten(mc('grey', '900'), 5%);
  460. color: mc('grey', '500');
  461. padding: 5px 10px;
  462. }
  463. }
  464. </style>