feat: native editing + admin editors (wip)

pull/6775/head
Nicolas Giard 2 years ago
parent 3aafe116ac
commit 638383252c
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

1
.gitignore vendored

@ -34,6 +34,7 @@ npm-debug.log*
/uploads
/content
/temp
/tmp
*.sqlite
# IDE exclude

@ -85,12 +85,25 @@ defaults:
maxHits: 100
maintainerEmail: security@requarks.io
editors:
code:
asciidoc:
contentType: html
config: {}
markdown:
contentType: markdown
config:
allowHTML: true
linkify: true
lineBreaks: true
typographer: false
underline: false
tabWidth: 2
latexEngine: katex
kroki: true
plantuml: true
multimdTable: true
wysiwyg:
contentType: html
config: {}
groups:
defaultPermissions:
- 'read:pages'
@ -109,18 +122,3 @@ groups:
path: ''
locales: []
sites: []
reservedPaths:
- login
- logout
- register
- verify
- favicons
- fonts
- img
- js
- svg
pageExtensions:
- md
- html
- txt
# ---------------------------------

@ -570,6 +570,31 @@ exports.up = async knex => {
faviconExt: 'svg',
loginBg: false
},
editors: {
asciidoc: {
isActive: true,
config: {}
},
markdown: {
isActive: true,
config: {
allowHTML: true,
linkify: true,
lineBreaks: true,
typographer: false,
underline: false,
tabWidth: 2,
latexEngine: 'katex',
kroki: true,
plantuml: true,
multimdTable: true
}
},
wysiwyg: {
isActive: true,
config: {}
}
},
theme: {
dark: false,
colorPrimary: '#1976D2',

@ -69,6 +69,7 @@ type Site {
locale: String
localeNamespaces: [String]
localeNamespacing: Boolean
editors: SiteEditors
theme: SiteTheme
}
@ -106,6 +107,17 @@ type SiteLocale {
namespaces: [String]
}
type SiteEditors {
asciidoc: SiteEditor
markdown: SiteEditor
wysiwyg: SiteEditor
}
type SiteEditor {
isActive: Boolean
config: JSON
}
type SiteTheme {
dark: Boolean
colorPrimary: String

@ -0,0 +1,5 @@
audit = false
fund = false
lockfile-version = "3"
save-exact = true
save-prefix = ""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,28 +0,0 @@
defaultSemverRangePrefix: ''
enableTelemetry: false
nodeLinker: node-modules
packageExtensions:
'rollup-plugin-visualizer@*':
dependencies:
'rollup': '*'
'v-network-graph@*':
dependencies:
'd3-force': '*'
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
supportedArchitectures:
cpu:
- x64
- arm64
os:
- darwin
- linux
- win32
yarnPath: .yarn/releases/yarn-3.2.0.cjs

7661
ux/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -11,57 +11,40 @@
"lint": "eslint --ext .js,.vue ./"
},
"dependencies": {
"@apollo/client": "3.7.1",
"@codemirror/autocomplete": "6.0.2",
"@codemirror/basic-setup": "0.20.0",
"@codemirror/closebrackets": "0.19.2",
"@codemirror/commands": "6.0.1",
"@codemirror/comment": "0.19.1",
"@codemirror/fold": "0.19.4",
"@codemirror/gutter": "0.19.9",
"@codemirror/highlight": "0.19.8",
"@codemirror/history": "0.19.2",
"@codemirror/lang-css": "6.0.0",
"@codemirror/lang-html": "6.1.0",
"@codemirror/lang-javascript": "6.0.1",
"@codemirror/lang-json": "6.0.0",
"@codemirror/lang-markdown": "6.0.0",
"@codemirror/matchbrackets": "0.19.4",
"@codemirror/search": "6.0.0",
"@codemirror/state": "6.0.1",
"@codemirror/tooltip": "0.19.16",
"@codemirror/view": "6.0.2",
"@lezer/common": "1.0.1",
"@quasar/extras": "1.15.5",
"@tiptap/core": "2.0.0-beta.176",
"@tiptap/extension-code-block": "2.0.0-beta.37",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.68",
"@tiptap/extension-color": "2.0.0-beta.9",
"@tiptap/extension-dropcursor": "2.0.0-beta.25",
"@tiptap/extension-font-family": "2.0.0-beta.21",
"@tiptap/extension-gapcursor": "2.0.0-beta.34",
"@tiptap/extension-hard-break": "2.0.0-beta.30",
"@tiptap/extension-highlight": "2.0.0-beta.33",
"@tiptap/extension-history": "2.0.0-beta.21",
"@tiptap/extension-image": "2.0.0-beta.27",
"@tiptap/extension-mention": "2.0.0-beta.97",
"@tiptap/extension-placeholder": "2.0.0-beta.48",
"@tiptap/extension-table": "2.0.0-beta.49",
"@tiptap/extension-table-cell": "2.0.0-beta.20",
"@tiptap/extension-table-header": "2.0.0-beta.22",
"@tiptap/extension-table-row": "2.0.0-beta.19",
"@tiptap/extension-task-item": "2.0.0-beta.32",
"@tiptap/extension-task-list": "2.0.0-beta.26",
"@tiptap/extension-text-align": "2.0.0-beta.29",
"@tiptap/extension-text-style": "2.0.0-beta.23",
"@tiptap/extension-typography": "2.0.0-beta.20",
"@tiptap/starter-kit": "2.0.0-beta.185",
"@tiptap/vue-3": "2.0.0-beta.91",
"@apollo/client": "3.7.7",
"@lezer/common": "1.0.2",
"@quasar/extras": "1.15.10",
"@tiptap/core": "2.0.0-beta.212",
"@tiptap/extension-code-block": "2.0.0-beta.212",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.212",
"@tiptap/extension-color": "2.0.0-beta.212",
"@tiptap/extension-dropcursor": "2.0.0-beta.212",
"@tiptap/extension-font-family": "2.0.0-beta.212",
"@tiptap/extension-gapcursor": "2.0.0-beta.212",
"@tiptap/extension-hard-break": "2.0.0-beta.212",
"@tiptap/extension-highlight": "2.0.0-beta.212",
"@tiptap/extension-history": "2.0.0-beta.212",
"@tiptap/extension-image": "2.0.0-beta.212",
"@tiptap/extension-mention": "2.0.0-beta.212",
"@tiptap/extension-placeholder": "2.0.0-beta.212",
"@tiptap/extension-table": "2.0.0-beta.212",
"@tiptap/extension-table-cell": "2.0.0-beta.212",
"@tiptap/extension-table-header": "2.0.0-beta.212",
"@tiptap/extension-table-row": "2.0.0-beta.212",
"@tiptap/extension-task-item": "2.0.0-beta.212",
"@tiptap/extension-task-list": "2.0.0-beta.212",
"@tiptap/extension-text-align": "2.0.0-beta.212",
"@tiptap/extension-text-style": "2.0.0-beta.212",
"@tiptap/extension-typography": "2.0.0-beta.212",
"@tiptap/pm": "2.0.0-beta.212",
"@tiptap/starter-kit": "2.0.0-beta.212",
"@tiptap/vue-3": "2.0.0-beta.212",
"apollo-upload-client": "17.0.0",
"browser-fs-access": "0.31.1",
"browser-fs-access": "0.31.2",
"clipboard": "2.0.11",
"codemirror": "6.0.1",
"filesize": "10.0.5",
"codemirror": "5.65.11",
"codemirror-asciidoc": "1.0.4",
"filesize": "10.0.6",
"filesize-parser": "1.5.0",
"fuse.js": "6.6.2",
"graphql": "16.6.0",
@ -69,39 +52,47 @@
"js-cookie": "3.0.1",
"jwt-decode": "3.1.2",
"lodash-es": "4.17.21",
"luxon": "3.1.0",
"pinia": "2.0.23",
"lowlight": "2.8.1",
"luxon": "3.2.1",
"pinia": "2.0.30",
"prosemirror-commands": "1.5.0",
"prosemirror-history": "1.3.0",
"prosemirror-keymap": "1.2.0",
"prosemirror-model": "1.19.0",
"prosemirror-schema-list": "1.2.2",
"prosemirror-state": "1.4.2",
"prosemirror-transform": "1.7.1",
"prosemirror-view": "1.30.1",
"pug": "3.0.2",
"quasar": "2.10.1",
"quasar": "2.11.5",
"slugify": "1.6.5",
"socket.io-client": "4.5.3",
"socket.io-client": "4.5.4",
"tippy.js": "6.3.7",
"uuid": "9.0.0",
"v-network-graph": "0.6.10",
"vue": "3.2.41",
"vue-codemirror": "6.1.1",
"v-network-graph": "0.8.1",
"vue": "3.2.47",
"vue-i18n": "9.2.2",
"vue-router": "4.1.6",
"vue3-otp-input": "0.3.6",
"vuedraggable": "4.1.0",
"xterm": "5.0.0",
"xterm": "5.1.0",
"zxcvbn": "4.4.2"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "6.0.3",
"@quasar/app-vite": "1.1.3",
"@types/lodash": "4.14.188",
"@volar/vue-language-plugin-pug": "1.0.9",
"@intlify/unplugin-vue-i18n": "0.8.1",
"@quasar/app-vite": "1.2.0",
"@types/lodash": "4.14.191",
"@volar/vue-language-plugin-pug": "1.0.24",
"browserlist": "latest",
"eslint": "8.27.0",
"eslint": "8.33.0",
"eslint-config-standard": "17.0.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-n": "15.5.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "15.6.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.7.0"
"eslint-plugin-vue": "9.9.0"
},
"engines": {
"node": "^18 || ^16",
"node": "^18",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
},

@ -50,7 +50,8 @@ module.exports = configure(function (/* ctx */) {
extras: [
// 'ionicons-v4',
// 'mdi-v5',
'fontawesome-v6',
'mdi-v7',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
'line-awesome'
@ -91,11 +92,17 @@ module.exports = configure(function (/* ctx */) {
// /^\/_site\//
// ]
// }
viteConf.optimizeDeps.include = [
'prosemirror-state',
'prosemirror-transform',
'prosemirror-model',
'prosemirror-view'
]
},
// viteVuePluginOptions: {},
vitePlugins: [
['@intlify/vite-plugin-vue-i18n', {
['@intlify/unplugin-vue-i18n/vite', {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,
@ -157,7 +164,7 @@ module.exports = configure(function (/* ctx */) {
}
},
iconSet: 'fontawesome-v6', // Quasar icon set
iconSet: 'mdi-v7', // Quasar icon set
lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact

@ -1,13 +1,396 @@
<template lang="pug">
.quill-container
.editor-markdown
.editor-markdown-main
.editor-markdown-sidebar X
.editor-markdown-editor
textarea(ref='cmRef')
transition(name='editor-markdown-preview')
.editor-markdown-preview(v-if='state.previewShown')
.editor-markdown-preview-content.contents(ref='editorPreviewContainer')
div(
ref='editorPreview'
v-html='state.previewHTML'
)
</template>
<script>
<script setup>
import { reactive, ref, shallowRef, onBeforeMount, onMounted, watch } from 'vue'
import { useMeta, useQuasar, setCssVar } from 'quasar'
import { useI18n } from 'vue-i18n'
export default {
data () {
return {
import { useEditorStore } from 'src/stores/editor'
// Code Mirror
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
import '../css/codemirror.scss'
// 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/search/searchcursor.js'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
// QUASAR
const $q = useQuasar()
// STORES
const editorStore = useEditorStore()
// I18N
const { t } = useI18n()
// STATE
const cm = shallowRef(null)
const cmRef = ref(null)
const state = reactive({
previewShown: true,
previewHTML: ''
})
// Platform detection
const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
// MOUNTED
onMounted(async () => {
// -> Setup Editor View
editorStore.$patch({
hideSideNav: true
})
// -> Initialize CodeMirror
cm.value = CodeMirror.fromTextArea(cmRef.value, {
tabSize: 2,
mode: 'text/markdown',
theme: 'wikijs-dark',
lineNumbers: true,
lineWrapping: true,
line: true,
styleActiveLine: true,
highlightSelectionMatches: {
annotateScrollbar: true
},
viewportMargin: 50,
inputStyle: 'contenteditable',
allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'],
// direction: siteConfig.rtl ? 'rtl' : 'ltr',
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
})
cm.value.setValue(state.content)
cm.value.on('change', c => {
editorStore.$patch({
content: c.getValue()
})
// onCmInput(editorStore.content)
})
cm.value.setSize(null, 'calc(100vh - 150px)')
// -> Set Keybindings
const keyBindings = {
'F11' (c) {
c.setOption('fullScreen', !c.getOption('fullScreen'))
},
'Esc' (c) {
if (c.getOption('fullScreen')) {
c.setOption('fullScreen', false)
}
},
[`${CtrlKey}-S`] (c) {
// save()
return false
},
[`${CtrlKey}-B`] (c) {
// toggleMarkup({ start: '**' })
return false
},
[`${CtrlKey}-I`] (c) {
// toggleMarkup({ start: '*' })
return false
},
[`${CtrlKey}-Alt-Right`] (c) {
// let lvl = getHeaderLevel(c)
// if (lvl >= 6) { lvl = 5 }
// setHeaderLine(lvl + 1)
return false
},
[`${CtrlKey}-Alt-Left`] (c) {
// let lvl = getHeaderLevel(c)
// if (lvl <= 1) { lvl = 2 }
// setHeaderLine(lvl - 1)
return false
}
}
}
cm.value.setOption('extraKeys', keyBindings)
// this.cm.on('inputRead', this.autocomplete)
// // Handle cursor movement
// this.cm.on('cursorActivity', c => {
// this.positionSync(c)
// this.scrollSync(c)
// })
// // Handle special paste
// this.cm.on('paste', this.onCmPaste)
// // Render initial preview
// this.processContent(this.$store.get('editor/content'))
// this.refresh()
// this.$root.$on('editorInsert', opts => {
// switch (opts.kind) {
// case 'IMAGE':
// let img = `![${opts.text}](${opts.path})`
// if (opts.align && opts.align !== '') {
// img += `{.align-${opts.align}}`
// }
// this.insertAtCursor({
// content: img
// })
// break
// case 'BINARY':
// this.insertAtCursor({
// content: `[${opts.text}](${opts.path})`
// })
// break
// case 'DIAGRAM':
// const selStartLine = this.cm.getCursor('from').line
// const selEndLine = this.cm.getCursor('to').line + 1
// this.cm.doc.replaceSelection('```diagram\n' + opts.text + '\n```\n', 'start')
// this.processMarkers(selStartLine, selEndLine)
// break
// }
// })
// // Handle save conflict
// this.$root.$on('saveConflict', () => {
// this.toggleModal(`editorModalConflict`)
// })
// this.$root.$on('overwriteEditorContent', () => {
// this.cm.setValue(this.$store.get('editor/content'))
// })
})
onBeforeMount(() => {
// if (editor.value) {
// editor.value.destroy()
// }
})
</script>
<style lang="scss">
$editor-height: calc(100vh - 112px - 24px);
$editor-height-mobile: calc(100vh - 112px - 16px);
.editor-markdown {
&-main {
display: flex;
width: 100%;
}
&-editor {
background-color: $dark-6;
flex: 1 1 50%;
display: block;
height: $editor-height;
position: relative;
// @include until($tablet) {
// height: $editor-height-mobile;
// }
}
&-preview {
flex: 1 1 50%;
background-color: $grey-2;
position: relative;
height: $editor-height;
overflow: hidden;
padding: 1rem;
@at-root .theme--dark & {
background-color: $grey-9;
}
// @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: $editor-height;
overflow-y: scroll;
padding: 0;
width: calc(100% + 17px);
// -ms-overflow-style: none;
// &::-webkit-scrollbar {
// width: 0px;
// background: transparent;
// }
// @include until($tablet) {
// height: $editor-height-mobile;
// }
> div {
outline: none;
}
p.line {
overflow-wrap: break-word;
}
.tabset {
background-color: $teal-7;
color: $teal-2 !important;
padding: 5px 12px;
font-size: 14px;
font-weight: 500;
border-radius: 5px 0 0 0;
font-style: italic;
&::after {
display: none;
}
&-header {
background-color: $teal-5;
color: #FFF !important;
padding: 5px 12px;
font-size: 14px;
font-weight: 500;
margin-top: 0 !important;
&::after {
display: none;
}
}
&-content {
border-left: 5px solid $teal-5;
background-color: $teal-1;
padding: 0 15px 15px;
overflow: hidden;
@at-root .theme--dark & {
background-color: rgba($teal-5, .1);
}
}
}
}
}
&-toolbar {
background-color: $blue-7;
background-image: linear-gradient(to bottom, $blue-7 0%, $blue-8 100%);
color: #FFF;
.v-toolbar__content {
padding-left: 64px;
// @include until($tablet) {
// padding-left: 8px;
// }
}
}
&-sidebar {
background-color: $dark-4;
border-right: 1px solid $dark-3;
color: #FFF;
width: 56px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 24px 0;
}
&-sysbar {
padding-left: 0;
&-locale {
background-color: rgba(255,255,255,.25);
display:inline-flex;
padding: 0 12px;
height: 24px;
width: 63px;
justify-content: center;
align-items: center;
}
}
// ==========================================
// CODE MIRROR
// ==========================================
.CodeMirror {
height: auto;
font-family: 'Roboto Mono', monospace;
font-size: .9rem;
.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-wrap pre.CodeMirror-line, .CodeMirror-wrap pre.CodeMirror-line-like {
word-break: break-word;
}
.CodeMirror-focused .cm-matchhighlight {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
background-position: bottom;
background-repeat: repeat-x;
}
.cm-matchhighlight {
background-color: $grey-8;
}
.CodeMirror-selection-highlight-scrollbar {
background-color: $green-6;
}
}
// HINT DROPDOWN
.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;
margin: 0;
padding: 1px;
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
border: 1px solid $grey-7;
background: $grey-9;
font-family: 'Roboto Mono', monospace;
font-size: .9rem;
max-height: 150px;
overflow-y: auto;
min-width: 250px;
max-width: 80vw;
}
.CodeMirror-hint {
margin: 0;
padding: 0 4px;
white-space: pre;
color: #FFF;
cursor: pointer;
}
li.CodeMirror-hint-active {
background: $blue-5;
color: #FFF;
}
</style>

@ -0,0 +1,69 @@
<template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 850px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/ultraviolet-markdown.svg', left, size='sm')
span {{t(`admin.editors.markdownName`)}}
q-card-section.q-pa-none
span Test
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='t(`common.actions.save`)'
color='primary'
padding='xs md'
@click='save'
:loading='state.loading > 0'
)
q-inner-loading(:showing='state.loading > 0')
q-spinner(color='accent', size='lg')
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive, ref } from 'vue'
import { useAdminStore } from '../stores/admin'
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
config: [],
loading: 0
})
// METHODS
async function save () {
}
</script>

File diff suppressed because it is too large Load Diff

@ -81,8 +81,8 @@ const { t } = useI18n()
// METHODS
function create (editor) {
window.location.assign('/_edit/new')
// pageStore.pageCreate({ editor })
// window.location.assign('/_edit/new')
pageStore.pageCreate({ editor })
}
function openFileManager () {

@ -1,18 +1,24 @@
<template lang="pug">
.util-code-editor(
ref='editorRef'
)
.util-code-editor
textarea(ref='cmRef')
</template>
<script setup>
/* eslint no-unused-vars: "off" */
import { keymap, EditorView, lineNumbers } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { ref, shallowRef, onBeforeMount, onMounted, watch } from 'vue'
// Code Mirror
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// Language
import 'codemirror/mode/markdown/markdown.js'
import 'codemirror/mode/htmlmixed/htmlmixed.js'
import 'codemirror/mode/css/css.js'
// Addons
import 'codemirror/addon/selection/active-line.js'
// PROPS
const props = defineProps({
@ -38,72 +44,77 @@ const emit = defineEmits([
// STATE
const editor = shallowRef(null)
const editorRef = ref(null)
const cm = shallowRef(null)
const cmRef = ref(null)
// WATCHERS
watch(() => props.modelValue, (newVal) => {
// Ignore loopback changes while editing
if (!editor.value.hasFocus) {
editor.value.dispatch({
changes: { from: 0, to: editor.value.state.length, insert: newVal }
})
if (!cm.value.hasFocus()) {
cm.value.setValue(newVal)
}
})
// MOUNTED
onMounted(async () => {
let langModule = null
let langMode = null
switch (props.language) {
case 'css': {
langModule = (await import('@codemirror/lang-css')).css
langMode = 'text/css'
break
}
case 'html': {
langModule = (await import('@codemirror/lang-html')).html
langMode = 'text/html'
break
}
case 'javascript': {
langModule = (await import('@codemirror/lang-javascript')).javascript
langMode = 'text/javascript'
break
}
case 'json': {
langModule = (await import('@codemirror/lang-json')).json
langMode = {
name: 'javascript',
json: true
}
break
}
case 'markdown': {
langModule = (await import('@codemirror/lang-markdown')).markdown
langMode = 'text/markdown'
break
}
default: {
langMode = null
break
}
}
editor.value = new EditorView({
state: EditorState.create({
doc: props.modelValue,
extensions: [
history(),
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
lineNumbers(),
EditorView.theme({
'.cm-content, .cm-gutter': { minHeight: `${props.minHeight}px` }
}),
...langModule && [langModule()],
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of(v => {
if (v.docChanged) {
emit('update:modelValue', v.state.doc.toString())
}
})
]
}),
parent: editorRef.value
// -> Initialize CodeMirror
cm.value = CodeMirror.fromTextArea(cmRef.value, {
tabSize: 2,
mode: langMode,
theme: 'wikijs-dark',
lineNumbers: true,
lineWrapping: true,
line: true,
styleActiveLine: true,
viewportMargin: 50,
inputStyle: 'contenteditable',
direction: 'ltr'
})
cm.value.setValue(props.modelValue)
cm.value.on('change', c => {
emit('update:modelValue', c.getValue())
})
cm.value.setSize(null, `${props.minHeight}px`)
})
onBeforeMount(() => {
if (editor.value) {
editor.value.destroy()
if (cm.value) {
cm.value.destroy()
}
})
</script>

@ -0,0 +1,100 @@
.cm-s-wikijs-dark.CodeMirror {
background: $dark-6;
color: #e0e0e0;
}
.cm-s-wikijs-dark div.CodeMirror-selected {
background: $teal-8;
}
.cm-s-wikijs-dark .cm-matchhighlight {
background: $teal-8;
}
.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: $blue-8;
}
.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: $blue-8;
}
.cm-s-wikijs-dark .CodeMirror-gutters {
background: $dark-3;
border-right: 1px solid $dark-2;
}
.cm-s-wikijs-dark .CodeMirror-guttermarker {
color: #ac4142;
}
.cm-s-wikijs-dark .CodeMirror-guttermarker-subtle {
color: #505050;
}
.cm-s-wikijs-dark .CodeMirror-linenumber {
color: $blue-grey-7;
}
.cm-s-wikijs-dark .CodeMirror-cursor {
border-left: 1px solid #b0b0b0;
}
.cm-s-wikijs-dark span.cm-comment {
color: $orange-8;
}
.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: $dark-4;
}
.cm-s-wikijs-dark .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}
.cm-s-wikijs-dark .CodeMirror-foldmarker {
margin-left: 10px;
display: inline-block;
background-color: rgba($amber-8, .3);
padding: 8px 5px;
color: $amber-5;
border-radius: 5px;
text-shadow: none;
}
.cm-s-wikijs-dark .CodeMirror-buttonmarker {
display: inline-block;
background-color: rgba($blue-5, .3);
border: 1px solid $blue-8;
padding: 1px 10px;
color: $blue-2 !important;
border-radius: 5px;
margin-left: 5px;
cursor: pointer;
}

@ -131,6 +131,8 @@
"admin.dev.voyager.title": "Voyager",
"admin.editors.apiDescription": "Document your REST / GraphQL APIs.",
"admin.editors.apiName": "API Docs Editor",
"admin.editors.asciidocDescription": "Use the AsciiDoc syntax to write content. Includes real-time preview.",
"admin.editors.asciidocName": "AsciiDoc Editor",
"admin.editors.blogDescription": "Write a series of posts over time.",
"admin.editors.blogName": "Blog Editor",
"admin.editors.channelDescription": "Create discussion channels to collaborate in real-time with your team.",
@ -1675,5 +1677,6 @@
"welcome.admin": "Administration Area",
"welcome.createHome": "Create the homepage",
"welcome.subtitle": "Let's get started...",
"welcome.title": "Welcome to Wiki.js!"
"welcome.title": "Welcome to Wiki.js!",
"admin.editors.useRenderingPipeline": "Uses the rendering pipeline."
}

@ -87,10 +87,10 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rfid-tag.svg')
q-item-section {{ t('admin.blocks.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-cashbook.svg')
q-item-section {{ t('admin.editors.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-cashbook.svg')
q-item-section {{ t('admin.editors.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/locale`', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-language.svg')

@ -2,7 +2,7 @@
q-layout(view='hHh Lpr lff')
header-nav
q-drawer.bg-sidebar(
v-model='siteStore.showSideNav'
:modelValue='isSidebarShown'
show-if-above
:width='255'
)
@ -83,11 +83,12 @@ q-layout(view='hHh Lpr lff')
<script setup>
import { useMeta, useQuasar } from 'quasar'
import { onMounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useSiteStore } from '../stores/site'
import { useEditorStore } from 'src/stores/editor'
import { useSiteStore } from 'src/stores/site'
// COMPONENTS
@ -101,6 +102,7 @@ const $q = useQuasar()
// STORES
const editorStore = useEditorStore()
const siteStore = useSiteStore()
// ROUTER
@ -136,6 +138,12 @@ const barStyle = {
opacity: 0.1
}
// COMPUTED
const isSidebarShown = computed(() => {
return siteStore.showSideNav && !(editorStore.isActive && editorStore.hideSideNav)
})
// METHODS
function openFileManager () {

@ -17,7 +17,7 @@ q-page.admin-mail
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -19,61 +19,75 @@ q-page.admin-flags
icon='las la-redo-alt'
flat
color='secondary'
:loading='loading > 0'
@click='load'
:loading='state.loading > 0'
@click='refresh'
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
:disabled='loading > 0'
:disabled='state.loading > 0'
)
q-separator(inset)
.q-pa-md.q-gutter-md
q-card.shadow-1
q-list(separator)
q-item(v-for='editor of editors', :key='editor.id')
blueprint-icon(:icon='editor.icon')
q-item-section
q-item-label: strong {{t(`admin.editors.` + editor.id + `Name`)}}
q-item-label.flex.items-center(caption)
span {{t(`admin.editors.` + editor.id + `Description`)}}
template(v-if='editor.config')
q-item-section(
side
)
q-btn(
icon='las la-cog'
:label='t(`admin.editors.configuration`)'
:color='$q.dark.isActive ? `blue-grey-3` : `blue-grey-8`'
outline
no-caps
padding='xs md'
)
q-separator.q-ml-md(vertical)
q-item-section(side)
q-toggle.q-pr-sm(
v-model='editor.isActive'
:color='editor.isDisabled ? `grey` : `primary`'
checked-icon='las la-check'
unchecked-icon='las la-times'
:label='t(`admin.sites.isActive`)'
:aria-label='t(`admin.sites.isActive`)'
:disabled='editor.isDisabled'
)
template(v-for='editor of editors', :key='editor.id')
q-item(v-if='flagsStore.experimental || !editor.isDisabled')
blueprint-icon(:icon='editor.icon')
q-item-section
q-item-label: strong {{t(`admin.editors.` + editor.id + `Name`)}}
q-item-label(caption)
span {{t(`admin.editors.` + editor.id + `Description`)}}
q-item-label(caption, v-if='editor.useRendering')
em.text-purple {{ t('admin.editors.useRenderingPipeline') }}
template(v-if='editor.hasConfig')
q-item-section(
side
)
q-btn(
icon='las la-cog'
:label='t(`admin.editors.configuration`)'
:color='$q.dark.isActive ? `blue-grey-3` : `blue-grey-8`'
outline
no-caps
padding='xs md'
@click='openConfig(editor.id)'
)
q-separator.q-ml-md(vertical)
q-item-section(side)
q-toggle.q-pr-sm(
v-model='state.config[editor.id]'
:color='editor.isDisabled ? `grey` : `primary`'
checked-icon='las la-check'
unchecked-icon='las la-times'
:label='t(`admin.sites.isActive`)'
:aria-label='t(`admin.sites.isActive`)'
:disabled='editor.isDisabled'
)
</template>
<script setup>
import { useMeta } from 'quasar'
import { useMeta, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
import { defineAsyncComponent, onMounted, reactive, watch } from 'vue'
import gql from 'graphql-tag'
import { cloneDeep } from 'lodash-es'
import { useAdminStore } from 'src/stores/admin'
import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const flagsStore = useFlagsStore()
const siteStore = useSiteStore()
// I18N
@ -86,46 +100,146 @@ useMeta({
title: t('admin.editors.title')
})
const loading = ref(false)
const state = reactive({
loading: 0,
config: {
api: false,
asciidoc: false,
blog: false,
channel: false,
markdown: false,
redirect: true,
wysiwyg: false
}
})
const editors = reactive([
{
id: 'wysiwyg',
icon: 'google-presentation',
isActive: true
},
{
id: 'markdown',
icon: 'markdown',
config: {},
isActive: true
id: 'api',
icon: 'api',
isDisabled: true,
useRendering: false
},
{
id: 'channel',
icon: 'chat',
isActive: true
id: 'asciidoc',
icon: 'asciidoc',
hasConfig: true,
useRendering: true
},
{
id: 'blog',
icon: 'typewriter-with-paper',
isActive: true,
isDisabled: true
isDisabled: true,
useRendering: true
},
{
id: 'api',
icon: 'api',
isActive: true,
isDisabled: true
id: 'channel',
icon: 'chat',
isDisabled: true,
useRendering: false
},
{
id: 'markdown',
icon: 'markdown',
hasConfig: true,
useRendering: true
},
{
id: 'redirect',
icon: 'advance',
isActive: true
isDisabled: true,
useRendering: false
},
{
id: 'wysiwyg',
icon: 'google-presentation',
useRendering: true
}
])
const load = async () => {}
const save = () => {}
const refresh = () => {}
// WATCHERS
watch(() => adminStore.currentSiteId, (newValue) => {
$q.loading.show()
load()
})
// METHODS
async function load () {
state.loading++
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query getEditorsState (
$siteId: UUID!
) {
siteById (
id: $siteId
) {
id
editors {
asciidoc {
isActive
}
markdown {
isActive
}
wysiwyg {
isActive
}
}
}
}`,
variables: {
siteId: adminStore.currentSiteId
},
fetchPolicy: 'network-only'
})
const data = cloneDeep(resp?.data?.siteById?.editors)
state.config.asciidoc = data?.asciidoc?.isActive ?? false
state.config.markdown = data?.markdown?.isActive ?? false
state.config.wysiwyg = data?.wysiwyg?.isActive ?? false
} catch (err) {
$q.notify({
type: 'negative',
message: 'Failed to fetch editors state.'
})
}
$q.loading.hide()
state.loading--
}
async function save () {}
async function refresh () {
await load()
}
function openConfig (editorId) {
switch (editorId) {
case 'markdown': {
$q.dialog({
component: defineAsyncComponent(() => import('../components/EditorMarkdownConfigDialog.vue'))
})
break
}
default: {
$q.notify({
type: 'negative',
message: 'Invalid Editor Config Call'
})
}
}
}
// MOUNTED
onMounted(async () => {
$q.loading.show()
if (adminStore.currentSiteId) {
await load()
}
})
</script>
<style lang='scss'>

@ -24,7 +24,7 @@ q-page.admin-flags
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -24,7 +24,7 @@ q-page.admin-general
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -24,7 +24,7 @@ q-page.admin-icons
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
@ -145,7 +145,6 @@ const packs = [
key: 'fa',
label: 'Font Awesome',
website: 'https://fontawesome.com',
isMandatory: true,
config: {}
},
{
@ -163,7 +162,7 @@ const packs = [
key: 'mdi',
label: 'Material Design Icons',
website: 'https://materialdesignicons.com',
config: {}
isMandatory: true
},
{
key: 'thm',

@ -33,7 +33,7 @@ q-page.admin-locale
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -24,7 +24,7 @@ q-page.admin-login
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -24,7 +24,7 @@ q-page.admin-mail
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -24,7 +24,7 @@ q-page.admin-navigation
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -24,7 +24,7 @@ q-page.admin-mail
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -36,7 +36,7 @@ q-page.admin-storage
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -25,7 +25,7 @@ q-page.admin-system
q-btn.acrylic-btn(
ref='copySysInfoBtn'
flat
icon='fa-regular fa-clipboard'
icon='mdi-clipboard-text-outline'
label='Copy System Info'
color='primary'
@click=''

@ -24,7 +24,7 @@ q-page.admin-theme
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'

@ -1,6 +1,8 @@
<template lang='pug'>
q-page.column
.page-breadcrumbs.q-py-sm.q-px-md.row
.page-breadcrumbs.q-py-sm.q-px-md.row(
v-if='!editorStore.isActive'
)
.col
q-breadcrumbs(
active-color='grey-7'
@ -36,7 +38,7 @@ q-page.column
//- PAGE HEADER
.col.q-pa-md
.text-h4.page-header-title {{pageStore.title}}
.text-subtitle2.page-header-subtitle {{pageStore.description}}
.text-subtitle2.page-header-subtitle {{pageStore.description }}
//- PAGE ACTIONS
.col-auto.q-pa-md.flex.items-center.justify-end
@ -100,11 +102,16 @@ q-page.column
label='Edit'
aria-label='Edit'
no-caps
:href='editUrl'
@click='editPage'
)
.page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
.col(style='order: 1;')
q-no-ssr(
v-if='editorStore.isActive'
)
component(:is='editorComponents[editorStore.editor]')
q-scroll-area(
v-else
:thumb-style='thumbStyle'
:bar-style='barStyle'
style='height: 100%;'
@ -344,6 +351,17 @@ const sideDialogs = {
})
}
const editorComponents = {
markdown: defineAsyncComponent({
loader: () => import('../components/EditorMarkdown.vue'),
loadingComponent: LoadingGeneric
}),
wysiwyg: defineAsyncComponent({
loader: () => import('../components/EditorWysiwyg.vue'),
loadingComponent: LoadingGeneric
})
}
// QUASAR
const $q = useQuasar()
@ -399,10 +417,7 @@ const barStyle = {
// COMPUTED
const showSidebar = computed(() => {
return pageStore.showSidebar && siteStore.showSidebar
})
const editorComponent = computed(() => {
return pageStore.editor ? `editor-${pageStore.editor}` : null
return pageStore.showSidebar && siteStore.showSidebar && !editorStore.isActive
})
const relationsLeft = computed(() => {
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'left') : []
@ -564,6 +579,13 @@ async function saveChanges () {
}
$q.loading.hide()
}
function editPage () {
editorStore.$patch({
isActive: true,
editor: 'markdown'
})
}
</script>
<style lang="scss">
@ -578,6 +600,8 @@ async function saveChanges () {
}
}
.page-header {
min-height: 95px;
@at-root .body--light & {
background: linear-gradient(to bottom, $grey-2 0%, $grey-1 100%);
border-bottom: 1px solid $grey-4;

@ -2,11 +2,13 @@ import { defineStore } from 'pinia'
export const useEditorStore = defineStore('editor', {
state: () => ({
isActive: false,
editor: '',
content: '',
mode: 'create',
activeModal: '',
activeModalData: null,
hideSideNav: false,
media: {
folderTree: [],
currentFolderId: 0,

@ -174,9 +174,7 @@ export const usePageStore = defineStore('page', {
* PAGE - CREATE
*/
pageCreate ({ editor, locale, path }) {
// -> Editor View
this.editor = editor
this.editorMode = 'create'
const editorStore = useEditorStore()
// if (['markdown', 'api'].includes(editor)) {
// commit('site/SET_SHOW_SIDE_NAV', false, { root: true })
@ -204,13 +202,19 @@ export const usePageStore = defineStore('page', {
this.isPublished = false
this.relations = []
this.tags = []
this.breadcrumbs = []
this.content = ''
this.render = ''
// -> View Mode
this.mode = 'edit'
// -> Editor Mode
editorStore.$patch({
isActive: true,
editor,
mode: 'create'
})
},
/**
* PAGE SAVE

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save