feat: native editing + admin editors (wip)

parent 3aafe116
...@@ -34,6 +34,7 @@ npm-debug.log* ...@@ -34,6 +34,7 @@ npm-debug.log*
/uploads /uploads
/content /content
/temp /temp
/tmp
*.sqlite *.sqlite
# IDE exclude # IDE exclude
......
...@@ -85,12 +85,25 @@ defaults: ...@@ -85,12 +85,25 @@ defaults:
maxHits: 100 maxHits: 100
maintainerEmail: security@requarks.io maintainerEmail: security@requarks.io
editors: editors:
code: asciidoc:
contentType: html contentType: html
config: {}
markdown: markdown:
contentType: 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: wysiwyg:
contentType: html contentType: html
config: {}
groups: groups:
defaultPermissions: defaultPermissions:
- 'read:pages' - 'read:pages'
...@@ -109,18 +122,3 @@ groups: ...@@ -109,18 +122,3 @@ groups:
path: '' path: ''
locales: [] locales: []
sites: [] sites: []
reservedPaths:
- login
- logout
- register
- verify
- favicons
- fonts
- img
- js
- svg
pageExtensions:
- md
- html
- txt
# ---------------------------------
...@@ -570,6 +570,31 @@ exports.up = async knex => { ...@@ -570,6 +570,31 @@ exports.up = async knex => {
faviconExt: 'svg', faviconExt: 'svg',
loginBg: false 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: { theme: {
dark: false, dark: false,
colorPrimary: '#1976D2', colorPrimary: '#1976D2',
......
...@@ -69,6 +69,7 @@ type Site { ...@@ -69,6 +69,7 @@ type Site {
locale: String locale: String
localeNamespaces: [String] localeNamespaces: [String]
localeNamespacing: Boolean localeNamespacing: Boolean
editors: SiteEditors
theme: SiteTheme theme: SiteTheme
} }
...@@ -106,6 +107,17 @@ type SiteLocale { ...@@ -106,6 +107,17 @@ type SiteLocale {
namespaces: [String] namespaces: [String]
} }
type SiteEditors {
asciidoc: SiteEditor
markdown: SiteEditor
wysiwyg: SiteEditor
}
type SiteEditor {
isActive: Boolean
config: JSON
}
type SiteTheme { type SiteTheme {
dark: Boolean dark: Boolean
colorPrimary: String colorPrimary: String
......
audit = false
fund = false
lockfile-version = "3"
save-exact = true
save-prefix = ""
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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
...@@ -11,57 +11,40 @@ ...@@ -11,57 +11,40 @@
"lint": "eslint --ext .js,.vue ./" "lint": "eslint --ext .js,.vue ./"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "3.7.1", "@apollo/client": "3.7.7",
"@codemirror/autocomplete": "6.0.2", "@lezer/common": "1.0.2",
"@codemirror/basic-setup": "0.20.0", "@quasar/extras": "1.15.10",
"@codemirror/closebrackets": "0.19.2", "@tiptap/core": "2.0.0-beta.212",
"@codemirror/commands": "6.0.1", "@tiptap/extension-code-block": "2.0.0-beta.212",
"@codemirror/comment": "0.19.1", "@tiptap/extension-code-block-lowlight": "2.0.0-beta.212",
"@codemirror/fold": "0.19.4", "@tiptap/extension-color": "2.0.0-beta.212",
"@codemirror/gutter": "0.19.9", "@tiptap/extension-dropcursor": "2.0.0-beta.212",
"@codemirror/highlight": "0.19.8", "@tiptap/extension-font-family": "2.0.0-beta.212",
"@codemirror/history": "0.19.2", "@tiptap/extension-gapcursor": "2.0.0-beta.212",
"@codemirror/lang-css": "6.0.0", "@tiptap/extension-hard-break": "2.0.0-beta.212",
"@codemirror/lang-html": "6.1.0", "@tiptap/extension-highlight": "2.0.0-beta.212",
"@codemirror/lang-javascript": "6.0.1", "@tiptap/extension-history": "2.0.0-beta.212",
"@codemirror/lang-json": "6.0.0", "@tiptap/extension-image": "2.0.0-beta.212",
"@codemirror/lang-markdown": "6.0.0", "@tiptap/extension-mention": "2.0.0-beta.212",
"@codemirror/matchbrackets": "0.19.4", "@tiptap/extension-placeholder": "2.0.0-beta.212",
"@codemirror/search": "6.0.0", "@tiptap/extension-table": "2.0.0-beta.212",
"@codemirror/state": "6.0.1", "@tiptap/extension-table-cell": "2.0.0-beta.212",
"@codemirror/tooltip": "0.19.16", "@tiptap/extension-table-header": "2.0.0-beta.212",
"@codemirror/view": "6.0.2", "@tiptap/extension-table-row": "2.0.0-beta.212",
"@lezer/common": "1.0.1", "@tiptap/extension-task-item": "2.0.0-beta.212",
"@quasar/extras": "1.15.5", "@tiptap/extension-task-list": "2.0.0-beta.212",
"@tiptap/core": "2.0.0-beta.176", "@tiptap/extension-text-align": "2.0.0-beta.212",
"@tiptap/extension-code-block": "2.0.0-beta.37", "@tiptap/extension-text-style": "2.0.0-beta.212",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.68", "@tiptap/extension-typography": "2.0.0-beta.212",
"@tiptap/extension-color": "2.0.0-beta.9", "@tiptap/pm": "2.0.0-beta.212",
"@tiptap/extension-dropcursor": "2.0.0-beta.25", "@tiptap/starter-kit": "2.0.0-beta.212",
"@tiptap/extension-font-family": "2.0.0-beta.21", "@tiptap/vue-3": "2.0.0-beta.212",
"@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-upload-client": "17.0.0", "apollo-upload-client": "17.0.0",
"browser-fs-access": "0.31.1", "browser-fs-access": "0.31.2",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"codemirror": "6.0.1", "codemirror": "5.65.11",
"filesize": "10.0.5", "codemirror-asciidoc": "1.0.4",
"filesize": "10.0.6",
"filesize-parser": "1.5.0", "filesize-parser": "1.5.0",
"fuse.js": "6.6.2", "fuse.js": "6.6.2",
"graphql": "16.6.0", "graphql": "16.6.0",
...@@ -69,39 +52,47 @@ ...@@ -69,39 +52,47 @@
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"jwt-decode": "3.1.2", "jwt-decode": "3.1.2",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"luxon": "3.1.0", "lowlight": "2.8.1",
"pinia": "2.0.23", "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", "pug": "3.0.2",
"quasar": "2.10.1", "quasar": "2.11.5",
"slugify": "1.6.5", "slugify": "1.6.5",
"socket.io-client": "4.5.3", "socket.io-client": "4.5.4",
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
"uuid": "9.0.0", "uuid": "9.0.0",
"v-network-graph": "0.6.10", "v-network-graph": "0.8.1",
"vue": "3.2.41", "vue": "3.2.47",
"vue-codemirror": "6.1.1",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "4.1.6", "vue-router": "4.1.6",
"vue3-otp-input": "0.3.6", "vue3-otp-input": "0.3.6",
"vuedraggable": "4.1.0", "vuedraggable": "4.1.0",
"xterm": "5.0.0", "xterm": "5.1.0",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@intlify/vite-plugin-vue-i18n": "6.0.3", "@intlify/unplugin-vue-i18n": "0.8.1",
"@quasar/app-vite": "1.1.3", "@quasar/app-vite": "1.2.0",
"@types/lodash": "4.14.188", "@types/lodash": "4.14.191",
"@volar/vue-language-plugin-pug": "1.0.9", "@volar/vue-language-plugin-pug": "1.0.24",
"browserlist": "latest", "browserlist": "latest",
"eslint": "8.27.0", "eslint": "8.33.0",
"eslint-config-standard": "17.0.0", "eslint-config-standard": "17.0.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "15.5.0", "eslint-plugin-n": "15.6.1",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.7.0" "eslint-plugin-vue": "9.9.0"
}, },
"engines": { "engines": {
"node": "^18 || ^16", "node": "^18",
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
"yarn": ">= 1.21.1" "yarn": ">= 1.21.1"
}, },
......
...@@ -50,7 +50,8 @@ module.exports = configure(function (/* ctx */) { ...@@ -50,7 +50,8 @@ module.exports = configure(function (/* ctx */) {
extras: [ extras: [
// 'ionicons-v4', // 'ionicons-v4',
// 'mdi-v5', // 'mdi-v5',
'fontawesome-v6', 'mdi-v7',
// 'fontawesome-v6',
// 'eva-icons', // 'eva-icons',
// 'themify', // 'themify',
'line-awesome' 'line-awesome'
...@@ -91,11 +92,17 @@ module.exports = configure(function (/* ctx */) { ...@@ -91,11 +92,17 @@ module.exports = configure(function (/* ctx */) {
// /^\/_site\// // /^\/_site\//
// ] // ]
// } // }
viteConf.optimizeDeps.include = [
'prosemirror-state',
'prosemirror-transform',
'prosemirror-model',
'prosemirror-view'
]
}, },
// viteVuePluginOptions: {}, // viteVuePluginOptions: {},
vitePlugins: [ 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` // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false, // compositionOnly: false,
...@@ -157,7 +164,7 @@ module.exports = configure(function (/* ctx */) { ...@@ -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 lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact // For special cases outside of where the auto-import strategy can have an impact
......
<template lang="pug"> <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> </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 { import { useEditorStore } from 'src/stores/editor'
data () {
return { // 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> </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>
<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>
...@@ -81,8 +81,8 @@ const { t } = useI18n() ...@@ -81,8 +81,8 @@ const { t } = useI18n()
// METHODS // METHODS
function create (editor) { function create (editor) {
window.location.assign('/_edit/new') // window.location.assign('/_edit/new')
// pageStore.pageCreate({ editor }) pageStore.pageCreate({ editor })
} }
function openFileManager () { function openFileManager () {
......
<template lang="pug"> <template lang="pug">
.util-code-editor( .util-code-editor
ref='editorRef' textarea(ref='cmRef')
)
</template> </template>
<script setup> <script setup>
/* eslint no-unused-vars: "off" */ /* 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' 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 // PROPS
const props = defineProps({ const props = defineProps({
...@@ -38,72 +44,77 @@ const emit = defineEmits([ ...@@ -38,72 +44,77 @@ const emit = defineEmits([
// STATE // STATE
const editor = shallowRef(null) const cm = shallowRef(null)
const editorRef = ref(null) const cmRef = ref(null)
// WATCHERS // WATCHERS
watch(() => props.modelValue, (newVal) => { watch(() => props.modelValue, (newVal) => {
// Ignore loopback changes while editing // Ignore loopback changes while editing
if (!editor.value.hasFocus) { if (!cm.value.hasFocus()) {
editor.value.dispatch({ cm.value.setValue(newVal)
changes: { from: 0, to: editor.value.state.length, insert: newVal }
})
} }
}) })
// MOUNTED // MOUNTED
onMounted(async () => { onMounted(async () => {
let langModule = null let langMode = null
switch (props.language) { switch (props.language) {
case 'css': { case 'css': {
langModule = (await import('@codemirror/lang-css')).css langMode = 'text/css'
break break
} }
case 'html': { case 'html': {
langModule = (await import('@codemirror/lang-html')).html langMode = 'text/html'
break break
} }
case 'javascript': { case 'javascript': {
langModule = (await import('@codemirror/lang-javascript')).javascript langMode = 'text/javascript'
break break
} }
case 'json': { case 'json': {
langModule = (await import('@codemirror/lang-json')).json langMode = {
name: 'javascript',
json: true
}
break break
} }
case 'markdown': { case 'markdown': {
langModule = (await import('@codemirror/lang-markdown')).markdown langMode = 'text/markdown'
break
}
default: {
langMode = null
break break
} }
} }
editor.value = new EditorView({
state: EditorState.create({ // -> Initialize CodeMirror
doc: props.modelValue, cm.value = CodeMirror.fromTextArea(cmRef.value, {
extensions: [ tabSize: 2,
history(), mode: langMode,
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]), theme: 'wikijs-dark',
lineNumbers(), lineNumbers: true,
EditorView.theme({ lineWrapping: true,
'.cm-content, .cm-gutter': { minHeight: `${props.minHeight}px` } line: true,
}), styleActiveLine: true,
...langModule && [langModule()], viewportMargin: 50,
syntaxHighlighting(defaultHighlightStyle), inputStyle: 'contenteditable',
EditorView.updateListener.of(v => { direction: 'ltr'
if (v.docChanged) {
emit('update:modelValue', v.state.doc.toString())
}
})
]
}),
parent: editorRef.value
}) })
cm.value.setValue(props.modelValue)
cm.value.on('change', c => {
emit('update:modelValue', c.getValue())
})
cm.value.setSize(null, `${props.minHeight}px`)
}) })
onBeforeMount(() => { onBeforeMount(() => {
if (editor.value) { if (cm.value) {
editor.value.destroy() cm.value.destroy()
} }
}) })
</script> </script>
......
.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 @@ ...@@ -131,6 +131,8 @@
"admin.dev.voyager.title": "Voyager", "admin.dev.voyager.title": "Voyager",
"admin.editors.apiDescription": "Document your REST / GraphQL APIs.", "admin.editors.apiDescription": "Document your REST / GraphQL APIs.",
"admin.editors.apiName": "API Docs Editor", "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.blogDescription": "Write a series of posts over time.",
"admin.editors.blogName": "Blog Editor", "admin.editors.blogName": "Blog Editor",
"admin.editors.channelDescription": "Create discussion channels to collaborate in real-time with your team.", "admin.editors.channelDescription": "Create discussion channels to collaborate in real-time with your team.",
...@@ -1675,5 +1677,6 @@ ...@@ -1675,5 +1677,6 @@
"welcome.admin": "Administration Area", "welcome.admin": "Administration Area",
"welcome.createHome": "Create the homepage", "welcome.createHome": "Create the homepage",
"welcome.subtitle": "Let's get started...", "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') ...@@ -87,10 +87,10 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rfid-tag.svg') q-icon(name='img:/_assets/icons/fluent-rfid-tag.svg')
q-item-section {{ t('admin.blocks.title') }} 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(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-cashbook.svg') q-icon(name='img:/_assets/icons/fluent-cashbook.svg')
q-item-section {{ t('admin.editors.title') }} q-item-section {{ t('admin.editors.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/locale`', v-ripple, active-class='bg-primary text-white') q-item(:to='`/_admin/` + adminStore.currentSiteId + `/locale`', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar) q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-language.svg') q-icon(name='img:/_assets/icons/fluent-language.svg')
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
q-layout(view='hHh Lpr lff') q-layout(view='hHh Lpr lff')
header-nav header-nav
q-drawer.bg-sidebar( q-drawer.bg-sidebar(
v-model='siteStore.showSideNav' :modelValue='isSidebarShown'
show-if-above show-if-above
:width='255' :width='255'
) )
...@@ -83,11 +83,12 @@ q-layout(view='hHh Lpr lff') ...@@ -83,11 +83,12 @@ q-layout(view='hHh Lpr lff')
<script setup> <script setup>
import { useMeta, useQuasar } from 'quasar' 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 { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useSiteStore } from '../stores/site' import { useEditorStore } from 'src/stores/editor'
import { useSiteStore } from 'src/stores/site'
// COMPONENTS // COMPONENTS
...@@ -101,6 +102,7 @@ const $q = useQuasar() ...@@ -101,6 +102,7 @@ const $q = useQuasar()
// STORES // STORES
const editorStore = useEditorStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
// ROUTER // ROUTER
...@@ -136,6 +138,12 @@ const barStyle = { ...@@ -136,6 +138,12 @@ const barStyle = {
opacity: 0.1 opacity: 0.1
} }
// COMPUTED
const isSidebarShown = computed(() => {
return siteStore.showSideNav && !(editorStore.isActive && editorStore.hideSideNav)
})
// METHODS // METHODS
function openFileManager () { function openFileManager () {
......
...@@ -17,7 +17,7 @@ q-page.admin-mail ...@@ -17,7 +17,7 @@ q-page.admin-mail
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -19,61 +19,75 @@ q-page.admin-flags ...@@ -19,61 +19,75 @@ q-page.admin-flags
icon='las la-redo-alt' icon='las la-redo-alt'
flat flat
color='secondary' color='secondary'
:loading='loading > 0' :loading='state.loading > 0'
@click='load' @click='refresh'
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
:disabled='loading > 0' :disabled='state.loading > 0'
) )
q-separator(inset) q-separator(inset)
.q-pa-md.q-gutter-md .q-pa-md.q-gutter-md
q-card.shadow-1 q-card.shadow-1
q-list(separator) q-list(separator)
q-item(v-for='editor of editors', :key='editor.id') template(v-for='editor of editors', :key='editor.id')
blueprint-icon(:icon='editor.icon') q-item(v-if='flagsStore.experimental || !editor.isDisabled')
q-item-section blueprint-icon(:icon='editor.icon')
q-item-label: strong {{t(`admin.editors.` + editor.id + `Name`)}} q-item-section
q-item-label.flex.items-center(caption) q-item-label: strong {{t(`admin.editors.` + editor.id + `Name`)}}
span {{t(`admin.editors.` + editor.id + `Description`)}} q-item-label(caption)
template(v-if='editor.config') span {{t(`admin.editors.` + editor.id + `Description`)}}
q-item-section( q-item-label(caption, v-if='editor.useRendering')
side em.text-purple {{ t('admin.editors.useRenderingPipeline') }}
) template(v-if='editor.hasConfig')
q-btn( q-item-section(
icon='las la-cog' side
:label='t(`admin.editors.configuration`)' )
:color='$q.dark.isActive ? `blue-grey-3` : `blue-grey-8`' q-btn(
outline icon='las la-cog'
no-caps :label='t(`admin.editors.configuration`)'
padding='xs md' :color='$q.dark.isActive ? `blue-grey-3` : `blue-grey-8`'
) outline
q-separator.q-ml-md(vertical) no-caps
q-item-section(side) padding='xs md'
q-toggle.q-pr-sm( @click='openConfig(editor.id)'
v-model='editor.isActive' )
:color='editor.isDisabled ? `grey` : `primary`' q-separator.q-ml-md(vertical)
checked-icon='las la-check' q-item-section(side)
unchecked-icon='las la-times' q-toggle.q-pr-sm(
:label='t(`admin.sites.isActive`)' v-model='state.config[editor.id]'
:aria-label='t(`admin.sites.isActive`)' :color='editor.isDisabled ? `grey` : `primary`'
:disabled='editor.isDisabled' 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> </template>
<script setup> <script setup>
import { useMeta } from 'quasar' import { useMeta, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n' 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' import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()
// STORES // STORES
const adminStore = useAdminStore()
const flagsStore = useFlagsStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
// I18N // I18N
...@@ -86,46 +100,146 @@ useMeta({ ...@@ -86,46 +100,146 @@ useMeta({
title: t('admin.editors.title') 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([ const editors = reactive([
{ {
id: 'wysiwyg', id: 'api',
icon: 'google-presentation', icon: 'api',
isActive: true isDisabled: true,
}, useRendering: false
{
id: 'markdown',
icon: 'markdown',
config: {},
isActive: true
}, },
{ {
id: 'channel', id: 'asciidoc',
icon: 'chat', icon: 'asciidoc',
isActive: true hasConfig: true,
useRendering: true
}, },
{ {
id: 'blog', id: 'blog',
icon: 'typewriter-with-paper', icon: 'typewriter-with-paper',
isActive: true, isDisabled: true,
isDisabled: true useRendering: true
}, },
{ {
id: 'api', id: 'channel',
icon: 'api', icon: 'chat',
isActive: true, isDisabled: true,
isDisabled: true useRendering: false
},
{
id: 'markdown',
icon: 'markdown',
hasConfig: true,
useRendering: true
}, },
{ {
id: 'redirect', id: 'redirect',
icon: 'advance', icon: 'advance',
isActive: true isDisabled: true,
useRendering: false
},
{
id: 'wysiwyg',
icon: 'google-presentation',
useRendering: true
} }
]) ])
const load = async () => {} // WATCHERS
const save = () => {}
const refresh = () => {} 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> </script>
<style lang='scss'> <style lang='scss'>
......
...@@ -24,7 +24,7 @@ q-page.admin-flags ...@@ -24,7 +24,7 @@ q-page.admin-flags
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -24,7 +24,7 @@ q-page.admin-general ...@@ -24,7 +24,7 @@ q-page.admin-general
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -24,7 +24,7 @@ q-page.admin-icons ...@@ -24,7 +24,7 @@ q-page.admin-icons
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
...@@ -145,7 +145,6 @@ const packs = [ ...@@ -145,7 +145,6 @@ const packs = [
key: 'fa', key: 'fa',
label: 'Font Awesome', label: 'Font Awesome',
website: 'https://fontawesome.com', website: 'https://fontawesome.com',
isMandatory: true,
config: {} config: {}
}, },
{ {
...@@ -163,7 +162,7 @@ const packs = [ ...@@ -163,7 +162,7 @@ const packs = [
key: 'mdi', key: 'mdi',
label: 'Material Design Icons', label: 'Material Design Icons',
website: 'https://materialdesignicons.com', website: 'https://materialdesignicons.com',
config: {} isMandatory: true
}, },
{ {
key: 'thm', key: 'thm',
......
...@@ -33,7 +33,7 @@ q-page.admin-locale ...@@ -33,7 +33,7 @@ q-page.admin-locale
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -24,7 +24,7 @@ q-page.admin-login ...@@ -24,7 +24,7 @@ q-page.admin-login
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -24,7 +24,7 @@ q-page.admin-mail ...@@ -24,7 +24,7 @@ q-page.admin-mail
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -24,7 +24,7 @@ q-page.admin-navigation ...@@ -24,7 +24,7 @@ q-page.admin-navigation
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -24,7 +24,7 @@ q-page.admin-mail ...@@ -24,7 +24,7 @@ q-page.admin-mail
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -36,7 +36,7 @@ q-page.admin-storage ...@@ -36,7 +36,7 @@ q-page.admin-storage
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
...@@ -25,7 +25,7 @@ q-page.admin-system ...@@ -25,7 +25,7 @@ q-page.admin-system
q-btn.acrylic-btn( q-btn.acrylic-btn(
ref='copySysInfoBtn' ref='copySysInfoBtn'
flat flat
icon='fa-regular fa-clipboard' icon='mdi-clipboard-text-outline'
label='Copy System Info' label='Copy System Info'
color='primary' color='primary'
@click='' @click=''
......
...@@ -24,7 +24,7 @@ q-page.admin-theme ...@@ -24,7 +24,7 @@ q-page.admin-theme
) )
q-btn( q-btn(
unelevated unelevated
icon='fa-solid fa-check' icon='mdi-check'
:label='t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
......
<template lang='pug'> <template lang='pug'>
q-page.column 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 .col
q-breadcrumbs( q-breadcrumbs(
active-color='grey-7' active-color='grey-7'
...@@ -36,7 +38,7 @@ q-page.column ...@@ -36,7 +38,7 @@ q-page.column
//- PAGE HEADER //- PAGE HEADER
.col.q-pa-md .col.q-pa-md
.text-h4.page-header-title {{pageStore.title}} .text-h4.page-header-title {{pageStore.title}}
.text-subtitle2.page-header-subtitle {{pageStore.description}} .text-subtitle2.page-header-subtitle {{pageStore.description }}
//- PAGE ACTIONS //- PAGE ACTIONS
.col-auto.q-pa-md.flex.items-center.justify-end .col-auto.q-pa-md.flex.items-center.justify-end
...@@ -100,11 +102,16 @@ q-page.column ...@@ -100,11 +102,16 @@ q-page.column
label='Edit' label='Edit'
aria-label='Edit' aria-label='Edit'
no-caps no-caps
:href='editUrl' @click='editPage'
) )
.page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;') .page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
.col(style='order: 1;') .col(style='order: 1;')
q-no-ssr(
v-if='editorStore.isActive'
)
component(:is='editorComponents[editorStore.editor]')
q-scroll-area( q-scroll-area(
v-else
:thumb-style='thumbStyle' :thumb-style='thumbStyle'
:bar-style='barStyle' :bar-style='barStyle'
style='height: 100%;' style='height: 100%;'
...@@ -344,6 +351,17 @@ const sideDialogs = { ...@@ -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 // QUASAR
const $q = useQuasar() const $q = useQuasar()
...@@ -399,10 +417,7 @@ const barStyle = { ...@@ -399,10 +417,7 @@ const barStyle = {
// COMPUTED // COMPUTED
const showSidebar = computed(() => { const showSidebar = computed(() => {
return pageStore.showSidebar && siteStore.showSidebar return pageStore.showSidebar && siteStore.showSidebar && !editorStore.isActive
})
const editorComponent = computed(() => {
return pageStore.editor ? `editor-${pageStore.editor}` : null
}) })
const relationsLeft = computed(() => { const relationsLeft = computed(() => {
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'left') : [] return pageStore.relations ? pageStore.relations.filter(r => r.position === 'left') : []
...@@ -564,6 +579,13 @@ async function saveChanges () { ...@@ -564,6 +579,13 @@ async function saveChanges () {
} }
$q.loading.hide() $q.loading.hide()
} }
function editPage () {
editorStore.$patch({
isActive: true,
editor: 'markdown'
})
}
</script> </script>
<style lang="scss"> <style lang="scss">
...@@ -578,6 +600,8 @@ async function saveChanges () { ...@@ -578,6 +600,8 @@ async function saveChanges () {
} }
} }
.page-header { .page-header {
min-height: 95px;
@at-root .body--light & { @at-root .body--light & {
background: linear-gradient(to bottom, $grey-2 0%, $grey-1 100%); background: linear-gradient(to bottom, $grey-2 0%, $grey-1 100%);
border-bottom: 1px solid $grey-4; border-bottom: 1px solid $grey-4;
......
...@@ -2,11 +2,13 @@ import { defineStore } from 'pinia' ...@@ -2,11 +2,13 @@ import { defineStore } from 'pinia'
export const useEditorStore = defineStore('editor', { export const useEditorStore = defineStore('editor', {
state: () => ({ state: () => ({
isActive: false,
editor: '', editor: '',
content: '', content: '',
mode: 'create', mode: 'create',
activeModal: '', activeModal: '',
activeModalData: null, activeModalData: null,
hideSideNav: false,
media: { media: {
folderTree: [], folderTree: [],
currentFolderId: 0, currentFolderId: 0,
......
...@@ -174,9 +174,7 @@ export const usePageStore = defineStore('page', { ...@@ -174,9 +174,7 @@ export const usePageStore = defineStore('page', {
* PAGE - CREATE * PAGE - CREATE
*/ */
pageCreate ({ editor, locale, path }) { pageCreate ({ editor, locale, path }) {
// -> Editor View const editorStore = useEditorStore()
this.editor = editor
this.editorMode = 'create'
// if (['markdown', 'api'].includes(editor)) { // if (['markdown', 'api'].includes(editor)) {
// commit('site/SET_SHOW_SIDE_NAV', false, { root: true }) // commit('site/SET_SHOW_SIDE_NAV', false, { root: true })
...@@ -204,13 +202,19 @@ export const usePageStore = defineStore('page', { ...@@ -204,13 +202,19 @@ export const usePageStore = defineStore('page', {
this.isPublished = false this.isPublished = false
this.relations = [] this.relations = []
this.tags = [] this.tags = []
this.breadcrumbs = []
this.content = '' this.content = ''
this.render = '' this.render = ''
// -> View Mode // -> View Mode
this.mode = 'edit' this.mode = 'edit'
// -> Editor Mode
editorStore.$patch({
isActive: true,
editor,
mode: 'create'
})
}, },
/** /**
* PAGE SAVE * PAGE SAVE
......
This diff was suppressed by a .gitattributes entry.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment