Index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. <template lang='pug'>
  2. q-page.column
  3. .page-breadcrumbs.q-py-sm.q-px-md.row
  4. .col
  5. q-breadcrumbs(
  6. active-color='grey-7'
  7. separator-color='grey'
  8. )
  9. template(v-slot:separator)
  10. q-icon(
  11. name='las la-angle-right'
  12. )
  13. q-breadcrumbs-el(icon='las la-home', to='/', aria-label='Home')
  14. q-tooltip Home
  15. q-breadcrumbs-el(
  16. v-for='brd of pageStore.breadcrumbs'
  17. :key='brd.id'
  18. :icon='brd.icon'
  19. :label='brd.title'
  20. :aria-label='brd.title'
  21. :to='brd.path'
  22. )
  23. .col-auto.flex.items-center.justify-end
  24. template(v-if='!pageStore.isPublished')
  25. .text-caption.text-accent: strong Unpublished
  26. q-separator.q-mx-sm(vertical)
  27. .text-caption.text-grey-6 Last modified on #[strong {{lastModified}}]
  28. .page-header.row
  29. //- PAGE ICON
  30. .col-auto.q-pl-md.flex.items-center
  31. q-icon.rounded-borders(
  32. :name='pageStore.icon'
  33. size='64px'
  34. color='primary'
  35. )
  36. //- PAGE HEADER
  37. .col.q-pa-md
  38. .text-h4.page-header-title {{pageStore.title}}
  39. .text-subtitle2.page-header-subtitle {{pageStore.description}}
  40. //- PAGE ACTIONS
  41. .col-auto.q-pa-md.flex.items-center.justify-end
  42. q-btn.q-mr-md(
  43. flat
  44. dense
  45. icon='las la-bell'
  46. color='grey'
  47. aria-label='Watch Page'
  48. )
  49. q-tooltip Watch Page
  50. q-btn.q-mr-md(
  51. flat
  52. dense
  53. icon='las la-bookmark'
  54. color='grey'
  55. aria-label='Bookmark Page'
  56. )
  57. q-tooltip Bookmark Page
  58. q-btn.q-mr-md(
  59. flat
  60. dense
  61. icon='las la-share-alt'
  62. color='grey'
  63. aria-label='Share'
  64. )
  65. q-tooltip Share
  66. social-sharing-menu
  67. q-btn.q-mr-md(
  68. flat
  69. dense
  70. icon='las la-print'
  71. color='grey'
  72. aria-label='Print'
  73. )
  74. q-tooltip Print
  75. q-btn.acrylic-btn(
  76. flat
  77. icon='las la-edit'
  78. color='deep-orange-9'
  79. label='Edit'
  80. aria-label='Edit'
  81. no-caps
  82. :href='editUrl'
  83. )
  84. .page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
  85. .col(style='order: 1;')
  86. q-scroll-area(
  87. :thumb-style='thumbStyle'
  88. :bar-style='barStyle'
  89. style='height: 100%;'
  90. )
  91. .q-pa-md
  92. .page-contents(v-html='pageStore.render')
  93. template(v-if='pageStore.relations && pageStore.relations.length > 0')
  94. q-separator.q-my-lg
  95. .row.align-center
  96. .col.text-left(v-if='relationsLeft.length > 0')
  97. q-btn.q-mr-sm.q-mb-sm(
  98. padding='sm md'
  99. outline
  100. :icon='rel.icon'
  101. no-caps
  102. color='primary'
  103. v-for='rel of relationsLeft'
  104. :key='`rel-id-` + rel.id'
  105. )
  106. .column.text-left.q-pl-md
  107. .text-body2: strong {{rel.label}}
  108. .text-caption {{rel.caption}}
  109. .col.text-center(v-if='relationsCenter.length > 0')
  110. .column
  111. q-btn(
  112. :label='rel.label'
  113. color='primary'
  114. flat
  115. no-caps
  116. :icon='rel.icon'
  117. v-for='rel of relationsCenter'
  118. :key='`rel-id-` + rel.id'
  119. )
  120. .col.text-right(v-if='relationsRight.length > 0')
  121. q-btn.q-ml-sm.q-mb-sm(
  122. padding='sm md'
  123. outline
  124. :icon-right='rel.icon'
  125. no-caps
  126. color='primary'
  127. v-for='rel of relationsRight'
  128. :key='`rel-id-` + rel.id'
  129. )
  130. .column.text-left.q-pr-md
  131. .text-body2: strong {{rel.label}}
  132. .text-caption {{rel.caption}}
  133. .page-sidebar(
  134. v-if='showSidebar'
  135. style='order: 2;'
  136. )
  137. template(v-if='pageStore.showToc')
  138. //- TOC
  139. .q-pa-md.flex.items-center
  140. q-icon.q-mr-sm(name='las la-stream', color='grey')
  141. .text-caption.text-grey-7 Contents
  142. .q-px-md.q-pb-sm
  143. q-tree(
  144. :nodes='state.toc'
  145. node-key='key'
  146. v-model:expanded='state.tocExpanded'
  147. v-model:selected='state.tocSelected'
  148. )
  149. //- Tags
  150. template(v-if='pageStore.showTags')
  151. q-separator(v-if='pageStore.showToc')
  152. .q-pa-md(
  153. @mouseover='state.showTagsEditBtn = true'
  154. @mouseleave='state.showTagsEditBtn = false'
  155. )
  156. .flex.items-center
  157. q-icon.q-mr-sm(name='las la-tags', color='grey')
  158. .text-caption.text-grey-7 Tags
  159. q-space
  160. transition(name='fade')
  161. q-btn(
  162. v-show='state.showTagsEditBtn'
  163. size='sm'
  164. padding='none xs'
  165. icon='las la-pen'
  166. color='deep-orange-9'
  167. flat
  168. label='Edit'
  169. no-caps
  170. @click='state.tagEditMode = !state.tagEditMode'
  171. )
  172. page-tags.q-mt-sm(:edit='state.tagEditMode')
  173. template(v-if='pageStore.allowRatings && pageStore.ratingsMode !== `off`')
  174. q-separator(v-if='pageStore.showToc || pageStore.showTags')
  175. //- Rating
  176. .q-pa-md.flex.items-center
  177. q-icon.q-mr-sm(name='las la-star-half-alt', color='grey')
  178. .text-caption.text-grey-7 Rate this page
  179. .q-px-md
  180. q-rating(
  181. v-if='pageStore.ratingsMode === `stars`'
  182. v-model='state.currentRating'
  183. icon='las la-star'
  184. color='secondary'
  185. size='sm'
  186. )
  187. .flex.items-center(v-else-if='pageStore.ratingsMode === `thumbs`')
  188. q-btn.acrylic-btn(
  189. flat
  190. icon='las la-thumbs-down'
  191. color='secondary'
  192. )
  193. q-btn.acrylic-btn.q-ml-sm(
  194. flat
  195. icon='las la-thumbs-up'
  196. color='secondary'
  197. )
  198. .page-actions.column.items-stretch.order-last
  199. q-btn.q-py-md(
  200. flat
  201. icon='las la-pen-nib'
  202. color='deep-orange-9'
  203. aria-label='Page Properties'
  204. @click='togglePageProperties'
  205. )
  206. q-tooltip(anchor='center left' self='center right') Page Properties
  207. q-btn.q-py-md(
  208. flat
  209. icon='las la-project-diagram'
  210. color='deep-orange-9'
  211. aria-label='Page Data'
  212. @click='togglePageData'
  213. )
  214. q-tooltip(anchor='center left' self='center right') Page Data
  215. q-separator.q-my-sm(inset)
  216. q-btn.q-py-sm(
  217. flat
  218. icon='las la-history'
  219. color='grey'
  220. aria-label='Page History'
  221. )
  222. q-tooltip(anchor='center left' self='center right') Page History
  223. q-btn.q-py-sm(
  224. flat
  225. icon='las la-code'
  226. color='grey'
  227. aria-label='Page Source'
  228. )
  229. q-tooltip(anchor='center left' self='center right') Page Source
  230. q-btn.q-py-sm(
  231. flat
  232. icon='las la-ellipsis-h'
  233. color='grey'
  234. aria-label='Page Actions'
  235. )
  236. q-menu(
  237. anchor='top left'
  238. self='top right'
  239. auto-close
  240. transition-show='jump-left'
  241. )
  242. q-list(padding, style='min-width: 225px;')
  243. q-item(clickable)
  244. q-item-section.items-center(avatar)
  245. q-icon(color='deep-orange-9', name='las la-atom', size='sm')
  246. q-item-section
  247. q-item-label Convert Page
  248. q-item(clickable)
  249. q-item-section.items-center(avatar)
  250. q-icon(color='deep-orange-9', name='las la-magic', size='sm')
  251. q-item-section
  252. q-item-label Re-render Page
  253. q-item(clickable)
  254. q-item-section.items-center(avatar)
  255. q-icon(color='deep-orange-9', name='las la-sun', size='sm')
  256. q-item-section
  257. q-item-label View Backlinks
  258. q-space
  259. q-btn.q-py-sm(
  260. flat
  261. icon='las la-copy'
  262. color='grey'
  263. aria-label='Duplicate Page'
  264. )
  265. q-tooltip(anchor='center left' self='center right') Duplicate Page
  266. q-btn.q-py-sm(
  267. flat
  268. icon='las la-share'
  269. color='grey'
  270. aria-label='Rename / Move Page'
  271. )
  272. q-tooltip(anchor='center left' self='center right') Rename / Move Page
  273. q-btn.q-py-sm(
  274. flat
  275. icon='las la-trash'
  276. color='grey'
  277. aria-label='Delete Page'
  278. @click='savePage'
  279. )
  280. q-tooltip(anchor='center left' self='center right') Delete Page
  281. q-dialog(
  282. v-model='state.showSideDialog'
  283. position='right'
  284. full-height
  285. transition-show='jump-left'
  286. transition-hide='jump-right'
  287. class='floating-sidepanel'
  288. )
  289. component(:is='sideDialogs[state.sideDialogComponent]')
  290. q-dialog(
  291. v-model='state.showGlobalDialog'
  292. transition-show='jump-up'
  293. transition-hide='jump-down'
  294. )
  295. component(:is='globalDialogs[state.globalDialogComponent]')
  296. </template>
  297. <script setup>
  298. import { useMeta, useQuasar, setCssVar } from 'quasar'
  299. import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
  300. import { useRouter, useRoute } from 'vue-router'
  301. import { useI18n } from 'vue-i18n'
  302. import { DateTime } from 'luxon'
  303. import { usePageStore } from 'src/stores/page'
  304. import { useSiteStore } from 'src/stores/site'
  305. // COMPONENTS
  306. import SocialSharingMenu from '../components/SocialSharingMenu.vue'
  307. import PageTags from '../components/PageTags.vue'
  308. const sideDialogs = {
  309. PageDataDialog: defineAsyncComponent(() => import('../components/PageDataDialog.vue')),
  310. PagePropertiesDialog: defineAsyncComponent(() => import('../components/PagePropertiesDialog.vue'))
  311. }
  312. const globalDialogs = {
  313. PageSaveDialog: defineAsyncComponent(() => import('../components/PageSaveDialog.vue'))
  314. }
  315. // QUASAR
  316. const $q = useQuasar()
  317. // STORES
  318. const pageStore = usePageStore()
  319. const siteStore = useSiteStore()
  320. // ROUTER
  321. const router = useRouter()
  322. const route = useRoute()
  323. // I18N
  324. const { t } = useI18n()
  325. // META
  326. useMeta({
  327. title: pageStore.title
  328. })
  329. // DATA
  330. const state = reactive({
  331. showSideDialog: false,
  332. sideDialogComponent: null,
  333. showGlobalDialog: false,
  334. globalDialogComponent: null,
  335. showTagsEditBtn: false,
  336. tagEditMode: false,
  337. toc: [
  338. {
  339. key: 'h1-0',
  340. label: 'Introduction'
  341. },
  342. {
  343. key: 'h1-1',
  344. label: 'Planets',
  345. children: [
  346. {
  347. key: 'h2-0',
  348. label: 'Earth',
  349. children: [
  350. {
  351. key: 'h3-0',
  352. label: 'Countries',
  353. children: [
  354. {
  355. key: 'h4-0',
  356. label: 'Cities',
  357. children: [
  358. {
  359. key: 'h5-0',
  360. label: 'Montreal',
  361. children: [
  362. {
  363. key: 'h6-0',
  364. label: 'Districts'
  365. }
  366. ]
  367. }
  368. ]
  369. }
  370. ]
  371. }
  372. ]
  373. },
  374. {
  375. key: 'h2-1',
  376. label: 'Mars'
  377. },
  378. {
  379. key: 'h2-2',
  380. label: 'Jupiter'
  381. }
  382. ]
  383. }
  384. ],
  385. tocExpanded: ['h1-0', 'h1-1'],
  386. tocSelected: [],
  387. currentRating: 3
  388. })
  389. const thumbStyle = {
  390. right: '2px',
  391. borderRadius: '5px',
  392. backgroundColor: '#000',
  393. width: '5px',
  394. opacity: 0.15
  395. }
  396. const barStyle = {
  397. backgroundColor: '#FAFAFA',
  398. width: '9px',
  399. opacity: 1
  400. }
  401. // COMPUTED
  402. const showSidebar = computed(() => {
  403. return pageStore.showSidebar && siteStore.showSidebar
  404. })
  405. const editorComponent = computed(() => {
  406. return pageStore.editor ? `editor-${pageStore.editor}` : null
  407. })
  408. const relationsLeft = computed(() => {
  409. return pageStore.relations ? pageStore.relations.filter(r => r.position === 'left') : []
  410. })
  411. const relationsCenter = computed(() => {
  412. return pageStore.relations ? pageStore.relations.filter(r => r.position === 'center') : []
  413. })
  414. const relationsRight = computed(() => {
  415. return pageStore.relations ? pageStore.relations.filter(r => r.position === 'right') : []
  416. })
  417. const editMode = computed(() => {
  418. return pageStore.mode === 'edit'
  419. })
  420. const editCreateMode = computed(() => {
  421. return pageStore.mode === 'edit' && pageStore.mode === 'create'
  422. })
  423. const editUrl = computed(() => {
  424. let pagePath = siteStore.useLocales ? `${pageStore.locale}/` : ''
  425. pagePath += !pageStore.path ? 'home' : pageStore.path
  426. return `/_edit/${pagePath}`
  427. })
  428. const lastModified = computed(() => {
  429. return pageStore.updatedAt ? DateTime.fromISO(pageStore.updatedAt).toLocaleString(DateTime.DATETIME_MED) : 'N/A'
  430. })
  431. // WATCHERS
  432. watch(() => route.path, async (newValue) => {
  433. if (newValue.startsWith('/_')) { return }
  434. try {
  435. await pageStore.pageLoad({ path: newValue })
  436. } catch (err) {
  437. if (err.message === 'ERR_PAGE_NOT_FOUND') {
  438. $q.notify({
  439. type: 'negative',
  440. message: 'This page does not exist (yet)!'
  441. })
  442. } else {
  443. $q.notify({
  444. type: 'negative',
  445. message: err.message
  446. })
  447. }
  448. }
  449. }, { immediate: true })
  450. watch(() => state.toc, refreshTocExpanded)
  451. watch(() => pageStore.tocDepth, refreshTocExpanded)
  452. // METHODS
  453. function togglePageProperties () {
  454. state.sideDialogComponent = 'PagePropertiesDialog'
  455. state.showSideDialog = true
  456. }
  457. function togglePageData () {
  458. state.sideDialogComponent = 'PageDataDialog'
  459. state.showSideDialog = true
  460. }
  461. function savePage () {
  462. state.globalDialogComponent = 'PageSaveDialog'
  463. state.showGlobalDialog = true
  464. }
  465. function refreshTocExpanded (baseToc) {
  466. const toExpand = []
  467. let isRootNode = false
  468. if (!baseToc) {
  469. baseToc = state.toc
  470. isRootNode = true
  471. }
  472. if (baseToc.length > 0) {
  473. for (const node of baseToc) {
  474. if (node.key >= `h${pageStore.tocDepth.min}` && node.key <= `h${pageStore.tocDepth.max}`) {
  475. toExpand.push(node.key)
  476. }
  477. if (node.children?.length && node.key < `h${pageStore.tocDepth.max}`) {
  478. toExpand.push(...refreshTocExpanded(node.children))
  479. }
  480. }
  481. }
  482. if (isRootNode) {
  483. state.tocExpanded = toExpand
  484. } else {
  485. return toExpand
  486. }
  487. }
  488. // MOUNTED
  489. onMounted(() => {
  490. refreshTocExpanded()
  491. })
  492. </script>
  493. <style lang="scss">
  494. .page-breadcrumbs {
  495. @at-root .body--light & {
  496. background: linear-gradient(to bottom, $grey-1 0%, $grey-3 100%);
  497. border-bottom: 1px solid $grey-4;
  498. }
  499. @at-root .body--dark & {
  500. background: linear-gradient(to bottom, $dark-3 0%, $dark-4 100%);
  501. border-bottom: 1px solid $dark-3;
  502. }
  503. }
  504. .page-header {
  505. @at-root .body--light & {
  506. background: linear-gradient(to bottom, $grey-2 0%, $grey-1 100%);
  507. border-bottom: 1px solid $grey-4;
  508. border-top: 1px solid #FFF;
  509. }
  510. @at-root .body--dark & {
  511. background: linear-gradient(to bottom, $dark-4 0%, $dark-3 100%);
  512. // border-bottom: 1px solid $dark-5;
  513. border-top: 1px solid $dark-6;
  514. }
  515. .no-height .q-field__control {
  516. height: auto;
  517. }
  518. &-title {
  519. @at-root .body--light & {
  520. color: $grey-9;
  521. }
  522. @at-root .body--dark & {
  523. color: #FFF;
  524. }
  525. }
  526. &-subtitle {
  527. @at-root .body--light & {
  528. color: $grey-7;
  529. }
  530. @at-root .body--dark & {
  531. color: rgba(255,255,255,.6);
  532. }
  533. }
  534. }
  535. .page-container {
  536. @at-root .body--light & {
  537. border-top: 1px solid #FFF;
  538. }
  539. // @at-root .body--dark & {
  540. // border-top: 1px solid $dark-6;
  541. // }
  542. }
  543. .page-sidebar {
  544. flex: 0 0 300px;
  545. @at-root .body--light & {
  546. background-color: $grey-2;
  547. }
  548. @at-root .body--dark & {
  549. background-color: $dark-5;
  550. }
  551. .q-separator {
  552. background-color: rgba(0,0,0,.05);
  553. border-bottom: 1px solid;
  554. @at-root .body--light & {
  555. background-color: rgba(0,0,0,.05);
  556. border-bottom-color: #FFF;
  557. }
  558. @at-root .body--dark & {
  559. background-color: rgba(255,255,255,.04);
  560. border-bottom-color: #070a0d;
  561. }
  562. }
  563. }
  564. .page-actions {
  565. flex: 0 0 56px;
  566. @at-root .body--light & {
  567. background-color: $grey-3;
  568. }
  569. @at-root .body--dark & {
  570. background-color: $dark-4;
  571. }
  572. }
  573. .floating-syncpanel {
  574. .q-dialog__inner {
  575. margin-top: 14px;
  576. right: 140px;
  577. left: auto;
  578. .q-card {
  579. border-radius: 17px;
  580. }
  581. }
  582. &-msg {
  583. padding-top: 1px;
  584. font-weight: 500;
  585. font-size: .75rem;
  586. padding-right: 16px;
  587. display: flex;
  588. align-items: center;
  589. }
  590. }
  591. .floating-sidepanel {
  592. .q-dialog__inner {
  593. right: 24px;
  594. .q-card {
  595. border-radius: 4px !important;
  596. min-width: 450px;
  597. .q-card__section {
  598. border-radius: 0;
  599. }
  600. }
  601. }
  602. .alt-card {
  603. @at-root .body--light & {
  604. background-color: $grey-2;
  605. border-top: 1px solid $grey-4;
  606. box-shadow: inset 0 1px 0 0 #FFF, inset 0 -1px 0 0 #FFF;
  607. border-bottom: 1px solid $grey-4;
  608. }
  609. @at-root .body--dark & {
  610. background-color: $dark-4;
  611. border-top: 1px solid lighten($dark-3, 8%);
  612. box-shadow: inset 0 1px 0 0 $dark-6, inset 0 -1px 0 0 $dark-6;
  613. border-bottom: 1px solid lighten($dark-3, 8%);
  614. }
  615. }
  616. &-quickaccess {
  617. width: 40px;
  618. border-radius: 4px !important;
  619. background-color: rgba(0,0,0,.75);
  620. color: #FFF;
  621. position: fixed;
  622. right: 486px;
  623. top: 74px;
  624. z-index: -1;
  625. display: flex;
  626. flex-direction: column;
  627. box-shadow: 0 0 5px 0 rgba(0,0,0,.5) !important;
  628. @at-root .q-transition--jump-left-enter-active & {
  629. display: none !important;
  630. }
  631. @at-root .q-transition--jump-right-leave-active & {
  632. display: none !important;
  633. }
  634. }
  635. }
  636. .q-card {
  637. @at-root .body--light & {
  638. background-color: #FFF;
  639. }
  640. @at-root .body--dark & {
  641. background-color: $dark-3;
  642. }
  643. }
  644. </style>