feat: page TOC + refactor PageDataDialog to composition API

pull/6078/head
Nicolas Giard 2 years ago
parent 85a74aa323
commit acc3b7369f
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -153,6 +153,11 @@ router.get(['/d', '/d/*'], async (req, res, next) => {
*/
router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
if (!site) {
throw new Error('INVALID_SITE')
}
if (pageArgs.path === '') {
return res.redirect(`/_edit/home`)
@ -175,10 +180,10 @@ router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
// -> Get page data from DB
let page = await WIKI.db.pages.getPageFromDb({
siteId: site.id,
path: pageArgs.path,
locale: pageArgs.locale,
userId: req.user.id,
isPrivate: false
userId: req.user.id
})
pageArgs.tags = _.get(page, 'tags', [])
@ -415,6 +420,11 @@ router.get('/*', async (req, res, next) => {
const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
const pageArgs = pageHelper.parsePath(req.path, { stripExt })
const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
if (!site) {
throw new Error('INVALID_SITE')
}
if (isPage) {
// if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
@ -426,6 +436,7 @@ router.get('/*', async (req, res, next) => {
try {
// -> Get Page from cache
const page = await WIKI.db.pages.getPage({
siteId: site.id,
path: pageArgs.path,
locale: pageArgs.locale,
userId: req.user.id
@ -470,67 +481,8 @@ router.get('/*', async (req, res, next) => {
})
}
// -> Build sidebar navigation
let sdi = 1
const sidebar = (await WIKI.db.navigation.getTree({ cache: true, locale: pageArgs.locale, groups: req.user.groups })).map(n => ({
i: `sdi-${sdi++}`,
k: n.kind,
l: n.label,
c: n.icon,
y: n.targetType,
t: n.target
}))
// -> Build theme code injection
const injectCode = {
css: '', // WIKI.config.theming.injectCSS,
head: '', // WIKI.config.theming.injectHead,
body: '' // WIKI.config.theming.injectBody
}
// Handle missing extra field
page.extra = page.extra || { css: '', js: '' }
if (!_.isEmpty(page.extra.css)) {
injectCode.css = `${injectCode.css}\n${page.extra.css}`
}
if (!_.isEmpty(page.extra.js)) {
injectCode.body = `${injectCode.body}\n${page.extra.js}`
}
// -> Convert page TOC
if (!_.isString(page.toc)) {
page.toc = JSON.stringify(page.toc)
}
// -> Inject comments variables
const commentTmpl = {
codeTemplate: '', // WIKI.data.commentProvider.codeTemplate,
head: '', // WIKI.data.commentProvider.head,
body: '', // WIKI.data.commentProvider.body,
main: '' // WIKI.data.commentProvider.main
}
if (false && WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) {
[
{ key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` },
{ key: 'pageId', value: page.id }
].forEach((cfg) => {
commentTmpl.head = _.replace(commentTmpl.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.body = _.replace(commentTmpl.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.main = _.replace(commentTmpl.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
})
}
// -> Render view
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
// res.render('page', {
// page,
// sidebar,
// injectCode,
// comments: commentTmpl,
// effectivePermissions
// })
} else if (pageArgs.path === 'home') {
res.redirect('/_welcome')
} else {

@ -166,7 +166,10 @@ module.exports = {
*/
async pageByPath (obj, args, context, info) {
const pageArgs = pageHelper.parsePath(args.path)
let page = await WIKI.db.pages.getPageFromDb(pageArgs)
let page = await WIKI.db.pages.getPageFromDb({
...pageArgs,
siteId: args.siteId
})
if (page) {
return {
...page,

@ -35,6 +35,7 @@ extend type Query {
): Page
pageByPath(
siteId: UUID!
path: String!
): Page
@ -173,7 +174,7 @@ type Page {
tags: [PageTag]
content: String
render: String
toc: String
toc: [JSON]
contentType: String
createdAt: Date
updatedAt: Date

@ -1010,6 +1010,7 @@ module.exports = class Page extends Model {
.where(queryModeID ? {
'pages.id': opts
} : {
'pages.siteId': opts.siteId,
'pages.path': opts.path,
'pages.localeCode': opts.locale
})

@ -62,8 +62,8 @@ module.exports = async ({ payload }) => {
$('.toc-anchor', el).remove()
_.get(toc, leafPath).push({
title: _.trim($(el).text()),
anchor: leafSlug,
label: _.trim($(el).text()),
key: leafSlug.substring(1),
children: []
})
})

@ -1,7 +1,7 @@
<template lang="pug">
q-card.page-data-dialog(style='width: 750px;')
q-toolbar.bg-primary.text-white.flex
.text-subtitle2 {{$t('editor.pageData.title')}}
.text-subtitle2 {{t('editor.pageData.title')}}
q-space
q-btn(
icon='las la-times'
@ -10,13 +10,13 @@ q-card.page-data-dialog(style='width: 750px;')
v-close-popup
)
q-card-section.page-data-dialog-selector
//- .text-overline.text-white {{$t('editor.pageData.template')}}
//- .text-overline.text-white {{t('editor.pageData.template')}}
.flex.q-gutter-sm
q-select(
dark
v-model='templateId'
:label='$t(`editor.pageData.template`)'
:aria-label='$t(`editor.pageData.template`)'
v-model='state.templateId'
:label='t(`editor.pageData.template`)'
:aria-label='t(`editor.pageData.template`)'
:options='templates'
option-value='id'
map-options
@ -28,14 +28,14 @@ q-card.page-data-dialog(style='width: 750px;')
q-btn.acrylic-btn(
dark
icon='las la-pen'
:label='$t(`common.actions.manage`)'
:label='t(`common.actions.manage`)'
unelevated
no-caps
color='deep-orange-9'
@click='editTemplates'
)
q-tabs.alt-card(
v-model='mode'
v-model='state.mode'
inline-label
no-caps
)
@ -48,11 +48,11 @@ q-card.page-data-dialog(style='width: 750px;')
label='YAML'
)
q-scroll-area(
:thumb-style='thumbStyle'
:bar-style='barStyle'
:thumb-style='siteStore.thumbStyle'
:bar-style='siteStore.barStyle'
style='height: calc(100% - 50px - 75px - 48px);'
)
q-card-section(v-if='mode === `visual`')
q-card-section(v-if='state.mode === `visual`')
.q-gutter-sm
q-input(
label='Attribute Text'
@ -76,60 +76,69 @@ q-card.page-data-dialog(style='width: 750px;')
dense
size='lg'
)
q-no-ssr(v-else, :placeholder='$t(`common.loading`)')
q-no-ssr(v-else, :placeholder='t(`common.loading`)')
codemirror.admin-theme-cm(
ref='cmData'
v-model='content'
v-model='state.content'
:options='{ mode: `text/yaml` }'
)
q-dialog(
v-model='showDataTemplateDialog'
v-model='state.showDataTemplateDialog'
)
page-data-template-dialog
</template>
<script>
import { get } from 'vuex-pathify'
<script setup>
import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar'
import { nextTick, onMounted, reactive, ref, watch } from 'vue'
import PageDataTemplateDialog from './PageDataTemplateDialog.vue'
export default {
components: {
PageDataTemplateDialog
},
data () {
return {
showDataTemplateDialog: false,
templateId: '',
content: '',
mode: 'visual'
}
},
computed: {
thumbStyle: get('site/thumbStyle', false),
barStyle: get('site/barStyle', false),
templates () {
return [
{
id: '',
label: 'None',
data: []
},
...this.$store.get('site/pageDataTemplates'),
{
id: 'basic',
label: 'Basic',
data: []
}
]
}
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()
// STORES
const pageStore = usePageStore()
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
showDataTemplateDialog: false,
templateId: '',
content: '',
mode: 'visual'
})
const templates = [
{
id: '',
label: 'None',
data: []
},
methods: {
editTemplates () {
this.showDataTemplateDialog = !this.showDataTemplateDialog
}
...siteStore.pageDataTemplates,
{
id: 'basic',
label: 'Basic',
data: []
}
]
// METHODS
function editTemplates () {
state.showDataTemplateDialog = !state.showDataTemplateDialog
}
</script>

@ -1,7 +1,7 @@
<template lang="pug">
q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-toolbar.bg-primary.text-white
.text-subtitle2 {{$t('editor.pageData.manageTemplates')}}
.text-subtitle2 {{t('editor.pageData.manageTemplates')}}
q-space
q-btn(
icon='las la-times'
@ -12,10 +12,10 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-card-section.page-datatmpl-selector
.flex.q-gutter-md
q-select.col(
v-model='selectedTemplateId'
:options='templates'
v-model='state.selectedTemplateId'
:options='siteStore.pageDataTemplates'
standout
:label='$t(`editor.pageData.template`)'
:label='t(`editor.pageData.template`)'
dense
dark
option-value='id'
@ -24,23 +24,23 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
)
q-btn(
icon='las la-plus'
:label='$t(`common.actions.new`)'
:label='t(`common.actions.new`)'
unelevated
color='primary'
no-caps
@click='create'
)
.row(v-if='tmpl')
.row(v-if='state.tmpl')
.col-auto.page-datatmpl-sd
.q-pa-md
q-btn.acrylic-btn.full-width(
:label='$t(`common.actions.howItWorks`)'
:label='t(`common.actions.howItWorks`)'
icon='las la-question-circle'
flat
color='pink'
no-caps
)
q-item-label(header, style='margin-top: 2px;') {{$t('editor.pageData.templateFullRowTypes')}}
q-item-label(header, style='margin-top: 2px;') {{t('editor.pageData.templateFullRowTypes')}}
.q-px-md
draggable(
class='q-list rounded-borders'
@ -49,8 +49,8 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
:clone='cloneFieldType'
:sort='false'
:animation='150'
@start='dragStarted = true'
@end='dragStarted = false'
@start='state.dragStarted = true'
@end='state.dragStarted = false'
item-key='id'
)
template(#item='{element}')
@ -59,7 +59,7 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-icon(:name='element.icon', color='primary')
q-item-section
q-item-label {{element.label}}
q-item-label(header) {{$t('editor.pageData.templateKeyValueTypes')}}
q-item-label(header) {{t('editor.pageData.templateKeyValueTypes')}}
.q-px-md.q-pb-md
draggable(
class='q-list rounded-borders'
@ -68,8 +68,8 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
:clone='cloneFieldType'
:sort='false'
:animation='150'
@start='dragStarted = true'
@end='dragStarted = false'
@start='state.dragStarted = true'
@end='state.dragStarted = false'
item-key='id'
)
template(#item='{element}')
@ -81,21 +81,21 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
.col.page-datatmpl-content
q-scroll-area(
ref='scrollArea'
:thumb-style='thumbStyle'
:bar-style='barStyle'
:thumb-style='siteStore.thumbStyle'
:bar-style='siteStore.barStyle'
style='height: 100%;'
)
.col.page-datatmpl-meta.q-px-md.q-py-md.flex.q-gutter-md
q-input.col(
ref='tmplTitleIpt'
:label='$t(`editor.pageData.templateTitle`)'
:label='t(`editor.pageData.templateTitle`)'
outlined
dense
v-model='tmpl.label'
v-model='state.tmpl.label'
)
q-btn.acrylic-btn(
icon='las la-check'
:label='$t(`common.actions.commit`)'
:label='t(`common.actions.commit`)'
no-caps
flat
color='positive'
@ -103,22 +103,22 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
)
q-btn.acrylic-btn(
icon='las la-trash'
:aria-label='$t(`common.actions.delete`)'
:aria-label='t(`common.actions.delete`)'
flat
color='negative'
@click='remove'
)
q-item-label(header) {{$t('editor.pageData.templateStructure')}}
q-item-label(header) {{t('editor.pageData.templateStructure')}}
.q-px-md.q-pb-md
div(:class='(dragStarted || tmpl.data.length < 1 ? `page-datatmpl-box` : ``)')
.text-caption.text-primary.q-pa-md(v-if='tmpl.data.length < 1 && !dragStarted'): em {{$t('editor.pageData.dragDropHint')}}
div(:class='(state.dragStarted || state.tmpl.data.length < 1 ? `page-datatmpl-box` : ``)')
.text-caption.text-primary.q-pa-md(v-if='state.tmpl.data.length < 1 && !state.dragStarted'): em {{t('editor.pageData.dragDropHint')}}
draggable(
class='q-list rounded-borders'
:list='tmpl.data'
:list='state.tmpl.data'
group='shared'
:animation='150'
handle='.handle'
@end='dragStarted = false'
@end='state.dragStarted = false'
item-key='id'
)
template(#item='{element}')
@ -129,14 +129,14 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-icon(:name='element.icon', color='primary')
q-item-section
q-input(
:label='$t(`editor.pageData.label`)'
:label='t(`editor.pageData.label`)'
v-model='element.label'
outlined
dense
)
q-item-section(v-if='element.type !== `header`')
q-input(
:label='$t(`editor.pageData.uniqueKey`)'
:label='t(`editor.pageData.uniqueKey`)'
v-model='element.key'
outlined
dense
@ -144,7 +144,7 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-item-section(side)
q-btn.acrylic-btn(
color='negative'
:aria-label='$t(`common.actions.delete`)'
:aria-label='t(`common.actions.delete`)'
padding='xs'
icon='las la-times'
flat
@ -152,171 +152,194 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
)
.page-datatmpl-scrollend(ref='scrollAreaEnd')
.q-pa-md.text-center(v-else-if='templates.length > 0')
em.text-grey-6 {{$t('editor.pageData.selectTemplateAbove')}}
.q-pa-md.text-center(v-else-if='siteStore.pageDataTemplates.length > 0')
em.text-grey-6 {{t('editor.pageData.selectTemplateAbove')}}
.q-pa-md.text-center(v-else)
em.text-grey-6 {{$t('editor.pageData.noTemplate')}}
em.text-grey-6 {{t('editor.pageData.noTemplate')}}
</template>
<script>
import { get, sync } from 'vuex-pathify'
<script setup>
import { v4 as uuid } from 'uuid'
import { cloneDeep, sortBy } from 'lodash-es'
import draggable from 'vuedraggable'
import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar'
import { nextTick, onMounted, reactive, ref, watch } from 'vue'
export default {
props: {
editId: {
type: String,
default: null
}
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// PROPS
const props = defineProps({
editId: {
type: String,
default: null
}
})
// QUASAR
const $q = useQuasar()
// STORES
const pageStore = usePageStore()
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
selectedTemplateId: null,
dragStarted: false,
tmpl: null
})
const inventoryMisc = [
{
key: 'header',
label: t('editor.pageData.fieldTypeHeader'),
icon: 'las la-heading'
},
components: {
draggable
{
key: 'image',
label: t('editor.pageData.fieldTypeImage'),
icon: 'las la-image'
}
]
const inventoryKV = [
{
key: 'text',
label: t('editor.pageData.fieldTypeText'),
icon: 'las la-font'
},
data () {
return {
selectedTemplateId: null,
dragStarted: false,
tmpl: null
}
{
key: 'number',
label: t('editor.pageData.fieldTypeNumber'),
icon: 'las la-infinity'
},
computed: {
templates: sync('site/pageDataTemplates', false),
thumbStyle: get('site/thumbStyle', false),
barStyle: get('site/barStyle', false),
inventoryMisc () {
return [
{
key: 'header',
label: this.$t('editor.pageData.fieldTypeHeader'),
icon: 'las la-heading'
},
{
key: 'image',
label: this.$t('editor.pageData.fieldTypeImage'),
icon: 'las la-image'
}
]
},
inventoryKV () {
return [
{
key: 'text',
label: this.$t('editor.pageData.fieldTypeText'),
icon: 'las la-font'
},
{
key: 'number',
label: this.$t('editor.pageData.fieldTypeNumber'),
icon: 'las la-infinity'
},
{
key: 'boolean',
label: this.$t('editor.pageData.fieldTypeBoolean'),
icon: 'las la-check-square'
},
{
key: 'link',
label: this.$t('editor.pageData.fieldTypeLink'),
icon: 'las la-link'
}
]
}
{
key: 'boolean',
label: t('editor.pageData.fieldTypeBoolean'),
icon: 'las la-check-square'
},
watch: {
dragStarted (newValue) {
if (newValue) {
this.$nextTick(() => {
this.$refs.scrollAreaEnd.scrollIntoView({
behavior: 'smooth'
})
})
}
},
selectedTemplateId (newValue) {
this.tmpl = cloneDeep(this.templates.find(t => t.id === this.selectedTemplateId))
{
key: 'link',
label: t('editor.pageData.fieldTypeLink'),
icon: 'las la-link'
}
]
// REFS
const scrollAreaEnd = ref(null)
const tmplTitleIpt = ref(null)
// WATCHERS
watch(() => state.dragStarted, (newValue) => {
if (newValue) {
nextTick(() => {
scrollAreaEnd.value.scrollIntoView({
behavior: 'smooth'
})
})
}
})
watch(() => state.selectedTemplateId, (newValue) => {
state.tmpl = cloneDeep(siteStore.pageDataTemplates.find(t => t.id === state.selectedTemplateId))
})
// METHODS
function cloneFieldType (tp) {
return {
id: uuid(),
type: tp.key,
label: '',
...(tp.key !== 'header' ? { key: '' } : {}),
icon: tp.icon
}
}
function removeItem (item) {
state.tmpl.data = state.tmpl.data.filter(i => i.id !== item.id)
}
function create () {
state.tmpl = {
id: uuid(),
label: t('editor.pageData.templateUntitled'),
data: []
}
nextTick(() => {
tmplTitleIpt.value.focus()
nextTick(() => {
document.execCommand('selectall')
})
})
}
function commit () {
try {
if (state.tmpl.label.length < 1) {
throw new Error(t('editor.pageData.invalidTemplateName'))
} else if (state.tmpl.data.length < 1) {
throw new Error(t('editor.pageData.emptyTemplateStructure'))
} else if (state.tmpl.data.some(f => f.label.length < 1)) {
throw new Error(t('editor.pageData.invalidTemplateLabels'))
} else if (state.tmpl.data.some(f => f.type !== 'header' && f.key.length < 1)) {
throw new Error(t('editor.pageData.invalidTemplateKeys'))
}
},
mounted () {
if (this.templates.length > 0) {
this.tmpl = this.templates[0]
this.selectedTemplateId = this.tmpl.id
} else {
this.create()
const keys = state.tmpl.data.filter(f => f.type !== 'header').map(f => f.key)
if ((new Set(keys)).size !== keys.length) {
throw new Error(t('editor.pageData.duplicateTemplateKeys'))
}
},
methods: {
cloneFieldType (tp) {
return {
id: uuid(),
type: tp.key,
label: '',
...(tp.key !== 'header' ? { key: '' } : {}),
icon: tp.icon
}
},
removeItem (item) {
this.tmpl.data = this.tmpl.data.filter(i => i.id !== item.id)
},
create () {
this.tmpl = {
id: uuid(),
label: this.$t('editor.pageData.templateUntitled'),
data: []
}
this.$nextTick(() => {
this.$refs.tmplTitleIpt.focus()
this.$nextTick(() => {
document.execCommand('selectall')
})
})
},
commit () {
try {
if (this.tmpl.label.length < 1) {
throw new Error(this.$t('editor.pageData.invalidTemplateName'))
} else if (this.tmpl.data.length < 1) {
throw new Error(this.$t('editor.pageData.emptyTemplateStructure'))
} else if (this.tmpl.data.some(f => f.label.length < 1)) {
throw new Error(this.$t('editor.pageData.invalidTemplateLabels'))
} else if (this.tmpl.data.some(f => f.type !== 'header' && f.key.length < 1)) {
throw new Error(this.$t('editor.pageData.invalidTemplateKeys'))
}
const keys = this.tmpl.data.filter(f => f.type !== 'header').map(f => f.key)
if ((new Set(keys)).size !== keys.length) {
throw new Error(this.$t('editor.pageData.duplicateTemplateKeys'))
}
if (this.templates.some(t => t.id === this.tmpl.id)) {
this.templates = sortBy([...this.templates.filter(t => t.id !== this.tmpl.id), cloneDeep(this.tmpl)], 'label')
} else {
this.templates = sortBy([...this.templates, cloneDeep(this.tmpl)], 'label')
}
this.selectedTemplateId = this.tmpl.id
} catch (err) {
this.$q.notify({
type: 'negative',
message: err.message
})
}
},
remove () {
this.$q.dialog({
title: this.$t('editor.pageData.templateDeleteConfirmTitle'),
message: this.$t('editor.pageData.templateDeleteConfirmText'),
cancel: true,
persistent: true,
color: 'negative'
}).onOk(() => {
this.templates = this.templates.filter(t => t.id !== this.selectedTemplateId)
this.selectedTemplateId = null
this.tmpl = null
})
if (siteStore.pageDataTemplates.some(t => t.id === state.tmpl.id)) {
siteStore.pageDataTemplates = sortBy([...siteStore.pageDataTemplates.filter(t => t.id !== state.tmpl.id), cloneDeep(state.tmpl)], 'label')
} else {
siteStore.pageDataTemplates = sortBy([...siteStore.pageDataTemplates, cloneDeep(state.tmpl)], 'label')
}
state.selectedTemplateId = state.tmpl.id
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
}
function remove () {
$q.dialog({
title: t('editor.pageData.templateDeleteConfirmTitle'),
message: t('editor.pageData.templateDeleteConfirmText'),
cancel: true,
persistent: true,
color: 'negative'
}).onOk(() => {
siteStore.pageDataTemplates = siteStore.pageDataTemplates.filter(t => t.id !== state.selectedTemplateId)
state.selectedTemplateId = null
state.tmpl = null
})
}
// MOUNTED
onMounted(() => {
if (siteStore.pageDataTemplates.length > 0) {
state.tmpl = siteStore.pageDataTemplates[0]
state.selectedTemplateId = state.tmpl.id
} else {
create()
}
})
</script>
<style lang="scss">

@ -270,8 +270,6 @@ q-card.page-properties-dialog
</template>
<script setup>
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar'
import { nextTick, onMounted, reactive, ref, watch } from 'vue'
@ -280,6 +278,9 @@ import PageRelationDialog from './PageRelationDialog.vue'
import PageScriptsDialog from './PageScriptsDialog.vue'
import PageTags from './PageTags.vue'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()

@ -141,9 +141,11 @@ q-page.column
q-icon.q-mr-sm(name='las la-stream', color='grey')
.text-caption.text-grey-7 Contents
.q-px-md.q-pb-sm
q-tree(
:nodes='state.toc'
q-tree.page-toc(
:nodes='pageStore.toc'
icon='las la-caret-right'
node-key='key'
dense
v-model:expanded='state.tocExpanded'
v-model:selected='state.tocSelected'
)
@ -287,6 +289,7 @@ q-page.column
transition-show='jump-left'
transition-hide='jump-right'
class='floating-sidepanel'
no-shake
)
component(:is='sideDialogs[state.sideDialogComponent]')
@ -354,54 +357,6 @@ const state = reactive({
globalDialogComponent: null,
showTagsEditBtn: false,
tagEditMode: false,
toc: [
{
key: 'h1-0',
label: 'Introduction'
},
{
key: 'h1-1',
label: 'Planets',
children: [
{
key: 'h2-0',
label: 'Earth',
children: [
{
key: 'h3-0',
label: 'Countries',
children: [
{
key: 'h4-0',
label: 'Cities',
children: [
{
key: 'h5-0',
label: 'Montreal',
children: [
{
key: 'h6-0',
label: 'Districts'
}
]
}
]
}
]
}
]
},
{
key: 'h2-1',
label: 'Mars'
},
{
key: 'h2-2',
label: 'Jupiter'
}
]
}
],
tocExpanded: ['h1-0', 'h1-1'],
tocSelected: [],
currentRating: 3
@ -472,8 +427,8 @@ watch(() => route.path, async (newValue) => {
}
}, { immediate: true })
watch(() => state.toc, refreshTocExpanded)
watch(() => pageStore.tocDepth, refreshTocExpanded)
watch(() => pageStore.toc, () => { refreshTocExpanded() }, { immediate: true })
watch(() => pageStore.tocDepth, () => { refreshTocExpanded() })
// METHODS
@ -492,20 +447,22 @@ function savePage () {
state.showGlobalDialog = true
}
function refreshTocExpanded (baseToc) {
function refreshTocExpanded (baseToc, lvl) {
console.info(pageStore.tocDepth.min, lvl, pageStore.tocDepth.max)
const toExpand = []
let isRootNode = false
if (!baseToc) {
baseToc = state.toc
baseToc = pageStore.toc
isRootNode = true
lvl = 1
}
if (baseToc.length > 0) {
for (const node of baseToc) {
if (node.key >= `h${pageStore.tocDepth.min}` && node.key <= `h${pageStore.tocDepth.max}`) {
if (lvl >= pageStore.tocDepth.min && lvl < pageStore.tocDepth.max) {
toExpand.push(node.key)
}
if (node.children?.length && node.key < `h${pageStore.tocDepth.max}`) {
toExpand.push(...refreshTocExpanded(node.children))
if (node.children?.length && lvl < pageStore.tocDepth.max - 1) {
toExpand.push(...refreshTocExpanded(node.children, lvl + 1))
}
}
}
@ -515,12 +472,6 @@ function refreshTocExpanded (baseToc) {
return toExpand
}
}
// MOUNTED
onMounted(() => {
refreshTocExpanded()
})
</script>
<style lang="scss">
@ -691,4 +642,10 @@ onMounted(() => {
background-color: $dark-3;
}
}
.page-toc {
&.q-tree--dense .q-tree__node {
padding-bottom: 5px;
}
}
</style>

@ -80,7 +80,8 @@ export const usePageStore = defineStore('page', {
},
commentsCount: 0,
content: '',
render: ''
render: '',
toc: []
}),
getters: {},
actions: {
@ -93,9 +94,11 @@ export const usePageStore = defineStore('page', {
const resp = await APOLLO_CLIENT.query({
query: gql`
query loadPage (
$siteId: UUID!
$path: String!
) {
pageByPath(
siteId: $siteId
path: $path
) {
id
@ -105,10 +108,12 @@ export const usePageStore = defineStore('page', {
locale
updatedAt
render
toc
}
}
`,
variables: {
siteId: siteStore.id,
path
},
fetchPolicy: 'network-only'

Loading…
Cancel
Save