page-selector.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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`') Select New Page Location
  12. .body-1(v-else-if='mode === `move`') Move / Rename Page Location
  13. .body-1(v-else-if='mode === `select`') Select a Page
  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='darkMode ? `darken-4` : `lighten-3`')
  24. v-toolbar(color='grey darken-3', dark, dense, flat)
  25. .body-2 Virtual Folders
  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 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.path')
  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 This folder is empty.
  72. v-card-actions.grey.pa-2(:class='darkMode ? `darken-2` : `lighten-1`')
  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') Cancel
  97. v-btn.px-4(color='primary', @click='open', :disabled='!isValidPath')
  98. v-icon(left) mdi-check
  99. span Select
  100. </template>
  101. <script>
  102. import _ from 'lodash'
  103. import { get } from 'vuex-pathify'
  104. import pageTreeQuery from 'gql/common/common-pages-query-tree.gql'
  105. const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
  106. /* global siteLangs, siteConfig */
  107. export default {
  108. props: {
  109. value: {
  110. type: Boolean,
  111. default: false
  112. },
  113. path: {
  114. type: String,
  115. default: 'new-page'
  116. },
  117. locale: {
  118. type: String,
  119. default: 'en'
  120. },
  121. mode: {
  122. type: String,
  123. default: 'create'
  124. },
  125. openHandler: {
  126. type: Function,
  127. default: () => {}
  128. }
  129. },
  130. data() {
  131. return {
  132. treeViewCacheId: 0,
  133. searchLoading: false,
  134. currentLocale: siteConfig.lang,
  135. currentFolderPath: '',
  136. currentPath: 'new-page',
  137. currentPage: null,
  138. currentNode: [0],
  139. openNodes: [0],
  140. tree: [
  141. {
  142. id: 0,
  143. title: '/ (root)',
  144. children: []
  145. }
  146. ],
  147. pages: [],
  148. all: [],
  149. namespaces: siteLangs.length ? siteLangs.map(ns => ns.code) : [siteConfig.lang],
  150. scrollStyle: {
  151. vuescroll: {},
  152. scrollPanel: {
  153. initialScrollX: 0.01, // fix scrollbar not disappearing on load
  154. scrollingX: false,
  155. speed: 50
  156. },
  157. rail: {
  158. gutterOfEnds: '2px'
  159. },
  160. bar: {
  161. onlyShowBarOnScroll: false,
  162. background: '#999',
  163. hoverStyle: {
  164. background: '#64B5F6'
  165. }
  166. }
  167. }
  168. }
  169. },
  170. computed: {
  171. darkMode: get('site/dark'),
  172. isShown: {
  173. get() { return this.value },
  174. set(val) { this.$emit('input', val) }
  175. },
  176. currentPages () {
  177. return _.sortBy(_.filter(this.pages, ['parent', _.head(this.currentNode) || 0]), ['title', 'path'])
  178. },
  179. isValidPath () {
  180. if (!this.currentPath) {
  181. return false
  182. }
  183. const firstSection = _.head(this.currentPath.split('/'))
  184. if (firstSection.length <= 1) {
  185. return false
  186. } else if (localeSegmentRegex.test(firstSection)) {
  187. return false
  188. } else if (
  189. _.some(['login', 'logout', 'register', 'verify', 'favicons', 'fonts', 'img', 'js', 'svg'], p => {
  190. return p === firstSection
  191. })) {
  192. return false
  193. } else {
  194. return true
  195. }
  196. }
  197. },
  198. watch: {
  199. isShown (newValue, oldValue) {
  200. if (newValue && !oldValue) {
  201. this.currentPath = this.path
  202. this.currentLocale = this.locale
  203. _.delay(() => {
  204. this.$refs.pathIpt.focus()
  205. })
  206. }
  207. },
  208. currentNode (newValue, oldValue) {
  209. if (newValue.length < 1) { // force a selection
  210. this.$nextTick(() => {
  211. this.currentNode = oldValue
  212. })
  213. } else {
  214. const current = _.find(this.all, ['id', newValue[0]])
  215. if (this.openNodes.indexOf(newValue[0]) < 0) { // auto open and load children
  216. if (current) {
  217. if (this.openNodes.indexOf(current.parent) < 0) {
  218. this.$nextTick(() => {
  219. this.openNodes.push(current.parent)
  220. })
  221. }
  222. }
  223. this.$nextTick(() => {
  224. this.openNodes.push(newValue[0])
  225. })
  226. }
  227. this.currentPath = _.compact([_.get(current, 'path', ''), _.last(this.currentPath.split('/'))]).join('/')
  228. }
  229. },
  230. currentPage (newValue, oldValue) {
  231. if (!_.isEmpty(newValue)) {
  232. this.currentPath = newValue
  233. }
  234. },
  235. currentLocale (newValue, oldValue) {
  236. this.$nextTick(() => {
  237. this.tree = [
  238. {
  239. id: 0,
  240. title: '/ (root)',
  241. children: []
  242. }
  243. ]
  244. this.currentNode = [0]
  245. this.openNodes = [0]
  246. this.pages = []
  247. this.all = []
  248. this.treeViewCacheId += 1
  249. })
  250. }
  251. },
  252. methods: {
  253. close() {
  254. this.isShown = false
  255. },
  256. open() {
  257. const exit = this.openHandler({
  258. locale: this.currentLocale,
  259. path: this.currentPath
  260. })
  261. if (exit !== false) {
  262. this.close()
  263. }
  264. },
  265. async fetchFolders (item) {
  266. this.searchLoading = true
  267. const resp = await this.$apollo.query({
  268. query: pageTreeQuery,
  269. fetchPolicy: 'network-only',
  270. variables: {
  271. parent: item.id,
  272. mode: 'ALL',
  273. locale: this.currentLocale
  274. }
  275. })
  276. const items = _.get(resp, 'data.pages.tree', [])
  277. const itemFolders = _.filter(items, ['isFolder', true]).map(f => ({...f, children: []}))
  278. const itemPages = _.filter(items, i => i.pageId > 0)
  279. if (itemFolders.length > 0) {
  280. item.children = itemFolders
  281. } else {
  282. item.children = undefined
  283. }
  284. this.pages = _.unionBy(this.pages, itemPages, 'id')
  285. this.all = _.unionBy(this.all, items, 'id')
  286. this.searchLoading = false
  287. }
  288. }
  289. }
  290. </script>
  291. <style lang='scss'>
  292. .page-selector {
  293. .v-treeview-node__label {
  294. font-size: 13px;
  295. }
  296. .v-treeview-node__content {
  297. cursor: pointer;
  298. }
  299. }
  300. </style>