feat: native editing + admin editors (wip)

pull/6775/head
Nicolas Giard 3 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 /uploads
/content /content
/temp /temp
/tmp
*.sqlite *.sqlite
# IDE exclude # IDE exclude

@ -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:
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 => {
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 {
locale: String locale: String
localeNamespaces: [String] localeNamespaces: [String]
localeNamespacing: Boolean localeNamespacing: Boolean
editors: SiteEditors
theme: SiteTheme theme: SiteTheme
} }
@ -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

@ -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 ./" "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 @@
"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 */) {
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 */) {
// /^\/_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 */) {
} }
}, },
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

@ -1,13 +1,396 @@
<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>

@ -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>

@ -8,7 +8,7 @@
) )
q-btn( q-btn(
v-else-if='menuItem.type === `dropdown`' v-else-if='menuItem.type === `dropdown`'
:key='menuItem.key' :key='`ddn-` + menuItem.key'
flat flat
:icon='menuItem.icon' :icon='menuItem.icon'
padding='xs' padding='xs'
@ -27,7 +27,7 @@
q-separator.q-my-sm(v-if='child.type === `divider`') q-separator.q-my-sm(v-if='child.type === `divider`')
q-item( q-item(
v-else v-else
:key='menuItem.key + `-` + child.key' :key='child.key'
clickable clickable
@click='child.action' @click='child.action'
:active='child.isActive && child.isActive()' :active='child.isActive && child.isActive()'
@ -43,12 +43,12 @@
q-item-label {{child.title}} q-item-label {{child.title}}
q-btn-group( q-btn-group(
v-else-if='menuItem.type === `btngroup`' v-else-if='menuItem.type === `btngroup`'
:key='menuItem.key' :key='`btngrp-` + menuItem.key'
flat flat
) )
q-btn( q-btn(
v-for='child of menuItem.children' v-for='child of menuItem.children'
:key='menuItem.key + `-` + child.key' :key='child.key'
flat flat
:icon='child.icon' :icon='child.icon'
padding='xs' padding='xs'
@ -60,6 +60,7 @@
) )
q-btn( q-btn(
v-else v-else
:key='`btn-` + menuItem.key'
flat flat
:icon='menuItem.icon' :icon='menuItem.icon'
padding='xs' padding='xs'
@ -69,24 +70,24 @@
:aria-label='menuItem.title' :aria-label='menuItem.title'
:disabled='menuItem.disabled && menuItem.disabled()' :disabled='menuItem.disabled && menuItem.disabled()'
) )
q-space //- q-space
q-btn( //- q-btn(
size='sm' //- size='sm'
unelevated //- unelevated
color='red' //- color='red'
label='Test' //- label='Test'
@click='snapshot' //- @click='snapshot'
) //- )
q-scroll-area( //- q-scroll-area(
:thumb-style='thumbStyle' //- :thumb-style='thumbStyle'
:bar-style='barStyle' //- :bar-style='barStyle'
style='height: 100%;' //- style='height: 100%;'
) //- )
editor-content(:editor='editor') editor-content(:editor='editor')
</template> </template>
<script> <script setup>
import { Editor, EditorContent } from '@tiptap/vue-3' import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
// import Collaboration from '@tiptap/extension-collaboration' // import Collaboration from '@tiptap/extension-collaboration'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
@ -105,77 +106,97 @@ import TaskItem from '@tiptap/extension-task-item'
import TextAlign from '@tiptap/extension-text-align' import TextAlign from '@tiptap/extension-text-align'
import TextStyle from '@tiptap/extension-text-style' import TextStyle from '@tiptap/extension-text-style'
import Typography from '@tiptap/extension-typography' import Typography from '@tiptap/extension-typography'
import { lowlight } from 'lowlight/lib/core'
import { onBeforeUnmount, onMounted, reactive, shallowRef } from 'vue'
// import * as Y from 'yjs' // import * as Y from 'yjs'
// import { IndexeddbPersistence } from 'y-indexeddb' // import { IndexeddbPersistence } from 'y-indexeddb'
// import { WebsocketProvider } from 'y-websocket' // import { WebsocketProvider } from 'y-websocket'
export default { import { useMeta, useQuasar, setCssVar } from 'quasar'
components: { import { useI18n } from 'vue-i18n'
EditorContent
}, import { useEditorStore } from 'src/stores/editor'
data () {
return { // QUASAR
editor: null,
ydoc: null, const $q = useQuasar()
thumbStyle: {
// STORES
const editorStore = useEditorStore()
// I18N
const { t } = useI18n()
// STATE
const state = reactive({
// editor: null,
ydoc: null
})
let editor = null
const thumbStyle = {
right: '2px', right: '2px',
borderRadius: '5px', borderRadius: '5px',
backgroundColor: '#000', backgroundColor: '#000',
width: '5px', width: '5px',
opacity: 0.15 opacity: 0.15
}, }
barStyle: { const barStyle = {
backgroundColor: '#FAFAFA', backgroundColor: '#FAFAFA',
width: '9px', width: '9px',
opacity: 1 opacity: 1
}, }
menuBar: [ const menuBar = [
{ {
key: 'bold', key: 'bold',
icon: 'mdi-format-bold', icon: 'mdi-format-bold',
title: 'Bold', title: 'Bold',
action: () => this.editor.chain().focus().toggleBold().run(), action: () => editor.value.chain().focus().toggleBold().run(),
isActive: () => this.editor.isActive('bold') isActive: () => editor.value.isActive('bold')
}, },
{ {
key: 'italic', key: 'italic',
icon: 'mdi-format-italic', icon: 'mdi-format-italic',
title: 'Italic', title: 'Italic',
action: () => this.editor.chain().focus().toggleItalic().run(), action: () => editor.value.chain().focus().toggleItalic().run(),
isActive: () => this.editor.isActive('italic') isActive: () => editor.value.isActive('italic')
}, },
{ {
key: 'strikethrough', key: 'strikethrough',
icon: 'mdi-format-strikethrough', icon: 'mdi-format-strikethrough',
title: 'Strike', title: 'Strike',
action: () => this.editor.chain().focus().toggleStrike().run(), action: () => editor.value.chain().focus().toggleStrike().run(),
isActive: () => this.editor.isActive('strike') isActive: () => editor.value.isActive('strike')
}, },
{ {
key: 'code', key: 'code',
icon: 'mdi-code-tags', icon: 'mdi-code-tags',
title: 'Code', title: 'Code',
action: () => this.editor.chain().focus().toggleCode().run(), action: () => editor.value.chain().focus().toggleCode().run(),
isActive: () => this.editor.isActive('code') isActive: () => editor.value.isActive('code')
}, },
{ {
key: 'fontfamily', key: 'fontfamily',
icon: 'mdi-format-font', icon: 'mdi-format-font',
title: 'Font Family', title: 'Font Family',
type: 'dropdown', type: 'dropdown',
isActive: () => this.editor.isActive('fontFamily'), isActive: () => editor.value.isActive('fontFamily'),
children: [ children: [
{ {
key: 'fontunset', key: 'fontunset',
icon: 'mdi-format-font', icon: 'mdi-format-font',
title: 'Sans-Serif', title: 'Sans-Serif',
action: () => this.editor.chain().focus().unsetFontFamily().run() action: () => editor.value.chain().focus().unsetFontFamily().run()
}, },
{ {
key: 'monospace', key: 'monospace',
icon: 'mdi-format-font', icon: 'mdi-format-font',
title: 'Monospace', title: 'Monospace',
action: () => this.editor.chain().focus().setFontFamily('monospace').run() action: () => editor.value.chain().focus().setFontFamily('monospace').run()
} }
] ]
}, },
@ -184,80 +205,80 @@ export default {
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Text Color', title: 'Text Color',
type: 'dropdown', type: 'dropdown',
isActive: () => this.editor.isActive('color'), isActive: () => editor.value.isActive('color'),
children: [ children: [
{ {
key: 'blue', key: 'color-blue',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Blue', title: 'Blue',
color: 'blue', color: 'blue',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'brown', key: 'color-brown',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Brown', title: 'Brown',
color: 'brown', color: 'brown',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'green', key: 'color-green',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Green', title: 'Green',
color: 'green', color: 'green',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'orange', key: 'color-orange',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Orange', title: 'Orange',
color: 'orange', color: 'orange',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'pink', key: 'color-pink',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Pink', title: 'Pink',
color: 'pink', color: 'pink',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'purple', key: 'color-purple',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Purple', title: 'Purple',
color: 'purple', color: 'purple',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'red', key: 'color-red',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Red', title: 'Red',
color: 'red', color: 'red',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'teal', key: 'color-teal',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Teal', title: 'Teal',
color: 'teal', color: 'teal',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'yellow', key: 'color-yellow',
icon: 'mdi-palette', icon: 'mdi-palette',
title: 'Yellow', title: 'Yellow',
color: 'yellow', color: 'yellow',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
type: 'divider' type: 'divider'
}, },
{ {
key: 'remove', key: 'color-remove',
icon: 'mdi-water-off', icon: 'mdi-palette',
title: 'Default', title: 'Default',
color: 'grey', color: 'grey',
action: () => this.editor.chain().focus().unsetHighlight().run() action: () => editor.value.chain().focus().unsetHighlight().run()
} }
] ]
}, },
@ -266,52 +287,52 @@ export default {
icon: 'mdi-marker', icon: 'mdi-marker',
title: 'Highlight', title: 'Highlight',
type: 'dropdown', type: 'dropdown',
isActive: () => this.editor.isActive('highlight'), isActive: () => editor.value.isActive('highlight'),
children: [ children: [
{ {
key: 'yellow', key: 'highlight-yellow',
icon: 'mdi-marker', icon: 'mdi-marker',
title: 'Yellow', title: 'Yellow',
color: 'yellow', color: 'yellow',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'blue', key: 'highlight-blue',
icon: 'mdi-marker', icon: 'mdi-marker',
title: 'Blue', title: 'Blue',
color: 'blue', color: 'blue',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'pink', key: 'highlight-pink',
icon: 'mdi-marker', icon: 'mdi-marker',
title: 'Pink', title: 'Pink',
color: 'pink', color: 'pink',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'green', key: 'highlight-green',
icon: 'mdi-marker', icon: 'mdi-marker',
title: 'Green', title: 'Green',
color: 'green', color: 'green',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
key: 'orange', key: 'highlight-orange',
icon: 'mdi-marker', icon: 'mdi-marker',
title: 'Orange', title: 'Orange',
color: 'orange', color: 'orange',
action: () => this.editor.chain().focus().toggleHighlight().run() action: () => editor.value.chain().focus().toggleHighlight().run()
}, },
{ {
type: 'divider' type: 'divider'
}, },
{ {
key: 'remove', key: 'highlight-remove',
icon: 'mdi-marker-cancel', icon: 'mdi-marker-cancel',
title: 'Remove', title: 'Remove',
color: 'grey', color: 'grey',
action: () => this.editor.chain().focus().unsetHighlight().run() action: () => editor.value.chain().focus().unsetHighlight().run()
} }
] ]
}, },
@ -323,49 +344,49 @@ export default {
icon: 'mdi-format-header-pound', icon: 'mdi-format-header-pound',
title: 'Header', title: 'Header',
type: 'dropdown', type: 'dropdown',
isActive: () => this.editor.isActive('heading'), isActive: () => editor.value.isActive('heading'),
children: [ children: [
{ {
key: 'h1', key: 'h1',
icon: 'mdi-format-header-1', icon: 'mdi-format-header-1',
title: 'Header 1', title: 'Header 1',
action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(), action: () => editor.value.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => this.editor.isActive('heading', { level: 1 }) isActive: () => editor.value.isActive('heading', { level: 1 })
}, },
{ {
key: 'h2', key: 'h2',
icon: 'mdi-format-header-2', icon: 'mdi-format-header-2',
title: 'Header 2', title: 'Header 2',
action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(), action: () => editor.value.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => this.editor.isActive('heading', { level: 2 }) isActive: () => editor.value.isActive('heading', { level: 2 })
}, },
{ {
key: 'h3', key: 'h3',
icon: 'mdi-format-header-3', icon: 'mdi-format-header-3',
title: 'Header 3', title: 'Header 3',
action: () => this.editor.chain().focus().toggleHeading({ level: 3 }).run(), action: () => editor.value.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => this.editor.isActive('heading', { level: 3 }) isActive: () => editor.value.isActive('heading', { level: 3 })
}, },
{ {
key: 'h4', key: 'h4',
icon: 'mdi-format-header-4', icon: 'mdi-format-header-4',
title: 'Header 4', title: 'Header 4',
action: () => this.editor.chain().focus().toggleHeading({ level: 4 }).run(), action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(),
isActive: () => this.editor.isActive('heading', { level: 4 }) isActive: () => editor.value.isActive('heading', { level: 4 })
}, },
{ {
key: 'h5', key: 'h5',
icon: 'mdi-format-header-5', icon: 'mdi-format-header-5',
title: 'Header 5', title: 'Header 5',
action: () => this.editor.chain().focus().toggleHeading({ level: 5 }).run(), action: () => editor.value.chain().focus().toggleHeading({ level: 5 }).run(),
isActive: () => this.editor.isActive('heading', { level: 5 }) isActive: () => editor.value.isActive('heading', { level: 5 })
}, },
{ {
key: 'h6', key: 'h6',
icon: 'mdi-format-header-6', icon: 'mdi-format-header-6',
title: 'Header 6', title: 'Header 6',
action: () => this.editor.chain().focus().toggleHeading({ level: 6 }).run(), action: () => editor.value.chain().focus().toggleHeading({ level: 6 }).run(),
isActive: () => this.editor.isActive('heading', { level: 6 }) isActive: () => editor.value.isActive('heading', { level: 6 })
} }
] ]
}, },
@ -373,8 +394,8 @@ export default {
key: 'paragraph', key: 'paragraph',
icon: 'mdi-format-paragraph', icon: 'mdi-format-paragraph',
title: 'Paragraph', title: 'Paragraph',
action: () => this.editor.chain().focus().setParagraph().run(), action: () => editor.value.chain().focus().setParagraph().run(),
isActive: () => this.editor.isActive('paragraph') isActive: () => editor.value.isActive('paragraph')
}, },
{ {
type: 'divider' type: 'divider'
@ -384,32 +405,32 @@ export default {
type: 'btngroup', type: 'btngroup',
children: [ children: [
{ {
key: 'left', key: 'align-left',
icon: 'mdi-format-align-left', icon: 'mdi-format-align-left',
title: 'Left Align', title: 'Left Align',
action: () => this.editor.chain().focus().setTextAlign('left').run(), action: () => editor.value.chain().focus().setTextAlign('left').run(),
isActive: () => this.editor.isActive({ textAlign: 'left' }) isActive: () => editor.value.isActive({ textAlign: 'left' })
}, },
{ {
key: 'center', key: 'align-center',
icon: 'mdi-format-align-center', icon: 'mdi-format-align-center',
title: 'Center Align', title: 'Center Align',
action: () => this.editor.chain().focus().setTextAlign('center').run(), action: () => editor.value.chain().focus().setTextAlign('center').run(),
isActive: () => this.editor.isActive({ textAlign: 'center' }) isActive: () => editor.value.isActive({ textAlign: 'center' })
}, },
{ {
key: 'right', key: 'align-right',
icon: 'mdi-format-align-right', icon: 'mdi-format-align-right',
title: 'Right Align', title: 'Right Align',
action: () => this.editor.chain().focus().setTextAlign('right').run(), action: () => editor.value.chain().focus().setTextAlign('right').run(),
isActive: () => this.editor.isActive({ textAlign: 'right' }) isActive: () => editor.value.isActive({ textAlign: 'right' })
}, },
{ {
key: 'justify', key: 'align-justify',
icon: 'mdi-format-align-justify', icon: 'mdi-format-align-justify',
title: 'Justify Align', title: 'Justify Align',
action: () => this.editor.chain().focus().setTextAlign('justify').run(), action: () => editor.value.chain().focus().setTextAlign('justify').run(),
isActive: () => this.editor.isActive({ textAlign: 'justify' }) isActive: () => editor.value.isActive({ textAlign: 'justify' })
} }
] ]
}, },
@ -420,22 +441,22 @@ export default {
key: 'bulletlist', key: 'bulletlist',
icon: 'mdi-format-list-bulleted', icon: 'mdi-format-list-bulleted',
title: 'Bullet List', title: 'Bullet List',
action: () => this.editor.chain().focus().toggleBulletList().run(), action: () => editor.value.chain().focus().toggleBulletList().run(),
isActive: () => this.editor.isActive('bulletList') isActive: () => editor.value.isActive('bulletList')
}, },
{ {
key: 'orderedlist', key: 'orderedlist',
icon: 'mdi-format-list-numbered', icon: 'mdi-format-list-numbered',
title: 'Ordered List', title: 'Ordered List',
action: () => this.editor.chain().focus().toggleOrderedList().run(), action: () => editor.value.chain().focus().toggleOrderedList().run(),
isActive: () => this.editor.isActive('orderedList') isActive: () => editor.value.isActive('orderedList')
}, },
{ {
key: 'tasklist', key: 'tasklist',
icon: 'mdi-format-list-checkbox', icon: 'mdi-format-list-checks',
title: 'Task List', title: 'Task List',
action: () => this.editor.chain().focus().toggleTaskList().run(), action: () => editor.value.chain().focus().toggleTaskList().run(),
isActive: () => this.editor.isActive('taskList') isActive: () => editor.value.isActive('taskList')
}, },
{ {
type: 'divider' type: 'divider'
@ -444,25 +465,25 @@ export default {
key: 'codeblock', key: 'codeblock',
icon: 'mdi-code-json', icon: 'mdi-code-json',
title: 'Code Block', title: 'Code Block',
action: () => this.editor.chain().focus().toggleCodeBlock().run(), action: () => editor.value.chain().focus().toggleCodeBlock().run(),
isActive: () => this.editor.isActive('codeBlock') isActive: () => editor.value.isActive('codeBlock')
}, },
{ {
key: 'blockquote', key: 'blockquote',
icon: 'mdi-format-quote-close', icon: 'mdi-format-quote-open',
title: 'Blockquote', title: 'Blockquote',
action: () => this.editor.chain().focus().toggleBlockquote().run(), action: () => editor.value.chain().focus().toggleBlockquote().run(),
isActive: () => this.editor.isActive('blockquote') isActive: () => editor.value.isActive('blockquote')
}, },
{ {
key: 'rule', key: 'rule',
icon: 'mdi-minus', icon: 'mdi-minus',
title: 'Horizontal Rule', title: 'Horizontal Rule',
action: () => this.editor.chain().focus().setHorizontalRule().run() action: () => editor.value.chain().focus().setHorizontalRule().run()
}, },
{ {
key: 'link', key: 'link',
icon: 'mdi-link-plus', icon: 'mdi-link-variant',
title: 'Link', title: 'Link',
action: () => { action: () => {
// TODO: insert link // TODO: insert link
@ -478,122 +499,122 @@ export default {
}, },
{ {
key: 'table', key: 'table',
icon: 'mdi-table-large', icon: 'mdi-table',
title: 'Table', title: 'Table',
type: 'dropdown', type: 'dropdown',
isActive: () => this.editor.isActive('table'), isActive: () => editor.value.isActive('table'),
children: [ children: [
{ {
key: 'insert', key: 'table-insert',
icon: 'mdi-table-large-plus', icon: 'mdi-table-large-plus',
title: 'Insert Table', title: 'Insert Table',
action: () => this.editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() action: () => editor.value.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
}, },
{ {
type: 'divider' type: 'divider'
}, },
{ {
key: 'addcolumnbefore', key: 'table-addcolumnbefore',
icon: 'mdi-table-column-plus-before', icon: 'mdi-table-column-plus-before',
title: 'Add Column Before', title: 'Add Column Before',
action: () => this.editor.chain().focus().addColumnBefore().run(), action: () => editor.value.chain().focus().addColumnBefore().run(),
disabled: () => !this.editor.can().addColumnBefore() disabled: () => !editor.value.can().addColumnBefore()
}, },
{ {
key: 'addcolumnafter', key: 'table-addcolumnafter',
icon: 'mdi-table-column-plus-after', icon: 'mdi-table-column-plus-after',
title: 'Add Column After', title: 'Add Column After',
action: () => this.editor.chain().focus().addColumnAfter().run(), action: () => editor.value.chain().focus().addColumnAfter().run(),
disabled: () => !this.editor.can().addColumnAfter() disabled: () => !editor.value.can().addColumnAfter()
}, },
{ {
key: 'deletecolumn', key: 'table-deletecolumn',
icon: 'mdi-table-column-remove', icon: 'mdi-table-column-remove',
title: 'Remove Column', title: 'Remove Column',
action: () => this.editor.chain().focus().deleteColumn().run(), action: () => editor.value.chain().focus().deleteColumn().run(),
disabled: () => !this.editor.can().deleteColumn() disabled: () => !editor.value.can().deleteColumn()
}, },
{ {
type: 'divider' type: 'divider'
}, },
{ {
key: 'addrowbefore', key: 'table-addrowbefore',
icon: 'mdi-table-row-plus-before', icon: 'mdi-table-row-plus-before',
title: 'Add Row Before', title: 'Add Row Before',
action: () => this.editor.chain().focus().addRowBefore().run(), action: () => editor.value.chain().focus().addRowBefore().run(),
disabled: () => !this.editor.can().addRowBefore() disabled: () => !editor.value.can().addRowBefore()
}, },
{ {
key: 'addrowafter', key: 'table-addrowafter',
icon: 'mdi-table-row-plus-after', icon: 'mdi-table-row-plus-after',
title: 'Add Row After', title: 'Add Row After',
action: () => this.editor.chain().focus().addRowAfter().run(), action: () => editor.value.chain().focus().addRowAfter().run(),
disabled: () => !this.editor.can().addRowAfter() disabled: () => !editor.value.can().addRowAfter()
}, },
{ {
key: 'deleterow', key: 'table-deleterow',
icon: 'mdi-table-row-remove', icon: 'mdi-table-row-remove',
title: 'Remove Row', title: 'Remove Row',
action: () => this.editor.chain().focus().deleteRow().run(), action: () => editor.value.chain().focus().deleteRow().run(),
disabled: () => !this.editor.can().deleteRow() disabled: () => !editor.value.can().deleteRow()
}, },
{ {
type: 'divider' type: 'divider'
}, },
{ {
key: 'merge', key: 'table-merge',
icon: 'mdi-table-merge-cells', icon: 'mdi-table-merge-cells',
title: 'Merge Cells', title: 'Merge Cells',
action: () => this.editor.chain().focus().mergeCells().run(), action: () => editor.value.chain().focus().mergeCells().run(),
disabled: () => !this.editor.can().mergeCells() disabled: () => !editor.value.can().mergeCells()
}, },
{ {
key: 'split', key: 'table-split',
icon: 'mdi-table-split-cell', icon: 'mdi-table-split-cell',
title: 'Split Cell', title: 'Split Cell',
action: () => this.editor.chain().focus().splitCell().run(), action: () => editor.value.chain().focus().splitCell().run(),
disabled: () => !this.editor.can().splitCell() disabled: () => !editor.value.can().splitCell()
}, },
{ {
type: 'divider' type: 'divider'
}, },
{ {
key: 'toggleHeaderColumn', key: 'table-toggleHeaderColumn',
icon: 'mdi-table-column', icon: 'mdi-table-column',
title: 'Toggle Header Column', title: 'Toggle Header Column',
action: () => this.editor.chain().focus().toggleHeaderColumn().run(), action: () => editor.value.chain().focus().toggleHeaderColumn().run(),
disabled: () => !this.editor.can().toggleHeaderColumn() disabled: () => !editor.value.can().toggleHeaderColumn()
}, },
{ {
key: 'toggleHeaderRow', key: 'table-toggleHeaderRow',
icon: 'mdi-table-row', icon: 'mdi-table-row',
title: 'Toggle Header Row', title: 'Toggle Header Row',
action: () => this.editor.chain().focus().toggleHeaderRow().run(), action: () => editor.value.chain().focus().toggleHeaderRow().run(),
disabled: () => !this.editor.can().toggleHeaderRow() disabled: () => !editor.value.can().toggleHeaderRow()
}, },
{ {
key: 'toggleHeaderCell', key: 'table-toggleHeaderCell',
icon: 'mdi-crop-square', icon: 'mdi-crop-square',
title: 'Toggle Header Cell', title: 'Toggle Header Cell',
action: () => this.editor.chain().focus().toggleHeaderCell().run(), action: () => editor.value.chain().focus().toggleHeaderCell().run(),
disabled: () => !this.editor.can().toggleHeaderCell() disabled: () => !editor.value.can().toggleHeaderCell()
}, },
{ {
type: 'divider' type: 'divider'
}, },
{ {
key: 'fix', key: 'table-fix',
icon: 'mdi-table-heart', icon: 'mdi-table-heart',
title: 'Fix Table', title: 'Fix Table',
action: () => this.editor.chain().focus().fixTables().run(), action: () => editor.value.chain().focus().fixTables().run(),
disabled: () => !this.editor.can().fixTables() disabled: () => !editor.value.can().fixTables()
}, },
{ {
key: 'remove', key: 'table-remove',
icon: 'mdi-table-large-remove', icon: 'mdi-table-large-remove',
title: 'Delete Table', title: 'Delete Table',
action: () => this.editor.chain().focus().deleteTable().run(), action: () => editor.value.chain().focus().deleteTable().run(),
disabled: () => !this.editor.can().deleteTable() disabled: () => !editor.value.can().deleteTable()
} }
] ]
}, },
@ -604,13 +625,13 @@ export default {
key: 'pagebreak', key: 'pagebreak',
icon: 'mdi-format-page-break', icon: 'mdi-format-page-break',
title: 'Hard Break', title: 'Hard Break',
action: () => this.editor.chain().focus().setHardBreak().run() action: () => editor.value.chain().focus().setHardBreak().run()
}, },
{ {
key: 'clearformat', key: 'clearformat',
icon: 'mdi-format-clear', icon: 'mdi-format-clear',
title: 'Clear Format', title: 'Clear Format',
action: () => this.editor.chain() action: () => editor.value.chain()
.focus() .focus()
.clearNodes() .clearNodes()
.unsetAllMarks() .unsetAllMarks()
@ -623,30 +644,27 @@ export default {
key: 'undo', key: 'undo',
icon: 'mdi-undo-variant', icon: 'mdi-undo-variant',
title: 'Undo', title: 'Undo',
action: () => this.editor.chain().focus().undo().run(), action: () => editor.value.chain().focus().undo().run(),
disabled: () => !this.editor.can().undo() disabled: () => !editor.value.can().undo()
}, },
{ {
key: 'redo', key: 'redo',
icon: 'mdi-redo-variant', icon: 'mdi-redo-variant',
title: 'Redo', title: 'Redo',
action: () => this.editor.chain().focus().redo().run(), action: () => editor.value.chain().focus().redo().run(),
disabled: () => !this.editor.can().redo() disabled: () => !editor.value.can().redo()
} }
] ]
}
}, // METHODS
mounted () {
if (!import.meta.env.SSR) { function init () {
this.init() // -> Setup Editor View
} editorStore.$patch({
}, hideSideNav: false
beforeUnmount () { })
this.editor.destroy()
}, // -> Init Live Collab
methods: {
init () {
console.info('BOOP')
// this.ydoc = new Y.Doc() // this.ydoc = new Y.Doc()
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
@ -654,8 +672,9 @@ export default {
// const wsProvider = new WebsocketProvider('ws://127.0.0.1:1234', 'example-document', this.ydoc) // const wsProvider = new WebsocketProvider('ws://127.0.0.1:1234', 'example-document', this.ydoc)
/* eslint-enable no-unused-vars */ /* eslint-enable no-unused-vars */
this.editor = new Editor({ // -> Initialize TipTap
content: this.$store.get('page/render'), editor = useEditor({
content: '<p>Im running Tiptap with Vue.js. 🎉</p>', // editorStore.content,
extensions: [ extensions: [
StarterKit.configure({ StarterKit.configure({
codeBlock: false, codeBlock: false,
@ -663,7 +682,9 @@ export default {
depth: 500 depth: 500
} }
}), }),
CodeBlockLowlight, CodeBlockLowlight.configure({
lowlight
}),
Color, Color,
// Collaboration.configure({ // Collaboration.configure({
// document: this.ydoc // document: this.ydoc
@ -692,18 +713,29 @@ export default {
Typography Typography
], ],
onUpdate: () => { onUpdate: () => {
this.$store.set('page/render', this.editor.getHTML()) // this.$store.set('page/render', editor.getHTML())
} }
}) })
},
insertTable () {
// this.ql.getModule('table').insertTable(3, 3)
},
snapshot () {
// console.info(Y.encodeStateVector(this.ydoc))
} }
function insertTable () {
// this.ql.getModule('table').insertTable(3, 3)
} }
function snapshot () {
// console.info(Y.encodeStateVector(this.ydoc))
} }
// MOUNTED
onMounted(() => {
// init()
})
onBeforeUnmount(() => {
editor.value.destroy()
})
init()
</script> </script>
<style lang="scss"> <style lang="scss">

@ -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 () {

@ -1,18 +1,24 @@
<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([
// 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 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())
} }
// -> 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)
parent: editorRef.value 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>

@ -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.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 @@
"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,7 +87,7 @@ 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') }}

@ -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')
<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()
// STORES // STORES
const editorStore = useEditorStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
// ROUTER // ROUTER
@ -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
) )
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,28 +19,31 @@ 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')
q-item(v-if='flagsStore.experimental || !editor.isDisabled')
blueprint-icon(:icon='editor.icon') blueprint-icon(:icon='editor.icon')
q-item-section q-item-section
q-item-label: strong {{t(`admin.editors.` + editor.id + `Name`)}} q-item-label: strong {{t(`admin.editors.` + editor.id + `Name`)}}
q-item-label.flex.items-center(caption) q-item-label(caption)
span {{t(`admin.editors.` + editor.id + `Description`)}} span {{t(`admin.editors.` + editor.id + `Description`)}}
template(v-if='editor.config') q-item-label(caption, v-if='editor.useRendering')
em.text-purple {{ t('admin.editors.useRenderingPipeline') }}
template(v-if='editor.hasConfig')
q-item-section( q-item-section(
side side
) )
@ -51,11 +54,12 @@ q-page.admin-flags
outline outline
no-caps no-caps
padding='xs md' padding='xs md'
@click='openConfig(editor.id)'
) )
q-separator.q-ml-md(vertical) q-separator.q-ml-md(vertical)
q-item-section(side) q-item-section(side)
q-toggle.q-pr-sm( q-toggle.q-pr-sm(
v-model='editor.isActive' v-model='state.config[editor.id]'
:color='editor.isDisabled ? `grey` : `primary`' :color='editor.isDisabled ? `grey` : `primary`'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
@ -66,14 +70,24 @@ q-page.admin-flags
</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({
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
) )
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
) )
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
) )
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 = [
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 = [
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
) )
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
) )
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
) )
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
) )
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
) )
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
) )
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
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
) )
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'

@ -1,6 +1,8 @@
<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'
@ -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 = {
}) })
} }
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 = {
// 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 () {
} }
$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 () {
} }
} }
.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'
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', {
* 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', {
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

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