editor-modal-properties.vue 14 KB

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