mirror of https://github.com/Requarks/wiki.git
10 changed files with 886 additions and 265 deletions
Split View
Diff Options
-
6client/app.js
-
52client/components/editor.vue
-
251client/components/editor/editor-code.vue
-
497client/components/editor/editor-markdown.vue
-
62client/components/editor/editor-wysiwyg.vue
-
16dev/webpack/webpack.dev.js
-
2package.json
-
2server/modules/editor/markdown/definition.yml
-
6server/modules/editor/wysiwyg/definition.yml
-
257yarn.lock
@ -0,0 +1,497 @@ |
|||
<template lang='pug'> |
|||
.editor-code |
|||
v-toolbar.editor-code-toolbar(dense, color='primary', dark) |
|||
v-tooltip(top) |
|||
v-btn(icon, slot='activator').mx-0 |
|||
v-icon format_bold |
|||
span Bold |
|||
v-tooltip(top) |
|||
v-btn(icon, slot='activator').mx-0 |
|||
v-icon format_italic |
|||
span Italic |
|||
v-tooltip(top) |
|||
v-btn(icon, slot='activator').mx-0 |
|||
v-icon format_strikethrough |
|||
span Strikethrough |
|||
v-menu(offset-y, open-on-hover) |
|||
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-btn(icon, slot='activator').mx-0 |
|||
v-icon format_quote |
|||
span Blockquote |
|||
v-tooltip(top) |
|||
v-btn(icon, slot='activator').mx-0 |
|||
v-icon format_list_bulleted |
|||
span Unordered List |
|||
v-tooltip(top) |
|||
v-btn(icon, slot='activator').mx-0 |
|||
v-icon format_list_numbered |
|||
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-icon space_bar |
|||
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-icon remove |
|||
span Horizontal Bar |
|||
|
|||
.editor-code-main |
|||
.editor-code-editor |
|||
.editor-code-editor-title(v-if='previewShown', @click='previewShown = false') Editor |
|||
.editor-code-editor-title(v-else='previewShown', @click='previewShown = true'): v-icon(dark) drag_indicator |
|||
codemirror(ref='cm', v-model='code', :options='cmOptions', @ready='onCmReady', @input='onCmInput') |
|||
transition(name='editor-code-preview') |
|||
.editor-code-preview(v-if='previewShown') |
|||
.editor-code-preview-title(@click='previewShown = false') Preview |
|||
.editor-code-preview-content.markdown-content(ref='editorPreview', v-html='previewHTML') |
|||
|
|||
v-speed-dial(v-model='fabInsertMenu', :open-on-hover='true', direction='top', transition='slide-y-reverse-transition', fixed, left, bottom) |
|||
v-btn(color='blue', fab, dark, v-model='fabInsertMenu', slot='activator') |
|||
v-icon add_circle |
|||
v-icon close |
|||
v-btn(color='teal', fab, dark): v-icon image |
|||
v-btn(color='pink', fab, dark): v-icon insert_drive_file |
|||
v-btn(color='red', fab, dark): v-icon play_circle_outline |
|||
v-btn(color='purple', fab, dark): v-icon multiline_chart |
|||
v-btn(color='indigo', fab, dark): v-icon functions |
|||
</template> |
|||
|
|||
<script> |
|||
import _ from 'lodash' |
|||
|
|||
// ======================================== |
|||
// IMPORTS |
|||
// ======================================== |
|||
|
|||
// Code Mirror |
|||
import { codemirror } from 'vue-codemirror' |
|||
import 'codemirror/lib/codemirror.css' |
|||
|
|||
// Language |
|||
import 'codemirror/mode/markdown/markdown.js' |
|||
|
|||
// Addons |
|||
import 'codemirror/addon/selection/active-line.js' |
|||
import 'codemirror/addon/display/fullscreen.js' |
|||
import 'codemirror/addon/display/fullscreen.css' |
|||
import 'codemirror/addon/selection/mark-selection.js' |
|||
import 'codemirror/addon/scroll/annotatescrollbar.js' |
|||
import 'codemirror/addon/search/matchesonscrollbar.js' |
|||
import 'codemirror/addon/search/searchcursor.js' |
|||
import 'codemirror/addon/search/match-highlighter.js' |
|||
|
|||
// Markdown-it |
|||
import MarkdownIt from 'markdown-it' |
|||
import mdEmoji from 'markdown-it-emoji' |
|||
import mdTaskLists from 'markdown-it-task-lists' |
|||
import mdExpandTabs from 'markdown-it-expand-tabs' |
|||
import mdAbbr from 'markdown-it-abbr' |
|||
import mdSup from 'markdown-it-sup' |
|||
import mdSub from 'markdown-it-sub' |
|||
import mdMark from 'markdown-it-mark' |
|||
import mdImsize from 'markdown-it-imsize' |
|||
|
|||
// Prism (Syntax Highlighting) |
|||
import Prism from '@/libs/prism/prism.js' |
|||
|
|||
// ======================================== |
|||
// INIT |
|||
// ======================================== |
|||
|
|||
// Platform detection |
|||
const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl' |
|||
|
|||
// Markdown Instance |
|||
const md = new MarkdownIt({ |
|||
html: true, |
|||
breaks: true, |
|||
linkify: true, |
|||
typography: true, |
|||
highlight(str, lang) { |
|||
return `<pre class="line-numbers"><code class="language-${lang}">${str}</code></pre>` |
|||
} |
|||
}) |
|||
.use(mdEmoji) |
|||
.use(mdTaskLists) |
|||
.use(mdExpandTabs) |
|||
.use(mdAbbr) |
|||
.use(mdSup) |
|||
.use(mdSub) |
|||
.use(mdMark) |
|||
.use(mdImsize) |
|||
|
|||
// ======================================== |
|||
// HELPER FUNCTIONS |
|||
// ======================================== |
|||
|
|||
// Inject line numbers for preview scroll sync |
|||
let linesMap = [] |
|||
function injectLineNumbers (tokens, idx, options, env, slf) { |
|||
let line |
|||
if (tokens[idx].map && tokens[idx].level === 0) { |
|||
line = tokens[idx].map[0] |
|||
tokens[idx].attrJoin('class', 'line') |
|||
tokens[idx].attrSet('data-line', String(line)) |
|||
linesMap.push(line) |
|||
} |
|||
return slf.renderToken(tokens, idx, options, env, slf) |
|||
} |
|||
md.renderer.rules.paragraph_open = injectLineNumbers |
|||
md.renderer.rules.heading_open = injectLineNumbers |
|||
|
|||
// ======================================== |
|||
// Vue Component |
|||
// ======================================== |
|||
|
|||
export default { |
|||
components: { |
|||
codemirror |
|||
}, |
|||
data() { |
|||
return { |
|||
fabInsertMenu: false, |
|||
code: '# Header 1\n\nSample **Text**\nhttp://wiki.js.org\n:rocket: :) :( :| :P\n\n## Header 2\nSample Text\n\n```javascript\nvar test = require("test");\n\n// some comment\nconst foo = bar(\'param\') + 1.234;\n```\n\n### Header 3\nLorem *ipsum* ~~text~~', |
|||
cmOptions: { |
|||
tabSize: 2, |
|||
mode: 'text/markdown', |
|||
theme: 'wikijs-dark', |
|||
lineNumbers: true, |
|||
lineWrapping: true, |
|||
line: true, |
|||
styleActiveLine: true, |
|||
highlightSelectionMatches: { |
|||
annotateScrollbar: true |
|||
}, |
|||
viewportMargin: 50 |
|||
}, |
|||
previewShown: true, |
|||
previewHTML: '' |
|||
} |
|||
}, |
|||
computed: { |
|||
cm() { |
|||
return this.$refs.cm.codemirror |
|||
}, |
|||
isMobile() { |
|||
return this.$vuetify.breakpoint.smAndDown |
|||
} |
|||
}, |
|||
methods: { |
|||
onCmReady(cm) { |
|||
let self = this |
|||
const keyBindings = { |
|||
'F11' (cm) { |
|||
cm.setOption('fullScreen', !cm.getOption('fullScreen')) |
|||
}, |
|||
'Esc' (cm) { |
|||
if (cm.getOption('fullScreen')) cm.setOption('fullScreen', false) |
|||
} |
|||
} |
|||
_.set(keyBindings, `${CtrlKey}-S`, cm => { |
|||
self.$parent.save() |
|||
}) |
|||
|
|||
cm.setSize(null, 'calc(100vh - 100px)') |
|||
cm.setOption('extraKeys', keyBindings) |
|||
cm.on('cursorActivity', cm => { |
|||
this.toolbarSync(cm) |
|||
this.scrollSync(cm) |
|||
}) |
|||
this.onCmInput(this.code) |
|||
}, |
|||
onCmInput: _.debounce(function (newContent) { |
|||
linesMap = [] |
|||
this.$store.set('editor/content', newContent) |
|||
this.previewHTML = md.render(newContent) |
|||
this.$nextTick(() => { |
|||
Prism.highlightAllUnder(this.$refs.editorPreview) |
|||
this.scrollSync(this.cm) |
|||
}) |
|||
}, 500), |
|||
/** |
|||
* Update toolbar state |
|||
*/ |
|||
toolbarSync(cm) { |
|||
const pos = cm.getCursor('start') |
|||
const token = cm.getTokenAt(pos) |
|||
|
|||
if (!token.type) { return } |
|||
|
|||
console.info(token) |
|||
}, |
|||
/** |
|||
* Update scroll sync |
|||
*/ |
|||
scrollSync: _.debounce(function (cm) { |
|||
if (!this.previewShown || cm.somethingSelected()) { return } |
|||
let currentLine = cm.getCursor().line |
|||
if (currentLine < 3) { |
|||
this.Velocity(this.$refs.editorPreview, 'stop', true) |
|||
this.Velocity(this.$refs.editorPreview.firstChild, 'scroll', { offset: '-50', duration: 1000, container: this.$refs.editorPreview }) |
|||
} else { |
|||
let closestLine = _.findLast(linesMap, n => n <= currentLine) |
|||
let destElm = this.$refs.editorPreview.querySelector(`[data-line='${closestLine}']`) |
|||
if (destElm) { |
|||
this.Velocity(this.$refs.editorPreview, 'stop', true) |
|||
this.Velocity(destElm, 'scroll', { offset: '-100', duration: 1000, container: this.$refs.editorPreview }) |
|||
} |
|||
} |
|||
}, 500), |
|||
toggleAround (before, after) { |
|||
|
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang='scss'> |
|||
.editor-code { |
|||
&-main { |
|||
display: flex; |
|||
width: 100%; |
|||
} |
|||
|
|||
&-editor { |
|||
background-color: darken(mc('grey', '900'), 4.5%); |
|||
flex: 1 1 50%; |
|||
display: block; |
|||
height: calc(100vh - 96px); |
|||
position: relative; |
|||
|
|||
&-title { |
|||
background-color: mc('grey', '800'); |
|||
border-bottom-left-radius: 5px; |
|||
display: inline-flex; |
|||
height: 30px; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 0 1rem; |
|||
color: mc('grey', '500'); |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
z-index: 7; |
|||
text-transform: uppercase; |
|||
font-size: .7rem; |
|||
cursor: pointer; |
|||
|
|||
@include until($tablet) { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&-preview { |
|||
flex: 1 1 50%; |
|||
background-color: mc('grey', '100'); |
|||
position: relative; |
|||
height: calc(100vh - 100px); |
|||
overflow: hidden; |
|||
|
|||
@include until($tablet) { |
|||
display: none; |
|||
} |
|||
|
|||
&-enter-active, &-leave-active { |
|||
transition: max-width .5s ease; |
|||
max-width: 50vw; |
|||
|
|||
.editor-code-preview-content { |
|||
width: 50vw; |
|||
overflow:hidden; |
|||
} |
|||
} |
|||
&-enter, &-leave-to { |
|||
max-width: 0; |
|||
} |
|||
|
|||
&-content { |
|||
height: calc(100vh - 100px); |
|||
overflow-y: scroll; |
|||
padding: 30px 1rem 1rem 1rem; |
|||
width: calc(100% + 1rem + 17px) |
|||
// -ms-overflow-style: none; |
|||
|
|||
// &::-webkit-scrollbar { |
|||
// width: 0px; |
|||
// background: transparent; |
|||
// } |
|||
} |
|||
|
|||
&-title { |
|||
background-color: rgba(mc('blue', '100'), .75); |
|||
border-bottom-right-radius: 5px; |
|||
display: inline-flex; |
|||
height: 30px; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 0 1rem; |
|||
color: mc('blue', '800'); |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 2; |
|||
text-transform: uppercase; |
|||
font-size: .7rem; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
&-toolbar { |
|||
background-color: mc('blue', '700'); |
|||
background-image: linear-gradient(to bottom, mc('blue', '700') 0%, mc('blue','800') 100%); |
|||
color: #FFF; |
|||
|
|||
.v-toolbar__content { |
|||
padding-left: 16px; |
|||
|
|||
@include until($tablet) { |
|||
padding-left: 8px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ========================================== |
|||
// Fix FAB revealing under codemirror |
|||
// ========================================== |
|||
|
|||
.speed-dial--fixed { |
|||
z-index: 8; |
|||
} |
|||
|
|||
// ========================================== |
|||
// CODE MIRROR |
|||
// ========================================== |
|||
|
|||
.CodeMirror { |
|||
height: auto; |
|||
|
|||
.cm-header-1 { |
|||
font-size: 1.5rem; |
|||
} |
|||
.cm-header-2 { |
|||
font-size: 1.25rem; |
|||
} |
|||
.cm-header-3 { |
|||
font-size: 1.15rem; |
|||
} |
|||
.cm-header-4 { |
|||
font-size: 1.1rem; |
|||
} |
|||
.cm-header-5 { |
|||
font-size: 1.05rem; |
|||
} |
|||
.cm-header-6 { |
|||
font-size: 1.025rem; |
|||
} |
|||
} |
|||
|
|||
.CodeMirror-focused .cm-matchhighlight { |
|||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==); |
|||
background-position: bottom; |
|||
background-repeat: repeat-x; |
|||
} |
|||
.cm-matchhighlight { |
|||
background-color: mc('grey', '800'); |
|||
} |
|||
.CodeMirror-selection-highlight-scrollbar { |
|||
background-color: mc('green', '600'); |
|||
} |
|||
|
|||
.cm-s-wikijs-dark.CodeMirror { |
|||
background: darken(mc('grey','900'), 3%); |
|||
color: #e0e0e0; |
|||
} |
|||
.cm-s-wikijs-dark div.CodeMirror-selected { |
|||
background: mc('blue','800'); |
|||
} |
|||
.cm-s-wikijs-dark .cm-matchhighlight { |
|||
background: mc('blue','800'); |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-line::selection, .cm-s-wikijs-dark .CodeMirror-line > span::selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::selection { |
|||
background: mc('red', '500'); |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-line::-moz-selection, .cm-s-wikijs-dark .CodeMirror-line > span::-moz-selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::-moz-selection { |
|||
background: mc('red', '500'); |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-gutters { |
|||
background: darken(mc('grey','900'), 6%); |
|||
border-right: 1px solid mc('grey','900'); |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-guttermarker { |
|||
color: #ac4142; |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-guttermarker-subtle { |
|||
color: #505050; |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-linenumber { |
|||
color: mc('grey','800'); |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-cursor { |
|||
border-left: 1px solid #b0b0b0; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-comment { |
|||
color: mc('orange','800'); |
|||
} |
|||
.cm-s-wikijs-dark span.cm-atom { |
|||
color: #aa759f; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-number { |
|||
color: #aa759f; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-property, .cm-s-wikijs-dark span.cm-attribute { |
|||
color: #90a959; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-keyword { |
|||
color: #ac4142; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-string { |
|||
color: #f4bf75; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-variable { |
|||
color: #90a959; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-variable-2 { |
|||
color: #6a9fb5; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-def { |
|||
color: #d28445; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-bracket { |
|||
color: #e0e0e0; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-tag { |
|||
color: #ac4142; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-link { |
|||
color: #aa759f; |
|||
} |
|||
.cm-s-wikijs-dark span.cm-error { |
|||
background: #ac4142; |
|||
color: #b0b0b0; |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-activeline-background { |
|||
background: mc('grey','900'); |
|||
} |
|||
.cm-s-wikijs-dark .CodeMirror-matchingbracket { |
|||
text-decoration: underline; |
|||
color: white !important; |
|||
} |
|||
|
|||
} |
|||
</style> |
@ -0,0 +1,62 @@ |
|||
<template lang='pug'> |
|||
.editor-wysiwyg |
|||
div(ref='editor') |
|||
|
|||
</template> |
|||
|
|||
<script> |
|||
import 'grapesjs/dist/css/grapes.min.css' |
|||
import grapesjs from 'grapesjs' |
|||
|
|||
let editor |
|||
|
|||
export default { |
|||
mounted() { |
|||
editor = grapesjs.init({ |
|||
container: this.$refs.editor, |
|||
width: 'auto', |
|||
height: 'calc(100vh - 64px)', |
|||
storageManager: { type: null }, |
|||
// panels: { defaults: [] } |
|||
blockManager: { |
|||
blocks: [ |
|||
{ |
|||
id: 'section', // id is mandatory |
|||
label: '<b>Section</b>', // You can use HTML/SVG inside labels |
|||
attributes: { class: 'gjs-block-section' }, |
|||
content: `<section> |
|||
<h1>This is a simple title</h1> |
|||
<div>This is just a Lorem text: Lorem ipsum dolor sit amet</div> |
|||
</section>` |
|||
}, { |
|||
id: 'text', |
|||
label: 'Text', |
|||
content: '<div data-gjs-type="text">Insert your text here</div>', |
|||
}, { |
|||
id: 'image', |
|||
label: 'Image', |
|||
// Select the component once it's dropped |
|||
select: true, |
|||
// You can pass components as a JSON instead of a simple HTML string, |
|||
// in this case we also use a defined component type `image` |
|||
content: { type: 'image' }, |
|||
// This triggers `active` event on dropped components and the `image` |
|||
// reacts by opening the AssetManager |
|||
activate: true |
|||
} |
|||
] |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
|
|||
.gjs-block { |
|||
width: auto; |
|||
height: auto; |
|||
min-height: auto; |
|||
} |
|||
|
|||
</style> |
@ -1,6 +1,6 @@ |
|||
key: markdown |
|||
title: Markdown |
|||
description: Default Markdown editor |
|||
description: Basic Markdown editor |
|||
contentType: markdown |
|||
author: requarks.io |
|||
props: {} |
@ -0,0 +1,6 @@ |
|||
key: wysiwyg |
|||
title: WYSIWYG |
|||
description: Advanced Visual HTML Builder |
|||
contentType: html |
|||
author: requarks.io |
|||
props: {} |
Write
Preview
Loading…
Cancel
Save