Files
wiki/ux/src/pages/Index.vue
T

500 lines
13 KiB
Vue

<template lang='pug'>
q-page.column
.page-breadcrumbs.q-py-sm.q-px-md.row(
v-if='!editorStore.isActive'
)
.col
q-breadcrumbs(
active-color='grey-7'
separator-color='grey'
)
template(v-slot:separator)
q-icon(
name='las la-angle-right'
)
q-breadcrumbs-el(icon='las la-home', to='/', aria-label='Home')
q-tooltip Home
q-breadcrumbs-el(
v-for='brd of pageStore.breadcrumbs'
:key='brd.id'
:icon='brd.icon'
:label='brd.title'
:aria-label='brd.title'
:to='brd.path'
)
.col-auto.flex.items-center.justify-end
template(v-if='!pageStore.publishState === `draft`')
.text-caption.text-accent: strong Unpublished
q-separator.q-mx-sm(vertical)
.text-caption.text-grey-6 Last modified on #[strong {{lastModified}}]
page-header
.page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
.col(
:style='siteStore.theme.tocPosition === `left` ? `order: 2;` : `order: 1;`'
)
q-no-ssr(
v-if='editorStore.isActive'
)
component(:is='editorComponents[editorStore.editor]')
q-scroll-area.page-container-scrl(
v-else
:thumb-style='thumbStyle'
:bar-style='barStyle'
style='height: 100%;'
)
.q-pa-md
.page-contents(ref='pageContents', v-html='pageStore.render')
template(v-if='pageStore.relations && pageStore.relations.length > 0')
q-separator.q-my-lg
.row.align-center
.col.text-left(v-if='relationsLeft.length > 0')
q-btn.q-mr-sm.q-mb-sm(
padding='sm md'
outline
:icon='rel.icon'
no-caps
color='primary'
v-for='rel of relationsLeft'
:key='`rel-id-` + rel.id'
)
.column.text-left.q-pl-md
.text-body2: strong {{rel.label}}
.text-caption {{rel.caption}}
.col.text-center(v-if='relationsCenter.length > 0')
.column
q-btn(
:label='rel.label'
color='primary'
flat
no-caps
:icon='rel.icon'
v-for='rel of relationsCenter'
:key='`rel-id-` + rel.id'
)
.col.text-right(v-if='relationsRight.length > 0')
q-btn.q-ml-sm.q-mb-sm(
padding='sm md'
outline
:icon-right='rel.icon'
no-caps
color='primary'
v-for='rel of relationsRight'
:key='`rel-id-` + rel.id'
)
.column.text-left.q-pr-md
.text-body2: strong {{rel.label}}
.text-caption {{rel.caption}}
.page-sidebar(
v-if='showSidebar'
:style='siteStore.theme.tocPosition === `left` ? `order: 1;` : `order: 2;`'
)
template(v-if='pageStore.showToc')
//- TOC
.q-pa-md.flex.items-center
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.page-toc(
:nodes='pageStore.toc'
icon='las la-caret-right'
node-key='key'
dense
v-model:expanded='state.tocExpanded'
v-model:selected='state.tocSelected'
)
//- Tags
template(v-if='pageStore.showTags')
q-separator(v-if='pageStore.showToc')
.q-pa-md(
@mouseover='state.showTagsEditBtn = true'
@mouseleave='state.showTagsEditBtn = false'
)
.flex.items-center
q-icon.q-mr-sm(name='las la-tags', color='grey')
.text-caption.text-grey-7 Tags
q-space
transition(name='fade')
q-btn(
v-show='state.showTagsEditBtn'
size='sm'
padding='none xs'
icon='las la-pen'
color='deep-orange-9'
flat
label='Edit'
no-caps
@click='state.tagEditMode = !state.tagEditMode'
)
page-tags.q-mt-sm(:edit='state.tagEditMode')
template(v-if='siteStore.features.ratingsMode !== `off` && pageStore.allowRatings')
q-separator(v-if='pageStore.showToc || pageStore.showTags')
//- Rating
.q-pa-md.flex.items-center
q-icon.q-mr-sm(name='las la-star-half-alt', color='grey')
.text-caption.text-grey-7 Rate this page
.q-px-md
q-rating(
v-if='siteStore.features.ratingsMode === `stars`'
v-model='state.currentRating'
icon='las la-star'
color='secondary'
size='sm'
)
.flex.items-center(v-else-if='siteStore.features.ratingsMode === `thumbs`')
q-btn.acrylic-btn(
flat
icon='las la-thumbs-down'
color='secondary'
)
q-btn.acrylic-btn.q-ml-sm(
flat
icon='las la-thumbs-up'
color='secondary'
)
page-actions-col
side-dialog
</template>
<script setup>
import { useMeta, useQuasar } from 'quasar'
import { computed, defineAsyncComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { DateTime } from 'luxon'
import { useCommonStore } from '@/stores/common'
import { useEditorStore } from '@/stores/editor'
import { useFlagsStore } from '@/stores/flags'
import { usePageStore } from '@/stores/page'
import { useSiteStore } from '@/stores/site'
import { useUserStore } from '@/stores/user'
// COMPONENTS
import LoadingGeneric from '@/components/LoadingGeneric.vue'
import PageActionsCol from '@/components/PageActionsCol.vue'
import PageHeader from '@/components/PageHeader.vue'
import PageTags from '@/components/PageTags.vue'
import SideDialog from '@/components/SideDialog.vue'
const editorComponents = {
markdown: defineAsyncComponent({
loader: () => import('../components/EditorMarkdown.vue'),
loadingComponent: LoadingGeneric
}),
wysiwyg: defineAsyncComponent({
loader: () => import('../components/EditorWysiwyg.vue'),
loadingComponent: LoadingGeneric
})
}
// QUASAR
const $q = useQuasar()
// STORES
const commonStore = useCommonStore()
const editorStore = useEditorStore()
const flagsStore = useFlagsStore()
const pageStore = usePageStore()
const siteStore = useSiteStore()
const userStore = useUserStore()
// ROUTER
const router = useRouter()
const route = useRoute()
// I18N
const { t } = useI18n()
// META
useMeta({
title: pageStore.title
})
// DATA
const state = reactive({
showSideDialog: false,
sideDialogComponent: null,
showGlobalDialog: false,
globalDialogComponent: null,
showTagsEditBtn: false,
tagEditMode: false,
tocExpanded: ['h1-0', 'h1-1'],
tocSelected: [],
currentRating: 3
})
const thumbStyle = {
right: '2px',
borderRadius: '5px',
backgroundColor: $q.dark.isActive ? '#FFF' : '#000',
width: '5px',
opacity: 0.15
}
const barStyle = {
backgroundColor: $q.dark.isActive ? '#161b22' : '#FAFAFA',
width: '9px',
opacity: 1
}
const pageContents = ref(null)
// COMPUTED
const showSidebar = computed(() => {
return pageStore.showSidebar && siteStore.showSidebar && siteStore.theme.tocPosition !== 'off' && !editorStore.isActive
})
const relationsLeft = computed(() => {
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'left') : []
})
const relationsCenter = computed(() => {
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'center') : []
})
const relationsRight = computed(() => {
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'right') : []
})
const lastModified = computed(() => {
return pageStore.updatedAt ? DateTime.fromISO(pageStore.updatedAt).toLocaleString(DateTime.DATETIME_MED) : 'N/A'
})
// WATCHERS
watch(() => route.path, async (newValue) => {
// -> Ignore route change (e.g. from page create route fix)
if (editorStore.ignoreRouteChange) {
editorStore.$patch({ ignoreRouteChange: false })
return
}
// -> Enter Create Mode?
if (newValue.startsWith('/_create')) {
if (!route.params.editor) {
$q.notify({
type: 'negative',
message: 'No editor specified!'
})
return router.replace('/')
}
$q.loading.show()
const pageCreateArgs = { editor: route.params.editor, fromNavigate: true }
if (route.query.path) {
pageCreateArgs.path = route.query.path
}
if (route.query.locale) {
pageCreateArgs.locale = route.query.locale
}
await pageStore.pageCreate(pageCreateArgs)
$q.loading.hide()
return
}
// -> Enter Edit Mode?
if (newValue.startsWith('/_edit')) {
if (!route.params.pagePath) {
return router.replace('/')
}
$q.loading.show()
await pageStore.pageEdit({ path: route.params.pagePath, fromNavigate: true })
$q.loading.hide()
return
}
// -> Moving to a non-page path? Ignore
if (newValue.startsWith('/_')) { return }
// -> Load Page
try {
await pageStore.pageLoad({ path: newValue })
if (editorStore.isActive) {
editorStore.$patch({
isActive: false
})
}
// -> Load Blocks
nextTick(() => {
for (const block of pageContents.value.querySelectorAll(':not(:defined)')) {
commonStore.loadBlocks([block.tagName.toLowerCase()])
}
})
} catch (err) {
if (err.message === 'ERR_PAGE_NOT_FOUND') {
if (newValue === '/') {
if (!userStore.authenticated) {
router.push('/login')
} else if (!userStore.can('write:pages')) {
router.replace('/_error/unauthorized')
} else {
siteStore.overlay = 'Welcome'
}
} else {
$q.notify({
type: 'negative',
message: 'This page does not exist (yet)!'
})
}
} else {
$q.notify({
type: 'negative',
message: err.message
})
}
}
}, { immediate: true })
watch(() => pageStore.toc, () => { refreshTocExpanded() }, { immediate: true })
watch(() => pageStore.tocDepth, () => { refreshTocExpanded() })
// METHODS
function refreshTocExpanded (baseToc, lvl) {
const toExpand = []
let isRootNode = false
if (!baseToc) {
baseToc = pageStore.toc
isRootNode = true
lvl = 1
}
if (baseToc.length > 0) {
for (const node of baseToc) {
if (lvl >= pageStore.tocDepth.min && lvl < pageStore.tocDepth.max) {
toExpand.push(node.key)
}
if (node.children?.length && lvl < pageStore.tocDepth.max - 1) {
toExpand.push(...refreshTocExpanded(node.children, lvl + 1))
}
}
}
if (isRootNode) {
state.tocExpanded = toExpand
} else {
return toExpand
}
}
</script>
<style lang="scss">
.page-breadcrumbs {
@at-root .body--light & {
background: linear-gradient(to bottom, $grey-1 0%, $grey-3 100%);
border-bottom: 1px solid $grey-4;
}
@at-root .body--dark & {
background: linear-gradient(to bottom, $dark-3 0%, $dark-4 100%);
border-bottom: 1px solid $dark-3;
}
}
.page-header {
height: 95px;
@at-root .body--light & {
background: linear-gradient(to bottom, $grey-2 0%, $grey-1 100%);
border-bottom: 1px solid $grey-4;
border-top: 1px solid #FFF;
}
@at-root .body--dark & {
background: linear-gradient(to bottom, $dark-4 0%, $dark-3 100%);
// border-bottom: 1px solid $dark-5;
border-top: 1px solid $dark-6;
}
.no-height .q-field__control {
height: auto;
}
&-title {
@at-root .body--light & {
color: $grey-9;
}
@at-root .body--dark & {
color: #FFF;
}
}
&-subtitle {
@at-root .body--light & {
color: $grey-7;
}
@at-root .body--dark & {
color: rgba(255,255,255,.6);
}
}
}
.page-container {
@at-root .body--light & {
border-top: 1px solid #FFF;
}
// @at-root .body--dark & {
// border-top: 1px solid $dark-6;
// }
.page-container-scrl > .q-scrollarea__container > .q-scrollarea__content {
width: 100%;
}
}
.page-sidebar {
flex: 0 0 300px;
@at-root .body--light & {
background-color: $grey-2;
}
@at-root .body--dark & {
background-color: $dark-5;
}
.q-separator {
background-color: rgba(0,0,0,.05);
border-bottom: 1px solid;
@at-root .body--light & {
background-color: rgba(0,0,0,.05);
border-bottom-color: #FFF;
}
@at-root .body--dark & {
background-color: rgba(255,255,255,.04);
border-bottom-color: #070a0d;
}
}
}
.floating-syncpanel {
.q-dialog__inner {
margin-top: 14px;
right: 140px;
left: auto;
.q-card {
border-radius: 17px;
}
}
&-msg {
padding-top: 1px;
font-weight: 500;
font-size: .75rem;
padding-right: 16px;
display: flex;
align-items: center;
}
}
.q-card {
@at-root .body--light & {
background-color: #FFF;
}
@at-root .body--dark & {
background-color: $dark-3;
}
}
.page-toc {
&.q-tree--dense .q-tree__node {
padding-bottom: 5px;
}
}
</style>