|
|
<template lang="pug"> v-app(v-scroll='upBtnScroll', :dark='$vuetify.theme.dark', :class='$vuetify.rtl ? `is-rtl` : `is-ltr`') nav-header(v-if='!printView') v-navigation-drawer( v-if='navMode !== `NONE` && !printView' :class='$vuetify.theme.dark ? `grey darken-4-d4` : `primary`' dark app clipped mobile-breakpoint='600' :temporary='$vuetify.breakpoint.smAndDown' v-model='navShown' :right='$vuetify.rtl' ) vue-scroll(:ops='scrollStyle') nav-sidebar(:color='$vuetify.theme.dark ? `grey darken-4-d4` : `primary`', :items='sidebarDecoded', :nav-mode='navMode')
v-fab-transition(v-if='navMode !== `NONE`') v-btn( fab color='primary' fixed bottom :right='$vuetify.rtl' :left='!$vuetify.rtl' small @click='navShown = !navShown' v-if='$vuetify.breakpoint.mdAndDown' v-show='!navShown' ) v-icon mdi-menu
v-main(ref='content') template(v-if='path !== `home`') v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-d3` : `grey lighten-3`', flat, dense, v-if='$vuetify.breakpoint.smAndUp') //- v-btn.pl-0(v-if='$vuetify.breakpoint.xsOnly', flat, @click='toggleNavigation')
//- v-icon(color='grey darken-2', left) menu
//- span Navigation
v-breadcrumbs.breadcrumbs-nav.pl-0( :items='breadcrumbs' divider='/' ) template(slot='item', slot-scope='props') v-icon(v-if='props.item.path === "/"', small, @click='goHome') mdi-home v-btn.ma-0(v-else, :href='props.item.path', small, text) {{props.item.name}} template(v-if='!isPublished') v-spacer .caption.red--text {{$t('common:page.unpublished')}} status-indicator.ml-3(negative, pulse) v-divider v-container.grey.pa-0(fluid, :class='$vuetify.theme.dark ? `darken-4-l3` : `lighten-4`') v-row(no-gutters, align-content='center', style='height: 90px;') v-col.page-col-content.is-page-header(offset-xl='2', offset-lg='3', style='margin-top: auto; margin-bottom: auto;', :class='$vuetify.rtl ? `pr-4` : `pl-4`') .headline.grey--text(:class='$vuetify.theme.dark ? `text--lighten-2` : `text--darken-3`') {{title}} .caption.grey--text.text--darken-1 {{description}} v-divider v-container.pl-5.pt-4(fluid, grid-list-xl) v-layout(row) v-flex.page-col-sd(lg3, xl2, v-if='$vuetify.breakpoint.lgAndUp') v-card.mb-5(v-if='tocDecoded.length') .overline.pa-5.pb-0(:class='$vuetify.theme.dark ? `blue--text text--lighten-2` : `primary--text`') {{$t('common:page.toc')}} v-list.pb-3(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``') template(v-for='(tocItem, tocIdx) in tocDecoded') v-list-item(@click='$vuetify.goTo(tocItem.anchor, scrollOpts)') v-icon(color='grey', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }} v-list-item-title.px-3 {{tocItem.title}} //- v-divider(v-if='tocIdx < toc.length - 1 || tocItem.children.length')
template(v-for='tocSubItem in tocItem.children') v-list-item(@click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)') v-icon.px-3(color='grey lighten-1', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }} v-list-item-title.px-3.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}} //- v-divider(inset, v-if='tocIdx < toc.length - 1')
v-card.mb-5(v-if='tags.length > 0') .pa-5 .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') {{$t('common:page.tags')}} v-chip.mr-1.mb-1( label :color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`' v-for='(tag, idx) in tags' :href='`/t/` + tag.tag' :key='`tag-` + tag.tag' ) v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', left, small) mdi-tag span(:class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`') {{tag.title}} v-chip.mr-1.mb-1( label :color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`' :href='`/t/` + tags.map(t => t.tag).join(`/`)' :aria-label='$t(`common:page.tagsMatching`)' ) v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', size='20') mdi-tag-multiple
v-card.mb-5(v-if='commentsEnabled && commentsPerms.read') .pa-5 .overline.pb-2.blue-grey--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : `text--darken-2`') span {{$t('common:comments.sdTitle')}} //- v-spacer
//- v-chip.text-center(
//- v-if='!commentsExternal'
//- label
//- x-small
//- :color='$vuetify.theme.dark ? `blue-grey darken-3` : `blue-grey darken-2`'
//- dark
//- style='min-width: 50px; justify-content: center;'
//- )
//- span {{commentsCount}}
.d-flex v-btn.text-none( @click='goToComments()' :color='$vuetify.theme.dark ? `blue-grey` : `blue-grey darken-2`' outlined style='flex: 1 1 100%;' small ) span.blue-grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') {{$t('common:comments.viewDiscussion')}} v-tooltip(right, v-if='commentsPerms.write') template(v-slot:activator='{ on }') v-btn.ml-2( @click='goToComments(true)' v-on='on' outlined small :color='$vuetify.theme.dark ? `blue-grey` : `blue-grey darken-2`' :aria-label='$t(`common:comments.newComment`)' ) v-icon(:color='$vuetify.theme.dark ? `blue-grey lighten-1` : `blue-grey darken-2`', dense) mdi-comment-plus span {{$t('common:comments.newComment')}}
v-card.mb-5 .pa-5 .overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') span {{$t('common:page.lastEditedBy')}} v-spacer v-tooltip(right, v-if='isAuthenticated') template(v-slot:activator='{ on }') v-btn.btn-animate-edit( icon :href='"/h/" + locale + "/" + path' v-on='on' x-small v-if='hasReadHistoryPermission' :aria-label='$t(`common:header.history`)' ) v-icon(color='indigo', dense) mdi-history span {{$t('common:header.history')}} .body-2.grey--text(:class='$vuetify.theme.dark ? `` : `text--darken-3`') {{ authorName }} .caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
//- v-card.mb-5
//- .pa-5
//- .overline.pb-2.yellow--text(:class='$vuetify.theme.dark ? `text--darken-3` : `text--darken-4`') Rating
//- .text-center
//- v-rating(
//- v-model='rating'
//- color='yellow darken-3'
//- background-color='grey lighten-1'
//- half-increments
//- hover
//- )
//- .caption.grey--text 5 votes
v-card(flat) v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-d3` : `grey lighten-3`', flat, dense) v-spacer v-tooltip(bottom) template(v-slot:activator='{ on }') v-btn(icon, tile, v-on='on', :aria-label='$t(`common:page.bookmark`)'): v-icon(color='grey') mdi-bookmark span {{$t('common:page.bookmark')}} v-menu(offset-y, bottom, min-width='300') template(v-slot:activator='{ on: menu }') v-tooltip(bottom) template(v-slot:activator='{ on: tooltip }') v-btn(icon, tile, v-on='{ ...menu, ...tooltip }', :aria-label='$t(`common:page.share`)'): v-icon(color='grey') mdi-share-variant span {{$t('common:page.share')}} social-sharing( :url='pageUrl' :title='title' :description='description' ) v-tooltip(bottom) template(v-slot:activator='{ on }') v-btn(icon, tile, v-on='on', @click='print', :aria-label='$t(`common:page.printFormat`)') v-icon(:color='printView ? `primary` : `grey`') mdi-printer span {{$t('common:page.printFormat')}} v-spacer
v-flex.page-col-content(xs12, lg9, xl10) v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasAnyPagePermissions') template(v-slot:activator='{ on: onEditActivator }') v-speed-dial( v-model='pageEditFab' direction='top' open-on-hover transition='scale-transition' bottom :right='!$vuetify.rtl' :left='$vuetify.rtl' fixed dark ) template(v-slot:activator) v-btn.btn-animate-edit( fab color='primary' v-model='pageEditFab' @click='pageEdit' v-on='onEditActivator' :disabled='!hasWritePagesPermission' :aria-label='$t(`common:page.editPage`)' ) v-icon mdi-pencil v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasReadHistoryPermission') template(v-slot:activator='{ on }') v-btn( fab small color='white' light v-on='on' @click='pageHistory' ) v-icon(size='20') mdi-history span {{$t('common:header.history')}} v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasReadSourcePermission') template(v-slot:activator='{ on }') v-btn( fab small color='white' light v-on='on' @click='pageSource' ) v-icon(size='20') mdi-code-tags span {{$t('common:header.viewSource')}} v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasWritePagesPermission') template(v-slot:activator='{ on }') v-btn( fab small color='white' light v-on='on' @click='pageDuplicate' ) v-icon(size='20') mdi-content-duplicate span {{$t('common:header.duplicate')}} v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasManagePagesPermission') template(v-slot:activator='{ on }') v-btn( fab small color='white' light v-on='on' @click='pageMove' ) v-icon(size='20') mdi-content-save-move-outline span {{$t('common:header.move')}} v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasDeletePagesPermission') template(v-slot:activator='{ on }') v-btn( fab dark small color='red' v-on='on' @click='pageDelete' ) v-icon(size='20') mdi-trash-can-outline span {{$t('common:header.delete')}} span {{$t('common:page.editPage')}} v-alert.mb-5(v-if='!isPublished', color='red', outlined, icon='mdi-minus-circle', dense) .caption {{$t('common:page.unpublishedWarning')}} .contents(ref='container') slot(name='contents') .comments-container#discussion(v-if='commentsEnabled && commentsPerms.read && !printView') .comments-header v-icon.mr-2(dark) mdi-comment-text-outline span {{$t('common:comments.title')}} .comments-main slot(name='comments') nav-footer notify search-results v-fab-transition v-btn( v-if='upBtnShown' fab fixed bottom :right='$vuetify.rtl' :left='!$vuetify.rtl' small :depressed='this.$vuetify.breakpoint.mdAndUp' @click='$vuetify.goTo(0, scrollOpts)' color='primary' dark :style='upBtnPosition' :aria-label='$t(`common:actions.returnToTop`)' ) v-icon mdi-arrow-up </template>
<script> import { StatusIndicator } from 'vue-status-indicator' import Tabset from './tabset.vue' import NavSidebar from './nav-sidebar.vue' import Prism from 'prismjs' import mermaid from 'mermaid' import { get, sync } from 'vuex-pathify' import _ from 'lodash' import ClipboardJS from 'clipboard' import Vue from 'vue'
Vue.component('tabset', Tabset)
Prism.plugins.autoloader.languages_path = '/_assets/js/prism/' Prism.plugins.NormalizeWhitespace.setDefaults({ 'remove-trailing': true, 'remove-indent': true, 'left-trim': true, 'right-trim': true, 'remove-initial-line-feed': true, 'tabs-to-spaces': 2 }) Prism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => { let linkCopy = document.createElement('button') linkCopy.textContent = 'Copy'
const clip = new ClipboardJS(linkCopy, { text: () => { return env.code } })
clip.on('success', () => { linkCopy.textContent = 'Copied!' resetClipboardText() }) clip.on('error', () => { linkCopy.textContent = 'Press Ctrl+C to copy' resetClipboardText() })
return linkCopy
function resetClipboardText() { setTimeout(() => { linkCopy.textContent = 'Copy' }, 5000) } })
export default { components: { NavSidebar, StatusIndicator }, props: { pageId: { type: Number, default: 0 }, locale: { type: String, default: 'en' }, path: { type: String, default: 'home' }, title: { type: String, default: 'Untitled Page' }, description: { type: String, default: '' }, createdAt: { type: String, default: '' }, updatedAt: { type: String, default: '' }, tags: { type: Array, default: () => ([]) }, authorName: { type: String, default: 'Unknown' }, authorId: { type: Number, default: 0 }, isPublished: { type: Boolean, default: false }, toc: { type: String, default: '' }, sidebar: { type: String, default: '' }, navMode: { type: String, default: 'MIXED' }, commentsEnabled: { type: Boolean, default: false }, effectivePermissions: { type: String, default: '' }, commentsExternal: { type: Boolean, default: false } }, data() { return { navShown: false, navExpanded: false, upBtnShown: false, pageEditFab: false, scrollOpts: { duration: 1500, offset: 0, easing: 'easeInOutCubic' }, scrollStyle: { vuescroll: {}, scrollPanel: { initialScrollX: 0.01, // fix scrollbar not disappearing on load
scrollingX: false, speed: 50 }, rail: { gutterOfEnds: '2px' }, bar: { onlyShowBarOnScroll: false, background: '#42A5F5', hoverStyle: { background: '#64B5F6' } } }, winWidth: 0 } }, computed: { isAuthenticated: get('user/authenticated'), commentsCount: get('page/commentsCount'), commentsPerms: get('page/effectivePermissions@comments'), rating: { get () { return 3.5 }, set (val) {
} }, breadcrumbs() { return [{ path: '/', name: 'Home' }].concat(_.reduce(this.path.split('/'), (result, value, key) => { result.push({ path: _.get(_.last(result), 'path', `/${this.locale}`) + `/${value}`, name: value }) return result }, [])) }, pageUrl () { return window.location.href }, upBtnPosition () { if (this.$vuetify.breakpoint.mdAndUp) { return this.$vuetify.rtl ? `right: 235px;` : `left: 235px;` } else { return this.$vuetify.rtl ? `right: 65px;` : `left: 65px;` } }, sidebarDecoded () { return JSON.parse(Buffer.from(this.sidebar, 'base64').toString()) }, tocDecoded () { return JSON.parse(Buffer.from(this.toc, 'base64').toString()) }, hasAdminPermission: get('page/effectivePermissions@system.manage'), hasWritePagesPermission: get('page/effectivePermissions@pages.write'), hasManagePagesPermission: get('page/effectivePermissions@pages.manage'), hasDeletePagesPermission: get('page/effectivePermissions@pages.delete'), hasReadSourcePermission: get('page/effectivePermissions@source.read'), hasReadHistoryPermission: get('page/effectivePermissions@history.read'), hasAnyPagePermissions () { return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission || this.hasDeletePagesPermission || this.hasReadSourcePermission || this.hasReadHistoryPermission }, printView: sync('site/printView') }, created() { this.$store.set('page/authorId', this.authorId) this.$store.set('page/authorName', this.authorName) this.$store.set('page/createdAt', this.createdAt) this.$store.set('page/description', this.description) this.$store.set('page/isPublished', this.isPublished) this.$store.set('page/id', this.pageId) this.$store.set('page/locale', this.locale) this.$store.set('page/path', this.path) this.$store.set('page/tags', this.tags) this.$store.set('page/title', this.title) this.$store.set('page/updatedAt', this.updatedAt) if (this.effectivePermissions) { this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString())) }
this.$store.set('page/mode', 'view') }, mounted () { if (this.$vuetify.theme.dark) { this.scrollStyle.bar.background = '#424242' }
// -> Check side navigation visibility
this.handleSideNavVisibility() window.addEventListener('resize', _.debounce(() => { this.handleSideNavVisibility() }, 500))
// -> Highlight Code Blocks
Prism.highlightAllUnder(this.$refs.container)
// -> Render Mermaid diagrams
mermaid.mermaidAPI.initialize({ startOnLoad: true, theme: this.$vuetify.theme.dark ? `dark` : `default` })
// -> Handle anchor scrolling
if (window.location.hash && window.location.hash.length > 1) { if (document.readyState === 'complete') { this.$nextTick(() => { this.$vuetify.goTo(window.location.hash, this.scrollOpts) }) } else { window.addEventListener('load', () => { this.$vuetify.goTo(window.location.hash, this.scrollOpts) }) } }
// -> Handle anchor links within the page contents
this.$nextTick(() => { this.$refs.container.querySelectorAll(`a[href^="#"], a[href^="${window.location.href.replace(window.location.hash, '')}#"]`).forEach(el => { el.onclick = ev => { ev.preventDefault() ev.stopPropagation() this.$vuetify.goTo(decodeURIComponent(ev.target.hash), this.scrollOpts) } }) }) }, methods: { goHome () { window.location.assign('/') }, toggleNavigation () { this.navOpen = !this.navOpen }, upBtnScroll () { const scrollOffset = window.pageYOffset || document.documentElement.scrollTop this.upBtnShown = scrollOffset > window.innerHeight * 0.33 }, print () { if (this.printView) { this.printView = false } else { this.printView = true this.$nextTick(() => { window.print() }) } }, pageEdit () { this.$root.$emit('pageEdit') }, pageHistory () { this.$root.$emit('pageHistory') }, pageSource () { this.$root.$emit('pageSource') }, pageDuplicate () { this.$root.$emit('pageDuplicate') }, pageMove () { this.$root.$emit('pageMove') }, pageDelete () { this.$root.$emit('pageDelete') }, handleSideNavVisibility () { if (window.innerWidth === this.winWidth) { return } this.winWidth = window.innerWidth if (this.$vuetify.breakpoint.mdAndUp) { this.navShown = true } else { this.navShown = false } }, goToComments (focusNewComment = false) { this.$vuetify.goTo('#discussion', this.scrollOpts) if (focusNewComment) { document.querySelector('#discussion-new').focus() } } } } </script>
<style lang="scss">
.breadcrumbs-nav { .v-btn { min-width: 0; &__content { text-transform: none; } } .v-breadcrumbs__divider:nth-child(2n) { padding: 0 6px; } .v-breadcrumbs__divider:nth-child(2) { padding: 0 6px 0 12px; } }
.page-col-sd { margin-top: -90px; align-self: flex-start; position: sticky; top: 64px; max-height: calc(100vh - 64px); overflow-y: auto; -ms-overflow-style: none; }
.page-col-sd::-webkit-scrollbar { display: none; }
</style>
|