You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

497 lines
16 KiB

  1. <template lang="pug">
  2. v-app(v-scroll='upBtnScroll', :dark='darkMode', :class='$vuetify.rtl ? `is-rtl` : `is-ltr`')
  3. nav-header
  4. v-navigation-drawer(
  5. :class='darkMode ? `grey darken-4-d4` : `primary`'
  6. dark
  7. app
  8. clipped
  9. mobile-break-point='600'
  10. :temporary='$vuetify.breakpoint.smAndDown'
  11. v-model='navShown'
  12. :right='$vuetify.rtl'
  13. )
  14. vue-scroll(:ops='scrollStyle')
  15. nav-sidebar(:color='darkMode ? `grey darken-4-d4` : `primary`', :items='sidebar')
  16. v-fab-transition
  17. v-btn(
  18. fab
  19. color='primary'
  20. fixed
  21. bottom
  22. :right='$vuetify.rtl'
  23. :left='!$vuetify.rtl'
  24. small
  25. @click='navShown = !navShown'
  26. v-if='$vuetify.breakpoint.mdAndDown'
  27. v-show='!navShown'
  28. )
  29. v-icon mdi-menu
  30. v-content(ref='content')
  31. template(v-if='path !== `home`')
  32. v-toolbar(:color='darkMode ? `grey darken-4-d3` : `grey lighten-3`', flat, dense, v-if='$vuetify.breakpoint.smAndUp')
  33. //- v-btn.pl-0(v-if='$vuetify.breakpoint.xsOnly', flat, @click='toggleNavigation')
  34. //- v-icon(color='grey darken-2', left) menu
  35. //- span Navigation
  36. v-breadcrumbs.breadcrumbs-nav.pl-0(
  37. :items='breadcrumbs'
  38. divider='/'
  39. )
  40. template(slot='item', slot-scope='props')
  41. v-icon(v-if='props.item.path === "/"', small, @click='goHome') mdi-home
  42. v-btn.ma-0(v-else, :href='props.item.path', small, text) {{props.item.name}}
  43. template(v-if='!isPublished')
  44. v-spacer
  45. .caption.red--text {{$t('common:page.unpublished')}}
  46. status-indicator.ml-3(negative, pulse)
  47. v-divider
  48. v-container.grey.pa-0(fluid, :class='darkMode ? `darken-4-l3` : `lighten-4`')
  49. v-row(no-gutters, align-content='center', style='height: 90px;')
  50. 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`')
  51. .headline.grey--text(:class='darkMode ? `text--lighten-2` : `text--darken-3`') {{title}}
  52. .caption.grey--text.text--darken-1 {{description}}
  53. v-divider
  54. v-container.pl-5.pt-4(fluid, grid-list-xl)
  55. v-layout(row)
  56. v-flex.page-col-sd(lg3, xl2, v-if='$vuetify.breakpoint.lgAndUp', style='margin-top: -90px;')
  57. v-card.mb-5(v-if='toc.length')
  58. .overline.pa-5.pb-0(:class='darkMode ? `blue--text text--lighten-2` : `primary--text`') {{$t('common:page.toc')}}
  59. v-list.pb-3(dense, nav, :class='darkMode ? `darken-3-d3` : ``')
  60. template(v-for='(tocItem, tocIdx) in toc')
  61. v-list-item(@click='$vuetify.goTo(tocItem.anchor, scrollOpts)')
  62. v-icon(color='grey', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }}
  63. v-list-item-title.px-3 {{tocItem.title}}
  64. //- v-divider(v-if='tocIdx < toc.length - 1 || tocItem.children.length')
  65. template(v-for='tocSubItem in tocItem.children')
  66. v-list-item(@click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)')
  67. v-icon.px-3(color='grey lighten-1', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }}
  68. v-list-item-title.px-3.caption.grey--text(:class='darkMode ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}}
  69. //- v-divider(inset, v-if='tocIdx < toc.length - 1')
  70. v-card.mb-5(v-if='tags.length > 0')
  71. .pa-5
  72. .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') Tags
  73. v-chip.mr-1.mb-1(
  74. label
  75. :color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`'
  76. v-for='(tag, idx) in tags'
  77. :href='`/t/` + tag.tag'
  78. :key='`tag-` + tag.tag'
  79. )
  80. v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', left, small) mdi-tag
  81. span(:class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`') {{tag.title}}
  82. v-chip.mr-1(
  83. label
  84. :color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`'
  85. :href='`/t/` + tags.map(t => t.tag).join(`/`)'
  86. )
  87. v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', size='20') mdi-tag-multiple
  88. v-card.mb-5
  89. .pa-5
  90. .overline.indigo--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
  91. span {{$t('common:page.lastEditedBy')}}
  92. //- v-spacer
  93. //- v-tooltip(top, v-if='isAuthenticated')
  94. //- template(v-slot:activator='{ on }')
  95. //- v-btn.btn-animate-edit(icon, :href='"/h/" + locale + "/" + path', v-on='on', x-small)
  96. //- v-icon(color='grey', dense) mdi-history
  97. //- span History
  98. .body-2.grey--text(:class='darkMode ? `` : `text--darken-3`') {{ authorName }}
  99. .caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
  100. //- v-card.mb-5
  101. //- .pa-5
  102. //- .overline.pb-2.yellow--text(:class='$vuetify.theme.dark ? `text--darken-3` : `text--darken-4`') Rating
  103. //- .text-center
  104. //- v-rating(
  105. //- v-model='rating'
  106. //- color='yellow darken-3'
  107. //- background-color='grey lighten-1'
  108. //- half-increments
  109. //- hover
  110. //- )
  111. //- .caption.grey--text 5 votes
  112. v-card(flat)
  113. v-toolbar(:color='darkMode ? `grey darken-4-d3` : `grey lighten-3`', flat, dense)
  114. v-spacer
  115. v-tooltip(bottom)
  116. template(v-slot:activator='{ on }')
  117. v-btn(icon, tile, v-on='on'): v-icon(color='grey') mdi-bookmark
  118. span {{$t('common:page.bookmark')}}
  119. v-menu(offset-y, bottom, min-width='300')
  120. template(v-slot:activator='{ on: menu }')
  121. v-tooltip(bottom)
  122. template(v-slot:activator='{ on: tooltip }')
  123. v-btn(icon, tile, v-on='{ ...menu, ...tooltip }'): v-icon(color='grey') mdi-share-variant
  124. span {{$t('common:page.share')}}
  125. social-sharing(
  126. :url='pageUrl'
  127. :title='title'
  128. :description='description'
  129. )
  130. v-tooltip(bottom)
  131. template(v-slot:activator='{ on }')
  132. v-btn(icon, tile, v-on='on', @click='print'): v-icon(color='grey') mdi-printer
  133. span {{$t('common:page.printFormat')}}
  134. v-spacer
  135. v-flex.page-col-content(xs12, lg9, xl10)
  136. v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='isAuthenticated')
  137. template(v-slot:activator='{ on: onEditActivator }')
  138. v-speed-dial(
  139. v-model='pageEditFab'
  140. direction='top'
  141. open-on-hover
  142. transition='scale-transition'
  143. bottom
  144. :right='!$vuetify.rtl'
  145. :left='$vuetify.rtl'
  146. fixed
  147. dark
  148. )
  149. template(v-slot:activator)
  150. v-btn.btn-animate-edit(
  151. fab
  152. color='primary'
  153. v-model='pageEditFab'
  154. @click='pageEdit'
  155. v-on='onEditActivator'
  156. )
  157. v-icon mdi-pencil
  158. v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl')
  159. template(v-slot:activator='{ on }')
  160. v-btn(
  161. fab
  162. small
  163. color='white'
  164. light
  165. v-on='on'
  166. @click='pageHistory'
  167. )
  168. v-icon(size='20') mdi-history
  169. span History
  170. v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl')
  171. template(v-slot:activator='{ on }')
  172. v-btn(
  173. fab
  174. small
  175. color='white'
  176. light
  177. v-on='on'
  178. @click='pageSource'
  179. )
  180. v-icon(size='20') mdi-code-tags
  181. span View Source
  182. v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl')
  183. template(v-slot:activator='{ on }')
  184. v-btn(
  185. fab
  186. small
  187. color='white'
  188. light
  189. v-on='on'
  190. @click='pageMove'
  191. )
  192. v-icon(size='20') mdi-content-save-move-outline
  193. span Move / Rename
  194. v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl')
  195. template(v-slot:activator='{ on }')
  196. v-btn(
  197. fab
  198. dark
  199. small
  200. color='red'
  201. v-on='on'
  202. @click='pageDelete'
  203. )
  204. v-icon(size='20') mdi-trash-can-outline
  205. span Delete
  206. span {{$t('common:page.editPage')}}
  207. .contents(ref='container')
  208. slot(name='contents')
  209. nav-footer
  210. notify
  211. search-results
  212. v-fab-transition
  213. v-btn(
  214. v-if='upBtnShown'
  215. fab
  216. fixed
  217. bottom
  218. :right='$vuetify.rtl'
  219. :left='!$vuetify.rtl'
  220. small
  221. :depressed='this.$vuetify.breakpoint.mdAndUp'
  222. @click='$vuetify.goTo(0, scrollOpts)'
  223. color='primary'
  224. dark
  225. :style='upBtnPosition'
  226. )
  227. v-icon mdi-arrow-up
  228. </template>
  229. <script>
  230. import { StatusIndicator } from 'vue-status-indicator'
  231. import Prism from 'prismjs'
  232. import { get } from 'vuex-pathify'
  233. import _ from 'lodash'
  234. import ClipboardJS from 'clipboard'
  235. Prism.plugins.autoloader.languages_path = '/js/prism/'
  236. Prism.plugins.NormalizeWhitespace.setDefaults({
  237. 'remove-trailing': true,
  238. 'remove-indent': true,
  239. 'left-trim': true,
  240. 'right-trim': true,
  241. 'remove-initial-line-feed': true,
  242. 'tabs-to-spaces': 2
  243. })
  244. Prism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => {
  245. let linkCopy = document.createElement('button')
  246. linkCopy.textContent = 'Copy'
  247. const clip = new ClipboardJS(linkCopy, {
  248. text: () => { return env.code }
  249. })
  250. clip.on('success', () => {
  251. linkCopy.textContent = 'Copied!'
  252. resetClipboardText()
  253. })
  254. clip.on('error', () => {
  255. linkCopy.textContent = 'Press Ctrl+C to copy'
  256. resetClipboardText()
  257. })
  258. return linkCopy
  259. function resetClipboardText() {
  260. setTimeout(() => {
  261. linkCopy.textContent = 'Copy'
  262. }, 5000)
  263. }
  264. })
  265. export default {
  266. components: {
  267. StatusIndicator
  268. },
  269. props: {
  270. pageId: {
  271. type: Number,
  272. default: 0
  273. },
  274. locale: {
  275. type: String,
  276. default: 'en'
  277. },
  278. path: {
  279. type: String,
  280. default: 'home'
  281. },
  282. title: {
  283. type: String,
  284. default: 'Untitled Page'
  285. },
  286. description: {
  287. type: String,
  288. default: ''
  289. },
  290. createdAt: {
  291. type: String,
  292. default: ''
  293. },
  294. updatedAt: {
  295. type: String,
  296. default: ''
  297. },
  298. tags: {
  299. type: Array,
  300. default: () => ([])
  301. },
  302. authorName: {
  303. type: String,
  304. default: 'Unknown'
  305. },
  306. authorId: {
  307. type: Number,
  308. default: 0
  309. },
  310. isPublished: {
  311. type: Boolean,
  312. default: false
  313. },
  314. toc: {
  315. type: Array,
  316. default: () => []
  317. },
  318. sidebar: {
  319. type: Array,
  320. default: () => []
  321. }
  322. },
  323. data() {
  324. return {
  325. navShown: false,
  326. navExpanded: false,
  327. upBtnShown: false,
  328. pageEditFab: false,
  329. scrollOpts: {
  330. duration: 1500,
  331. offset: 0,
  332. easing: 'easeInOutCubic'
  333. },
  334. scrollStyle: {
  335. vuescroll: {},
  336. scrollPanel: {
  337. initialScrollX: 0.01, // fix scrollbar not disappearing on load
  338. scrollingX: false,
  339. speed: 50
  340. },
  341. rail: {
  342. gutterOfEnds: '2px'
  343. },
  344. bar: {
  345. onlyShowBarOnScroll: false,
  346. background: '#42A5F5',
  347. hoverStyle: {
  348. background: '#64B5F6'
  349. }
  350. }
  351. },
  352. winWidth: 0
  353. }
  354. },
  355. computed: {
  356. darkMode: get('site/dark'),
  357. isAuthenticated: get('user/authenticated'),
  358. rating: {
  359. get () {
  360. return 3.5
  361. },
  362. set (val) {
  363. }
  364. },
  365. breadcrumbs() {
  366. return [{ path: '/', name: 'Home' }].concat(_.reduce(this.path.split('/'), (result, value, key) => {
  367. result.push({
  368. path: _.get(_.last(result), 'path', `/${this.locale}`) + `/${value}`,
  369. name: value
  370. })
  371. return result
  372. }, []))
  373. },
  374. pageUrl () { return window.location.href },
  375. upBtnPosition () {
  376. if (this.$vuetify.breakpoint.mdAndUp) {
  377. return this.$vuetify.rtl ? `right: 235px;` : `left: 235px;`
  378. } else {
  379. return this.$vuetify.rtl ? `right: 65px;` : `left: 65px;`
  380. }
  381. }
  382. },
  383. created() {
  384. this.$store.commit('page/SET_AUTHOR_ID', this.authorId)
  385. this.$store.commit('page/SET_AUTHOR_NAME', this.authorName)
  386. this.$store.commit('page/SET_CREATED_AT', this.createdAt)
  387. this.$store.commit('page/SET_DESCRIPTION', this.description)
  388. this.$store.commit('page/SET_IS_PUBLISHED', this.isPublished)
  389. this.$store.commit('page/SET_ID', this.pageId)
  390. this.$store.commit('page/SET_LOCALE', this.locale)
  391. this.$store.commit('page/SET_PATH', this.path)
  392. this.$store.commit('page/SET_TAGS', this.tags)
  393. this.$store.commit('page/SET_TITLE', this.title)
  394. this.$store.commit('page/SET_UPDATED_AT', this.updatedAt)
  395. this.$store.commit('page/SET_MODE', 'view')
  396. },
  397. mounted () {
  398. // -> Check side navigation visibility
  399. this.handleSideNavVisibility()
  400. window.addEventListener('resize', _.debounce(() => {
  401. this.handleSideNavVisibility()
  402. }, 500))
  403. // -> Highlight Code Blocks
  404. Prism.highlightAllUnder(this.$refs.container)
  405. // -> Handle anchor scrolling
  406. this.$nextTick(() => {
  407. if (window.location.hash && window.location.hash.length > 1) {
  408. this.$vuetify.goTo(window.location.hash, this.scrollOpts)
  409. }
  410. this.$refs.container.querySelectorAll(`a[href^="#"], a[href^="${window.location.href.replace(window.location.hash, '')}#"]`).forEach(el => {
  411. el.onclick = ev => {
  412. ev.preventDefault()
  413. ev.stopPropagation()
  414. this.$vuetify.goTo(ev.target.hash, this.scrollOpts)
  415. }
  416. })
  417. })
  418. },
  419. methods: {
  420. goHome () {
  421. window.location.assign('/')
  422. },
  423. toggleNavigation () {
  424. this.navOpen = !this.navOpen
  425. },
  426. upBtnScroll () {
  427. const scrollOffset = window.pageYOffset || document.documentElement.scrollTop
  428. this.upBtnShown = scrollOffset > window.innerHeight * 0.33
  429. },
  430. print () {
  431. window.print()
  432. },
  433. pageEdit () {
  434. this.$root.$emit('pageEdit')
  435. },
  436. pageHistory () {
  437. this.$root.$emit('pageHistory')
  438. },
  439. pageSource () {
  440. this.$root.$emit('pageSource')
  441. },
  442. pageMove () {
  443. this.$root.$emit('pageMove')
  444. },
  445. pageDelete () {
  446. this.$root.$emit('pageDelete')
  447. },
  448. handleSideNavVisibility () {
  449. if (window.innerWidth === this.winWidth) { return }
  450. this.winWidth = window.innerWidth
  451. if (this.$vuetify.breakpoint.mdAndUp) {
  452. this.navShown = true
  453. } else {
  454. this.navShown = false
  455. }
  456. }
  457. }
  458. }
  459. </script>
  460. <style lang="scss">
  461. .breadcrumbs-nav {
  462. .v-btn {
  463. min-width: 0;
  464. &__content {
  465. text-transform: none;
  466. }
  467. }
  468. .v-breadcrumbs__divider:nth-child(2n) {
  469. padding: 0 6px;
  470. }
  471. .v-breadcrumbs__divider:nth-child(2) {
  472. padding: 0 6px 0 12px;
  473. }
  474. }
  475. </style>