Browse Source

feat: TOC, scroll to header, page UI improvements

pull/760/head
Nicolas Giard 6 years ago
parent
commit
3abc254685
15 changed files with 1076 additions and 807 deletions
  1. 2
      client/client-app.js
  2. 5
      client/components/common/page-selector.vue
  3. 8
      client/components/editor.vue
  4. 2
      client/scss/components/markdown-content.scss
  5. 6
      client/scss/components/v-dialog.scss
  6. 2
      client/store/page.js
  7. 101
      client/themes/default/components/page.vue
  8. 3
      client/themes/default/scss/app.scss
  9. 100
      package.json
  10. 1
      server/db/migrations/2.0.0.js
  11. 33
      server/jobs/render-page.js
  12. 4
      server/models/pageHistory.js
  13. 13
      server/models/pages.js
  14. 1
      server/views/page.pug
  15. 1602
      yarn.lock

2
client/client-app.js

@ -21,7 +21,6 @@ import Hammer from 'hammerjs'
import moment from 'moment'
import VueMoment from 'vue-moment'
import VueTour from 'vue-tour'
import VueTreeNavigation from 'vue-tree-navigation'
import store from './store'
import Cookies from 'js-cookie'
@ -149,7 +148,6 @@ Vue.use(VeeValidate, { events: '' })
Vue.use(Vuetify)
Vue.use(VueMoment, { moment })
Vue.use(VueTour)
Vue.use(VueTreeNavigation)
Vue.prototype.Velocity = Velocity

5
client/components/common/page-selector.vue

@ -1,7 +1,8 @@
<template lang="pug">
v-dialog(v-model='isShown', lazy, max-width='850px')
v-card.page-selector
.dialog-header
.dialog-header.is-dark
v-icon.mr-2(color='white') find_in_page
span Select Page Location
v-spacer
v-progress-circular(
@ -59,7 +60,7 @@
v-list-tile
v-list-tile-avatar: v-icon insert_drive_file
v-list-tile-title File D
v-card-text.grey.lighten-2.pa-2
v-card-text.grey.lighten-1.pa-2
v-text-field(
solo
hide-details

8
client/components/editor.vue

@ -224,8 +224,8 @@ export default {
isPrivate: false,
isPublished: this.$store.get('page/isPublished'),
path: this.$store.get('page/path'),
publishEndDate: this.$store.get('page/publishEndDate'),
publishStartDate: this.$store.get('page/publishStartDate'),
publishEndDate: this.$store.get('page/publishEndDate') || '',
publishStartDate: this.$store.get('page/publishStartDate') || '',
tags: this.$store.get('page/tags'),
title: this.$store.get('page/title')
}
@ -258,8 +258,8 @@ export default {
isPrivate: false,
isPublished: this.$store.get('page/isPublished'),
path: this.$store.get('page/path'),
publishEndDate: this.$store.get('page/publishEndDate'),
publishStartDate: this.$store.get('page/publishStartDate'),
publishEndDate: this.$store.get('page/publishEndDate') || '',
publishStartDate: this.$store.get('page/publishStartDate') || '',
tags: this.$store.get('page/tags'),
title: this.$store.get('page/title')
}

2
client/scss/components/markdown-content.scss

@ -12,7 +12,7 @@
}
> * + h1, > * + h2, > * + h3, > * + h4 {
margin-top: 1rem;
margin-top: 3rem;
}
h1 {
font-size: 1.5rem;

6
client/scss/components/v-dialog.scss

@ -14,4 +14,10 @@
background: radial-gradient(ellipse at top, mc('red', '500'), mc('red', '700')),
radial-gradient(ellipse at bottom, mc('red', '800'), mc('red', '700'));
}
&.is-dark {
background-color: mc('grey', '900');
background: radial-gradient(ellipse at top, mc('grey', '800'), mc('grey', '900')),
radial-gradient(ellipse at bottom, mc('grey', '800'), mc('grey', '900'));
}
}

2
client/store/page.js

@ -9,6 +9,8 @@ const state = {
isPublished: true,
locale: 'en',
path: '',
publishEndDate: '',
publishStartDate: '',
tags: [],
title: '',
updatedAt: ''

101
client/themes/default/components/page.vue

@ -1,5 +1,5 @@
<template lang="pug">
v-app
v-app(v-scroll='upBtnScroll')
nav-header
v-navigation-drawer.primary(
dark
@ -15,23 +15,24 @@
slot(name='sidebar')
v-content
v-toolbar(color='grey lighten-3', flat, dense)
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(
v-else
:items='breadcrumbs'
divider='/'
)
template(slot='item', slot-scope='props')
v-icon(v-if='props.item.path === "/"', small) home
v-btn.ma-0(v-else, :href='props.item.path', small, flat) {{props.item.name}}
template(v-if='!isPublished')
v-spacer
.caption.red--text Unpublished
status-indicator.ml-3(negative, pulse)
v-divider
template(v-if='path !== `home`')
v-toolbar(color='grey lighten-3', flat, dense)
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(
v-else
:items='breadcrumbs'
divider='/'
)
template(slot='item', slot-scope='props')
v-icon(v-if='props.item.path === "/"', small) home
v-btn.ma-0(v-else, :href='props.item.path', small, flat) {{props.item.name}}
template(v-if='!isPublished')
v-spacer
.caption.red--text Unpublished
status-indicator.ml-3(negative, pulse)
v-divider
v-layout(row)
v-flex(xs12, lg9, xl10)
v-toolbar(color='grey lighten-4', flat, :height='90')
@ -54,10 +55,20 @@
v-icon(color='grey') edit
span Edit Page
v-divider
v-list.grey.lighten-3.pb-3(dense)
v-subheader.pl-4.primary--text Table of contents
vue-tree-navigation.treenav(:items='toc', :defaultOpenLevel='1')
v-divider
template(v-if='toc.length')
v-list.grey.lighten-3.pb-3(dense)
v-subheader.pl-4.primary--text Table of contents
template(v-for='(tocItem, tocIdx) in toc')
v-list-tile(@click='$vuetify.goTo(tocItem.anchor, scrollOpts)')
v-icon(color='grey') arrow_right
v-list-tile-title.pl-3 {{tocItem.title}}
v-divider.ml-4(v-if='tocIdx < toc.length - 1 || tocItem.children.length')
template(v-for='tocSubItem in tocItem.children')
v-list-tile(@click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)')
v-icon.pl-3(color='grey lighten-1') arrow_right
v-list-tile-title.pl-3.caption {{tocSubItem.title}}
v-divider(inset, v-if='tocIdx < toc.length - 1')
v-divider
v-list.grey.lighten-4(dense)
v-subheader.pl-4.yellow--text.text--darken-4 Rating
.text-xs-center
@ -97,6 +108,9 @@
span Print Format
v-spacer
nav-footer
v-fab-transition
v-btn(v-if='upBtnShown', fab, fixed, bottom, right, small, @click='$vuetify.goTo(0, scrollOpts)', color='primary')
v-icon arrow_upward
</template>
<script>
@ -147,46 +161,27 @@ export default {
isPublished: {
type: Boolean,
default: false
},
toc: {
type: Array,
default: () => []
}
},
data() {
return {
navOpen: false,
upBtnShown: false,
scrollOpts: {
duration: 1500,
offset: -75,
easing: 'easeInOutCubic'
},
breadcrumbs: [
{ path: '/', name: 'Home' },
{ path: '/universe', name: 'Universe' },
{ path: '/universe/galaxy', name: 'Galaxy' },
{ path: '/universe/galaxy/solar-system', name: 'Solar System' },
{ path: '/universe/galaxy/solar-system/planet-earth', name: 'Planet Earth' }
],
toc: [
{
name: 'Introduction',
element: 'introduction'
},
{
name: 'Cities',
element: 'cities',
children: [
{
name: 'New York',
element: 'contact',
children: [
{ name: 'E-mail', element: 'email' },
{ name: 'Phone', element: 'phone' }
]
},
{
name: 'Chicago',
element: 'contact',
children: [
{ name: 'E-mail', element: 'email' },
{ name: 'Phone', element: 'phone' }
]
}
]
},
{ name: 'Population', external: 'https://github.com' }
]
}
},
@ -222,6 +217,10 @@ export default {
methods: {
toggleNavigation () {
this.navOpen = !this.navOpen
},
upBtnScroll () {
const scrollOffset = window.pageYOffset || document.documentElement.scrollTop
this.upBtnShown = scrollOffset > window.innerHeight * 0.33
}
}
}

3
client/themes/default/scss/app.scss

@ -2,6 +2,7 @@
.contents {
color: mc('grey', '800');
padding-bottom: 50px;
h1, h2, h3, h4, h5, h6 {
position: relative;
@ -26,7 +27,7 @@
h1 {
padding-left: 24px;
color: mc('blue', '800');
margin-top: 1rem;
margin-top: 2rem;
position: relative;
&::after {

100
package.json

@ -42,15 +42,15 @@
"node": ">=10.10"
},
"dependencies": {
"apollo-server": "2.1.0",
"apollo-server-express": "2.1.0",
"auto-load": "3.0.1",
"apollo-server": "2.2.2",
"apollo-server-express": "2.2.2",
"auto-load": "3.0.4",
"axios": "0.18.0",
"bcryptjs-then": "1.0.1",
"bluebird": "3.5.2",
"bluebird": "3.5.3",
"body-parser": "1.18.3",
"bugsnag": "2.4.3",
"bull": "3.4.8",
"bull": "3.5.2",
"chalk": "2.4.1",
"cheerio": "1.0.0-rc.2",
"child-process-promise": "2.2.1",
@ -58,34 +58,34 @@
"compression": "1.7.3",
"connect-redis": "3.4.0",
"cookie-parser": "1.4.3",
"cors": "2.8.4",
"cors": "2.8.5",
"dependency-graph": "0.7.2",
"diff2html": "2.4.0",
"diff2html": "2.5.0",
"dotize": "^0.2.0",
"execa": "1.0.0",
"express": "4.16.4",
"express-brute": "1.0.1",
"express-brute-redis": "0.0.1",
"express-session": "1.15.6",
"file-type": "10.2.0",
"file-type": "10.4.0",
"filesize": "3.6.1",
"follow-redirects": "1.5.9",
"fs-extra": "7.0.0",
"fs-extra": "7.0.1",
"getos": "3.1.0",
"graphql": "14.0.2",
"graphql-list-fields": "2.0.2",
"graphql-subscriptions": "1.0.0",
"graphql-tools": "4.0.2",
"graphql-tools": "4.0.3",
"highlight.js": "9.13.1",
"i18next": "12.0.0",
"i18next-express-middleware": "1.4.1",
"i18next-express-middleware": "1.5.0",
"i18next-localstorage-cache": "1.1.1",
"i18next-node-fs-backend": "2.1.0",
"image-size": "0.6.3",
"ioredis": "4.2.0",
"js-binary": "1.2.0",
"js-yaml": "3.12.0",
"jsonwebtoken": "8.3.0",
"jsonwebtoken": "8.4.0",
"klaw": "3.0.0",
"knex": "0.15.2",
"lodash": "4.17.11",
@ -106,11 +106,11 @@
"mathjax-node": "2.1.1",
"mime-types": "2.1.21",
"moment": "2.22.2",
"moment-timezone": "0.5.21",
"mongodb": "3.1.8",
"mssql": "4.2.2",
"moment-timezone": "0.5.23",
"mongodb": "3.1.10",
"mssql": "4.2.3",
"multer": "1.4.1",
"mysql2": "1.6.1",
"mysql2": "1.6.4",
"node-2fa": "1.1.2",
"node-cache": "4.2.0",
"oauth2orize": "1.11.0",
@ -135,7 +135,7 @@
"passport-slack": "0.0.7",
"passport-twitch": "1.0.3",
"passport-windowslive": "1.0.2",
"pg": "7.6.0",
"pg": "7.6.1",
"pg-hstore": "2.3.2",
"pm2": "3.2.2",
"pug": "2.0.3",
@ -148,20 +148,20 @@
"scim-query-filter-parser": "1.1.0",
"semver": "5.6.0",
"serve-favicon": "2.5.0",
"sqlite3": "4.0.3",
"sqlite3": "4.0.4",
"subscriptions-transport-ws": "0.9.15",
"uslug": "1.0.4",
"uuid": "3.3.2",
"validator": "10.8.0",
"validator": "10.9.0",
"validator-as-promised": "1.0.2",
"winston": "3.1.0",
"yargs": "12.0.2"
"yargs": "12.0.4"
},
"devDependencies": {
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2",
"@babel/cli": "^7.1.5",
"@babel/core": "^7.1.6",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-decorators": "^7.1.2",
"@babel/plugin-proposal-decorators": "^7.1.6",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@babel/plugin-proposal-function-sent": "^7.1.0",
"@babel/plugin-proposal-json-strings": "^7.0.0",
@ -170,20 +170,20 @@
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-env": "^7.1.6",
"@panter/vue-i18next": "0.13.0",
"@vue/cli": "3.0.5",
"@vue/cli": "3.1.3",
"animated-number-vue": "0.1.3",
"apollo-cache-inmemory": "1.3.7",
"apollo-client": "2.4.4",
"apollo-cache-inmemory": "1.3.10",
"apollo-client": "2.4.6",
"apollo-fetch": "0.7.0",
"apollo-link": "1.2.3",
"apollo-link-batch-http": "1.2.3",
"apollo-link-error": "1.1.1",
"apollo-link-http": "1.5.5",
"apollo-link-persisted-queries": "0.2.1",
"apollo-link-persisted-queries": "0.2.2",
"apollo-link-ws": "1.0.9",
"apollo-utilities": "1.0.24",
"apollo-utilities": "1.0.25",
"autoprefixer": "9.3.1",
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
@ -192,15 +192,16 @@
"babel-plugin-lodash": "3.3.4",
"babel-plugin-transform-imports": "1.5.1",
"brace": "0.11.1",
"cache-loader": "1.2.2",
"cache-loader": "1.2.5",
"chart.js": "2.7.3",
"clean-webpack-plugin": "0.1.19",
"copy-webpack-plugin": "4.5.4",
"css-loader": "1.0.0",
"clean-webpack-plugin": "1.0.0",
"copy-webpack-plugin": "4.6.0",
"core-js": "2.5.7",
"css-loader": "1.0.1",
"cssnano": "4.1.7",
"duplicate-package-checker-webpack-plugin": "3.0.0",
"epic-spinners": "1.0.4",
"eslint": "5.8.0",
"eslint": "5.9.0",
"eslint-config-requarks": "1.0.7",
"eslint-config-standard": "12.0.0",
"eslint-plugin-import": "2.14.0",
@ -210,7 +211,7 @@
"eslint-plugin-vue": "4.7.1",
"file-loader": "2.0.0",
"filesize.js": "1.0.2",
"grapesjs": "0.14.33",
"grapesjs": "0.14.40",
"graphiql": "0.12.0",
"graphql-persisted-document-loader": "1.0.1",
"graphql-tag": "^2.10.0",
@ -223,62 +224,61 @@
"js-cookie": "2.2.0",
"mini-css-extract-plugin": "0.4.4",
"node-sass": "4.9.4",
"offline-plugin": "5.0.5",
"offline-plugin": "5.0.6",
"optimize-css-assets-webpack-plugin": "5.0.1",
"postcss-cssnext": "3.1.0",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-flexibility": "2.0.0",
"postcss-import": "12.0.1",
"postcss-loader": "3.0.0",
"postcss-preset-env": "6.3.0",
"postcss-preset-env": "6.4.0",
"postcss-selector-parser": "5.0.0-rc.4",
"pug-lint": "2.5.0",
"pug-loader": "2.4.0",
"pug-plain-loader": "1.0.0",
"raw-loader": "0.5.1",
"react": "16.6.0",
"react-dom": "16.6.0",
"react": "16.6.3",
"react-dom": "16.6.3",
"resolve-url-loader": "3.0.0",
"sass-loader": "7.1.0",
"sass-resources-loader": "1.3.4",
"script-ext-html-webpack-plugin": "2.0.1",
"sass-resources-loader": "2.0.0",
"script-ext-html-webpack-plugin": "2.1.3",
"simple-progress-webpack-plugin": "1.1.2",
"style-loader": "0.23.1",
"stylus": "0.54.5",
"stylus-loader": "3.0.2",
"twemoji-awesome": "1.0.6",
"url-loader": "1.1.2",
"vee-validate": "2.1.1",
"vee-validate": "2.1.3",
"velocity-animate": "1.5.2",
"viz.js": "2.0.0",
"viz.js": "2.1.1",
"vue": "2.5.17",
"vue-apollo": "3.0.0-beta.25",
"vue-apollo": "3.0.0-beta.26",
"vue-chartjs": "3.4.0",
"vue-clipboards": "1.2.4",
"vue-codemirror": "4.0.5",
"vue-codemirror": "4.0.6",
"vue-hot-reload-api": "2.3.1",
"vue-loader": "15.4.2",
"vue-material-design-icons": "2.3.0",
"vue-material-design-icons": "2.4.0",
"vue-moment": "4.0.0",
"vue-router": "3.0.1",
"vue-simple-breakpoints": "1.0.3",
"vue-status-indicator": "1.1.1",
"vue-template-compiler": "2.5.17",
"vue-tour": "1.1.0",
"vue-tree-navigation": "3.0.1",
"vue2-animate": "2.1.0",
"vuedraggable": "2.16.0",
"vuetify": "1.3.3",
"vuetify": "1.3.8",
"vuex": "3.0.1",
"vuex-pathify": "1.1.3",
"vuex-persistedstate": "2.5.4",
"webpack": "4.23.1",
"webpack": "4.25.1",
"webpack-bundle-analyzer": "3.0.3",
"webpack-cli": "3.1.2",
"webpack-dev-middleware": "3.4.0",
"webpack-hot-middleware": "2.24.3",
"webpack-merge": "4.1.4",
"webpack-subresource-integrity": "1.2.0",
"webpack-subresource-integrity": "1.3.0",
"whatwg-fetch": "3.0.0",
"write-file-webpack-plugin": "4.4.1",
"xterm": "3.8.0"

1
server/db/migrations/2.0.0.js

@ -116,6 +116,7 @@ exports.up = knex => {
table.string('publishEndDate')
table.text('content')
table.text('render')
table.json('toc')
table.string('contentType').notNullable()
table.string('createdAt').notNullable()
table.string('updatedAt').notNullable()

33
server/jobs/render-page.js

@ -1,6 +1,7 @@
require('../core/worker')
const _ = require('lodash')
const cheerio = require('cheerio')
/* global WIKI */
@ -21,15 +22,43 @@ module.exports = async (job) => {
})
}
// Parse TOC
const $ = cheerio.load(output)
let isStrict = $('h1').length > 0 // <- Allows for documents using H2 as top level
let toc = { root: [] }
$('h1,h2,h3,h4,h5,h6').each((idx, el) => {
const depth = _.toSafeInteger(el.name.substring(1)) - (isStrict ? 1 : 2)
const leafPath = _.reduce(_.times(depth), (curPath, curIdx) => {
if (_.has(toc, curPath)) {
const lastLeafIdx = _.get(toc, curPath).length - 1
curPath = `${curPath}[${lastLeafIdx}].children`
}
return curPath
}, 'root')
const leafSlug = $('.toc-anchor', el).first().attr('href')
$('.toc-anchor', el).remove()
_.get(toc, leafPath).push({
title: _.trim($(el).text()),
anchor: leafSlug,
children: []
})
})
// Save to DB
await WIKI.models.pages.query()
.patch({ render: output })
.patch({
render: output,
toc: JSON.stringify(toc.root)
})
.where('id', job.data.page.id)
// Save to cache
await WIKI.models.pages.savePageToCache({
...job.data.page,
render: output
render: output,
toc: JSON.stringify(toc.root)
})
WIKI.logger.info(`Rendering page ${job.data.page.path}: [ COMPLETED ]`)

4
server/models/pageHistory.js

@ -96,8 +96,8 @@ module.exports = class PageHistory extends Model {
isPublished: opts.isPublished,
localeCode: opts.localeCode,
path: opts.path,
publishEndDate: opts.publishEndDate,
publishStartDate: opts.publishStartDate,
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
title: opts.title
})
}

13
server/models/pages.js

@ -108,6 +108,7 @@ module.exports = class Page extends Model {
publishStartDate: 'string',
render: 'string',
title: 'string',
toc: 'string',
updatedAt: 'string'
})
}
@ -125,9 +126,10 @@ module.exports = class Page extends Model {
isPublished: opts.isPublished,
localeCode: opts.locale,
path: opts.path,
publishEndDate: opts.publishEndDate,
publishStartDate: opts.publishStartDate,
title: opts.title
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
title: opts.title,
toc: '[]'
})
const page = await WIKI.models.pages.getPageFromDb({
path: opts.path,
@ -154,8 +156,8 @@ module.exports = class Page extends Model {
content: opts.content,
description: opts.description,
isPublished: opts.isPublished,
publishEndDate: opts.publishEndDate,
publishStartDate: opts.publishStartDate,
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
title: opts.title
}).where('id', ogPage.id)
const page = await WIKI.models.pages.getPageFromDb({
@ -243,6 +245,7 @@ module.exports = class Page extends Model {
publishStartDate: page.publishStartDate,
render: page.render,
title: page.title,
toc: page.toc,
updatedAt: page.updatedAt
}))
}

1
server/views/page.pug

@ -15,6 +15,7 @@ block body
author-name=page.authorName
:author-id=page.authorId
:is-published=page.isPublished
:toc=page.toc
)
template(slot='sidebar')
each navItem in sidebar

1602
yarn.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save