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.

329 lines
9.1 KiB

5 years ago
5 years ago
5 years ago
  1. <template lang="pug">
  2. v-dialog(
  3. v-model='isShown'
  4. max-width='850px'
  5. overlay-color='blue darken-4'
  6. overlay-opacity='.7'
  7. )
  8. v-card.page-selector
  9. .dialog-header.is-blue
  10. v-icon.mr-3(color='white') mdi-page-next-outline
  11. .body-1(v-if='mode === `create`') {{$t('common:pageSelector.createTitle')}}
  12. .body-1(v-else-if='mode === `move`') {{$t('common:pageSelector.moveTitle')}}
  13. .body-1(v-else-if='mode === `select`') {{$t('common:pageSelector.selectTitle')}}
  14. v-spacer
  15. v-progress-circular(
  16. indeterminate
  17. color='white'
  18. :size='20'
  19. :width='2'
  20. v-show='searchLoading'
  21. )
  22. .d-flex
  23. v-flex.grey(xs5, :class='$vuetify.theme.dark ? `darken-4` : `lighten-3`')
  24. v-toolbar(color='grey darken-3', dark, dense, flat)
  25. .body-2 {{$t('common:pageSelector.virtualFolders')}}
  26. v-spacer
  27. v-btn(icon, tile, href='https://docs.requarks.io/guide/pages#folders', target='_blank')
  28. v-icon mdi-help-box
  29. div(style='height:400px;')
  30. vue-scroll(:ops='scrollStyle')
  31. v-treeview(
  32. :key='`pageTree-` + treeViewCacheId'
  33. :active.sync='currentNode'
  34. :open.sync='openNodes'
  35. :items='tree'
  36. :load-children='fetchFolders'
  37. dense
  38. expand-icon='mdi-menu-down-outline'
  39. item-id='path'
  40. item-text='title'
  41. activatable
  42. hoverable
  43. )
  44. template(slot='prepend', slot-scope='{ item, open, leaf }')
  45. v-icon mdi-{{ open ? 'folder-open' : 'folder' }}
  46. v-flex(xs7)
  47. v-toolbar(color='blue darken-2', dark, dense, flat)
  48. .body-2 {{$t('common:pageSelector.pages')}}
  49. //- v-spacer
  50. //- v-btn(icon, tile, disabled): v-icon mdi-content-save-move-outline
  51. //- v-btn(icon, tile, disabled): v-icon mdi-trash-can-outline
  52. div(v-if='currentPages.length > 0', style='height:400px;')
  53. vue-scroll(:ops='scrollStyle')
  54. v-list.py-0(dense)
  55. v-list-item-group(
  56. v-model='currentPage'
  57. color='primary'
  58. )
  59. template(v-for='(page, idx) of currentPages')
  60. v-list-item(:key='`page-` + page.id', :value='page')
  61. v-list-item-icon: v-icon mdi-text-box
  62. v-list-item-title {{page.title}}
  63. v-divider(v-if='idx < pages.length - 1')
  64. v-alert.animated.fadeIn(
  65. v-else
  66. text
  67. color='orange'
  68. prominent
  69. icon='mdi-alert'
  70. )
  71. .body-2 {{$t('common:pageSelector.folderEmptyWarning')}}
  72. v-card-actions.grey.pa-2(:class='$vuetify.theme.dark ? `darken-2` : `lighten-1`', v-if='!mustExist')
  73. v-select(
  74. solo
  75. dark
  76. flat
  77. background-color='grey darken-3-d2'
  78. hide-details
  79. single-line
  80. :items='namespaces'
  81. style='flex: 0 0 100px; border-radius: 4px 0 0 4px;'
  82. v-model='currentLocale'
  83. )
  84. v-text-field(
  85. ref='pathIpt'
  86. solo
  87. hide-details
  88. prefix='/'
  89. v-model='currentPath'
  90. flat
  91. clearable
  92. style='border-radius: 0 4px 4px 0;'
  93. )
  94. v-card-chin
  95. v-spacer
  96. v-btn(text, @click='close') {{$t('common:actions.cancel')}}
  97. v-btn.px-4(color='primary', @click='open', :disabled='!isValidPath')
  98. v-icon(left) mdi-check
  99. span {{$t('common:actions.select')}}
  100. </template>
  101. <script>
  102. import _ from 'lodash'
  103. import gql from 'graphql-tag'
  104. const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
  105. /* global siteLangs, siteConfig */
  106. export default {
  107. props: {
  108. value: {
  109. type: Boolean,
  110. default: false
  111. },
  112. path: {
  113. type: String,
  114. default: 'new-page'
  115. },
  116. locale: {
  117. type: String,
  118. default: 'en'
  119. },
  120. mode: {
  121. type: String,
  122. default: 'create'
  123. },
  124. openHandler: {
  125. type: Function,
  126. default: () => {}
  127. },
  128. mustExist: {
  129. type: Boolean,
  130. default: false
  131. }
  132. },
  133. data() {
  134. return {
  135. treeViewCacheId: 0,
  136. searchLoading: false,
  137. currentLocale: siteConfig.lang,
  138. currentFolderPath: '',
  139. currentPath: 'new-page',
  140. currentPage: null,
  141. currentNode: [0],
  142. openNodes: [0],
  143. tree: [
  144. {
  145. id: 0,
  146. title: '/ (root)',
  147. children: []
  148. }
  149. ],
  150. pages: [],
  151. all: [],
  152. namespaces: siteLangs.length ? siteLangs.map(ns => ns.code) : [siteConfig.lang],
  153. scrollStyle: {
  154. vuescroll: {},
  155. scrollPanel: {
  156. initialScrollX: 0.01, // fix scrollbar not disappearing on load
  157. scrollingX: false,
  158. speed: 50
  159. },
  160. rail: {
  161. gutterOfEnds: '2px'
  162. },
  163. bar: {
  164. onlyShowBarOnScroll: false,
  165. background: '#999',
  166. hoverStyle: {
  167. background: '#64B5F6'
  168. }
  169. }
  170. }
  171. }
  172. },
  173. computed: {
  174. isShown: {
  175. get() { return this.value },
  176. set(val) { this.$emit('input', val) }
  177. },
  178. currentPages () {
  179. return _.sortBy(_.filter(this.pages, ['parent', _.head(this.currentNode) || 0]), ['title', 'path'])
  180. },
  181. isValidPath () {
  182. if (!this.currentPath) {
  183. return false
  184. }
  185. if (this.mustExist && !this.currentPage) {
  186. return false
  187. }
  188. const firstSection = _.head(this.currentPath.split('/'))
  189. if (firstSection.length <= 1) {
  190. return false
  191. } else if (localeSegmentRegex.test(firstSection)) {
  192. return false
  193. } else if (
  194. _.some(['login', 'logout', 'register', 'verify', 'favicons', 'fonts', 'img', 'js', 'svg'], p => {
  195. return p === firstSection
  196. })) {
  197. return false
  198. } else {
  199. return true
  200. }
  201. }
  202. },
  203. watch: {
  204. isShown (newValue, oldValue) {
  205. if (newValue && !oldValue) {
  206. this.currentPath = this.path
  207. this.currentLocale = this.locale
  208. _.delay(() => {
  209. this.$refs.pathIpt.focus()
  210. })
  211. }
  212. },
  213. currentNode (newValue, oldValue) {
  214. if (newValue.length < 1) { // force a selection
  215. this.$nextTick(() => {
  216. this.currentNode = oldValue
  217. })
  218. } else {
  219. const current = _.find(this.all, ['id', newValue[0]])
  220. if (this.openNodes.indexOf(newValue[0]) < 0) { // auto open and load children
  221. if (current) {
  222. if (this.openNodes.indexOf(current.parent) < 0) {
  223. this.$nextTick(() => {
  224. this.openNodes.push(current.parent)
  225. })
  226. }
  227. }
  228. this.$nextTick(() => {
  229. this.openNodes.push(newValue[0])
  230. })
  231. }
  232. this.currentPath = _.compact([_.get(current, 'path', ''), _.last(this.currentPath.split('/'))]).join('/')
  233. }
  234. },
  235. currentPage (newValue, oldValue) {
  236. if (!_.isEmpty(newValue)) {
  237. this.currentPath = newValue.path
  238. }
  239. },
  240. currentLocale (newValue, oldValue) {
  241. this.$nextTick(() => {
  242. this.tree = [
  243. {
  244. id: 0,
  245. title: '/ (root)',
  246. children: []
  247. }
  248. ]
  249. this.currentNode = [0]
  250. this.openNodes = [0]
  251. this.pages = []
  252. this.all = []
  253. this.treeViewCacheId += 1
  254. })
  255. }
  256. },
  257. methods: {
  258. close() {
  259. this.isShown = false
  260. },
  261. open() {
  262. const exit = this.openHandler({
  263. locale: this.currentLocale,
  264. path: this.currentPath,
  265. id: (this.mustExist && this.currentPage) ? this.currentPage.pageId : 0
  266. })
  267. if (exit !== false) {
  268. this.close()
  269. }
  270. },
  271. async fetchFolders (item) {
  272. this.searchLoading = true
  273. const resp = await this.$apollo.query({
  274. query: gql`
  275. query ($parent: Int!, $mode: PageTreeMode!, $locale: String!) {
  276. pages {
  277. tree(parent: $parent, mode: $mode, locale: $locale) {
  278. id
  279. path
  280. title
  281. isFolder
  282. pageId
  283. parent
  284. }
  285. }
  286. }
  287. `,
  288. fetchPolicy: 'network-only',
  289. variables: {
  290. parent: item.id,
  291. mode: 'ALL',
  292. locale: this.currentLocale
  293. }
  294. })
  295. const items = _.get(resp, 'data.pages.tree', [])
  296. const itemFolders = _.filter(items, ['isFolder', true]).map(f => ({...f, children: []}))
  297. const itemPages = _.filter(items, i => i.pageId > 0)
  298. if (itemFolders.length > 0) {
  299. item.children = itemFolders
  300. } else {
  301. item.children = undefined
  302. }
  303. this.pages = _.unionBy(this.pages, itemPages, 'id')
  304. this.all = _.unionBy(this.all, items, 'id')
  305. this.searchLoading = false
  306. }
  307. }
  308. }
  309. </script>
  310. <style lang='scss'>
  311. .page-selector {
  312. .v-treeview-node__label {
  313. font-size: 13px;
  314. }
  315. .v-treeview-node__content {
  316. cursor: pointer;
  317. }
  318. }
  319. </style>