Browse Source

feat: markdown editor toolbar + default group rules fix

pull/835/head
Nick 5 years ago
parent
commit
ca4e0ada30
6 changed files with 240 additions and 52 deletions
  1. 3
      client/components/editor.vue
  2. 184
      client/components/editor/editor-markdown.vue
  3. 86
      client/components/editor/editor-modal-blocks.vue
  4. 2
      client/components/editor/editor-modal-media.vue
  5. 15
      server/app/data.yml
  6. 2
      server/graph/resolvers/group.js

3
client/components/editor.vue

@ -67,7 +67,8 @@ export default {
editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.vue'), editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.vue'),
editorModalProperties: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-properties.vue'), editorModalProperties: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-properties.vue'),
editorModalUnsaved: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-unsaved.vue'), editorModalUnsaved: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-unsaved.vue'),
editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-media.vue')
editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-media.vue'),
editorModalBlocks: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-blocks.vue')
}, },
props: { props: {
locale: { locale: {

184
client/components/editor/editor-markdown.vue

@ -1,91 +1,130 @@
<template lang='pug'> <template lang='pug'>
.editor-markdown .editor-markdown
v-toolbar.editor-markdown-toolbar(dense, color='primary', dark, flat) v-toolbar.editor-markdown-toolbar(dense, color='primary', dark, flat)
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='toggleMarkup({ start: `**` })').mx-0
v-icon format_bold v-icon format_bold
span Bold span Bold
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='toggleMarkup({ start: `*` })').mx-0
v-icon format_italic v-icon format_italic
span Italic span Italic
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='toggleMarkup({ start: `~~` })').mx-0
v-icon format_strikethrough v-icon format_strikethrough
span Strikethrough span Strikethrough
v-menu(offset-y, open-on-hover) v-menu(offset-y, open-on-hover)
v-btn(icon, slot='activator').mx-0 v-btn(icon, slot='activator').mx-0
v-icon font_download
v-list
v-list-tile(v-for='(n, idx) in 6', @click='', :key='idx')
v-list-tile-action
v-icon font_download
v-list-tile-title Heading {{n}}
v-tooltip(top)
v-icon text_fields
v-list.py-0
template(v-for='(n, idx) in 6')
v-list-tile(@click='setHeaderLine(n)', :key='idx')
v-list-tile-action
v-icon(:size='24 - (idx - 1) * 2') title
v-list-tile-title Heading {{n}}
v-divider(v-if='idx < 5')
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='toggleMarkup({ start: `~` })').mx-0
v-icon vertical_align_bottom
span Subscript
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='toggleMarkup({ start: `^` })').mx-0
v-icon vertical_align_top
span Superscript
v-menu(offset-y, open-on-hover)
v-btn(icon, slot='activator').mx-0 v-btn(icon, slot='activator').mx-0
v-icon format_quote v-icon format_quote
span Blockquote
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-list.py-0
v-list-tile(@click='insertBeforeEachLine({ content: `> `})')
v-list-tile-action
v-icon format_quote
v-list-tile-title Blockquote
v-divider
v-list-tile(@click='insertBeforeEachLine({ content: `> `, after: `{.is-info}`})')
v-list-tile-action
v-icon(color='blue') format_quote
v-list-tile-title Info Blockquote
v-divider
v-list-tile(@click='insertBeforeEachLine({ content: `> `, after: `{.is-success}`})')
v-list-tile-action
v-icon(color='success') format_quote
v-list-tile-title Success Blockquote
v-divider
v-list-tile(@click='insertBeforeEachLine({ content: `> `, after: `{.is-warning}`})')
v-list-tile-action
v-icon(color='warning') format_quote
v-list-tile-title Warning Blockquote
v-divider
v-list-tile(@click='insertBeforeEachLine({ content: `> `, after: `{.is-error}`})')
v-list-tile-action
v-icon(color='error') format_quote
v-list-tile-title Error Blockquote
v-divider
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='insertBeforeEachLine({ content: `- `})').mx-0
v-icon format_list_bulleted v-icon format_list_bulleted
span Unordered List span Unordered List
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='insertBeforeEachLine({ content: `1. `})').mx-0
v-icon format_list_numbered v-icon format_list_numbered
span Ordered List span Ordered List
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-icon insert_link
span Link
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='toggleMarkup({ start: "`" })').mx-0
v-icon space_bar v-icon space_bar
span Inline Code span Inline Code
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-icon code
span Code Block
v-tooltip(top)
v-btn(icon, slot='activator').mx-0
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='toggleMarkup({ start: `<kbd>`, end: `</kbd>` })').mx-0
v-icon font_download
span Keyboard Key
v-tooltip(bottom, color='primary')
v-btn(icon, slot='activator', @click='insertAfter({ content: `---`, newLine: true })').mx-0
v-icon remove v-icon remove
span Horizontal Bar span Horizontal Bar
.editor-markdown-main .editor-markdown-main
.editor-markdown-sidebar .editor-markdown-sidebar
v-tooltip(right, color='primary')
v-btn(icon, slot='activator', dark).mx-0
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, disabled).mx-0
v-icon link v-icon link
span Insert Link span Insert Link
v-tooltip(right)
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, @click='toggleModal(`editorModalMedia`)').mx-0 v-btn(icon, slot='activator', dark, @click='toggleModal(`editorModalMedia`)').mx-0
v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') image v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') image
span Insert Image span Insert Image
v-tooltip(right, color='primary')
v-btn(icon, slot='activator', dark).mx-0
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, @click='toggleModal(`editorModalBlocks`)').mx-0
v-icon(:color='activeModal === `editorModalBlocks` ? `teal` : ``') dashboard
span Insert Block
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, disabled).mx-0
v-icon insert_drive_file v-icon insert_drive_file
span Insert File span Insert File
v-tooltip(right, color='primary')
v-btn(icon, slot='activator', dark).mx-0
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, disabled).mx-0
v-icon code
span Insert Code Block
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, disabled).mx-0
v-icon play_circle_outline v-icon play_circle_outline
span Insert Video / Audio span Insert Video / Audio
v-tooltip(right, color='primary')
v-btn(icon, slot='activator', dark).mx-0
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, disabled).mx-0
v-icon multiline_chart v-icon multiline_chart
span Insert Diagram span Insert Diagram
v-tooltip(right, color='primary')
v-btn(icon, slot='activator', dark).mx-0
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, disabled).mx-0
v-icon functions v-icon functions
span Insert Math Expression span Insert Math Expression
v-tooltip(right, color='primary')
v-btn(icon, slot='activator', dark).mx-0
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, disabled).mx-0
v-icon border_outer v-icon border_outer
span Table Helper span Table Helper
v-spacer v-spacer
v-tooltip(right, color='primary')
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, @click='toggleFullscreen').mx-0 v-btn(icon, slot='activator', dark, @click='toggleFullscreen').mx-0
v-icon crop_free v-icon crop_free
span Fullscreen Editor
v-tooltip(right, color='primary')
v-btn(icon, slot='activator', dark).mx-0
span Distraction Free Mode
v-tooltip(right, color='teal')
v-btn(icon, slot='activator', dark, disabled).mx-0
v-icon help v-icon help
span Markdown Formatting Help span Markdown Formatting Help
.editor-markdown-editor .editor-markdown-editor
@ -273,6 +312,57 @@ export default {
// console.info(token) // console.info(token)
}, },
toggleMarkup({ start, end }) {
if (!end) { end = start }
if (!this.cm.doc.somethingSelected()) {
return this.$store.commit('showNotification', {
message: 'You must select something first!',
style: 'warning',
icon: 'warning'
})
}
this.cm.doc.replaceSelections(this.cm.doc.getSelections().map(s => start + s + end))
},
setHeaderLine(lvl) {
const curLine = this.cm.doc.getCursor('head').line
let lineContent = this.cm.doc.getLine(curLine)
const lineLength = lineContent.length
if (_.startsWith(lineContent, '#')) {
lineContent = lineContent.replace(/^(#+ )/, '')
}
lineContent = _.times(lvl, n => '#').join('') + ` ` + lineContent
this.cm.doc.replaceRange(lineContent, { line: curLine, ch: 0 }, { line: curLine, ch: lineLength })
},
insertAfter({ content, newLine }) {
const curLine = this.cm.doc.getCursor('to').line
const lineLength = this.cm.doc.getLine(curLine).length
this.cm.doc.replaceRange(newLine ? `\n${content}\n` : content, { line: curLine, ch: lineLength + 1 })
},
insertBeforeEachLine({ content, after }) {
let lines = []
if (!this.cm.doc.somethingSelected()) {
lines.push(this.cm.doc.getCursor('head').line)
} else {
lines = _.flatten(this.cm.doc.listSelections().map(sl => {
const range = Math.abs(sl.anchor.line - sl.head.line) + 1
const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line
return _.times(range, l => l + lowestLine)
}))
}
lines.forEach(ln => {
let lineContent = this.cm.doc.getLine(ln)
const lineLength = lineContent.length
if (_.startsWith(lineContent, content)) {
lineContent = lineContent.substring(content.length)
}
this.cm.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength })
})
if (after) {
const lastLine = _.last(lines)
this.cm.doc.replaceRange(`\n${after}\n`, { line: lastLine, ch: this.cm.doc.getLine(lastLine).length + 1 })
}
},
/** /**
* Update scroll sync * Update scroll sync
*/ */

86
client/components/editor/editor-modal-blocks.vue

@ -0,0 +1,86 @@
<template lang='pug'>
v-card.editor-modal-blocks.animated.fadeInLeft(flat, tile)
v-container.pa-3(grid-list-lg, fluid)
v-layout(row, wrap)
v-flex(xs3)
v-card.radius-7(light)
v-card-text
.d-flex
v-toolbar.radius-7(color='teal lighten-5', dense, flat, height='44')
.body-2.teal--text Blocks
v-list(two-line)
template(v-for='(item, idx) of blocks')
v-list-tile(@click='selectBlock(item)')
v-list-tile-avatar
v-avatar.radius-7(color='teal')
v-icon(dark) dashboard
v-list-tile-content
v-list-tile-title.body-2 {{item.title}}
v-list-tile-sub-title {{item.description}}
v-list-tile-avatar(v-if='block.key === item.key')
v-icon.animated.fadeInLeft(color='teal') arrow_forward_ios
v-divider(v-if='idx < blocks.length - 1')
v-flex(xs3)
v-card.radius-7.animated.fadeInLeft(light, v-if='block.key')
v-card-text
v-toolbar.radius-7(color='teal lighten-5', dense, flat)
v-icon.mr-3(color='teal') dashboard
.body-2.teal--text {{block.title}}
.d-flex.mt-3
v-toolbar.radius-7(flat, color='grey lighten-4', dense, height='44')
.body-2 Coming soon
v-btn.ml-3.my-0.mr-0.radius-7(color='teal', large, @click='', disabled)
v-icon(left) save_alt
span Insert
</template>
<script>
import _ from 'lodash'
import { sync } from 'vuex-pathify'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
blocks: [
{ key: 'hero', title: 'Hero', description: 'A large banner with a title.' },
{ key: 'toc', title: 'Table of Contents', description: 'A list of children pages.' }
// { key: 'form', title: 'Form', description: '' }
],
block: {
key: false
}
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
activeModal: sync('editor/activeModal')
},
methods: {
selectBlock (item) {
this.block = _.cloneDeep(item)
}
}
}
</script>
<style lang='scss'>
.editor-modal-blocks {
position: fixed;
top: 112px;
left: 64px;
z-index: 10;
width: calc(100vw - 64px - 17px);
height: calc(100vh - 112px - 24px);
background-color: rgba(darken(mc('grey', '900'), 3%), .9) !important;
}
</style>

2
client/components/editor/editor-modal-media.vue

@ -18,7 +18,7 @@
template(v-for='(item, idx) of [1,2,3,4,5,6,7,8,9,10]') template(v-for='(item, idx) of [1,2,3,4,5,6,7,8,9,10]')
v-list-tile(@click='') v-list-tile(@click='')
v-list-tile-avatar v-list-tile-avatar
v-avatar.radius-7(color='teal', tile)
v-avatar.radius-7(color='teal')
v-icon(dark) image v-icon(dark) image
v-list-tile-content v-list-tile-content
v-list-tile-title Image {{item}} v-list-tile-title Image {{item}}

15
server/app/data.yml

@ -63,10 +63,21 @@ jobs:
schedule: P1D schedule: P1D
groups: groups:
defaultPermissions: defaultPermissions:
- 'manage:pages'
- 'write:assets'
- 'read:pages'
- 'read:assets'
- 'read:comments' - 'read:comments'
- 'write:comments' - 'write:comments'
defaultPageRules:
- id: default
deny: false
match: START
roles:
- 'read:pages'
- 'read:assets'
- 'read:comments'
- 'write:comments'
path: ''
locales: []
telemetry: telemetry:
BUGSNAG_ID: 'bb4b324d0675bcbba10025617fd2cec8' BUGSNAG_ID: 'bb4b324d0675bcbba10025617fd2cec8'
BUGSNAG_REMOTE: 'https://notify.bugsnag.com' BUGSNAG_REMOTE: 'https://notify.bugsnag.com'

2
server/graph/resolvers/group.js

@ -43,7 +43,7 @@ module.exports = {
const group = await WIKI.models.groups.query().insertAndFetch({ const group = await WIKI.models.groups.query().insertAndFetch({
name: args.name, name: args.name,
permissions: JSON.stringify(WIKI.data.groups.defaultPermissions), permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
pageRules: JSON.stringify([]),
pageRules: JSON.stringify(WIKI.data.groups.defaultPageRules),
isSystem: false isSystem: false
}) })
await WIKI.auth.reloadGroups() await WIKI.auth.reloadGroups()

Loading…
Cancel
Save