feat: native editing + admin editors (wip)

parent 3aafe116
......@@ -34,6 +34,7 @@ npm-debug.log*
/uploads
/content
/temp
/tmp
*.sqlite
# IDE exclude
......
......@@ -85,12 +85,25 @@ defaults:
maxHits: 100
maintainerEmail: security@requarks.io
editors:
code:
asciidoc:
contentType: html
config: {}
markdown:
contentType: markdown
config:
allowHTML: true
linkify: true
lineBreaks: true
typographer: false
underline: false
tabWidth: 2
latexEngine: katex
kroki: true
plantuml: true
multimdTable: true
wysiwyg:
contentType: html
config: {}
groups:
defaultPermissions:
- 'read:pages'
......@@ -109,18 +122,3 @@ groups:
path: ''
locales: []
sites: []
reservedPaths:
- login
- logout
- register
- verify
- favicons
- fonts
- img
- js
- svg
pageExtensions:
- md
- html
- txt
# ---------------------------------
......@@ -570,6 +570,31 @@ exports.up = async knex => {
faviconExt: 'svg',
loginBg: false
},
editors: {
asciidoc: {
isActive: true,
config: {}
},
markdown: {
isActive: true,
config: {
allowHTML: true,
linkify: true,
lineBreaks: true,
typographer: false,
underline: false,
tabWidth: 2,
latexEngine: 'katex',
kroki: true,
plantuml: true,
multimdTable: true
}
},
wysiwyg: {
isActive: true,
config: {}
}
},
theme: {
dark: false,
colorPrimary: '#1976D2',
......
......@@ -69,6 +69,7 @@ type Site {
locale: String
localeNamespaces: [String]
localeNamespacing: Boolean
editors: SiteEditors
theme: SiteTheme
}
......@@ -106,6 +107,17 @@ type SiteLocale {
namespaces: [String]
}
type SiteEditors {
asciidoc: SiteEditor
markdown: SiteEditor
wysiwyg: SiteEditor
}
type SiteEditor {
isActive: Boolean
config: JSON
}
type SiteTheme {
dark: Boolean
colorPrimary: String
......
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 @@
"lint": "eslint --ext .js,.vue ./"
},
"dependencies": {
"@apollo/client": "3.7.1",
"@codemirror/autocomplete": "6.0.2",
"@codemirror/basic-setup": "0.20.0",
"@codemirror/closebrackets": "0.19.2",
"@codemirror/commands": "6.0.1",
"@codemirror/comment": "0.19.1",
"@codemirror/fold": "0.19.4",
"@codemirror/gutter": "0.19.9",
"@codemirror/highlight": "0.19.8",
"@codemirror/history": "0.19.2",
"@codemirror/lang-css": "6.0.0",
"@codemirror/lang-html": "6.1.0",
"@codemirror/lang-javascript": "6.0.1",
"@codemirror/lang-json": "6.0.0",
"@codemirror/lang-markdown": "6.0.0",
"@codemirror/matchbrackets": "0.19.4",
"@codemirror/search": "6.0.0",
"@codemirror/state": "6.0.1",
"@codemirror/tooltip": "0.19.16",
"@codemirror/view": "6.0.2",
"@lezer/common": "1.0.1",
"@quasar/extras": "1.15.5",
"@tiptap/core": "2.0.0-beta.176",
"@tiptap/extension-code-block": "2.0.0-beta.37",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.68",
"@tiptap/extension-color": "2.0.0-beta.9",
"@tiptap/extension-dropcursor": "2.0.0-beta.25",
"@tiptap/extension-font-family": "2.0.0-beta.21",
"@tiptap/extension-gapcursor": "2.0.0-beta.34",
"@tiptap/extension-hard-break": "2.0.0-beta.30",
"@tiptap/extension-highlight": "2.0.0-beta.33",
"@tiptap/extension-history": "2.0.0-beta.21",
"@tiptap/extension-image": "2.0.0-beta.27",
"@tiptap/extension-mention": "2.0.0-beta.97",
"@tiptap/extension-placeholder": "2.0.0-beta.48",
"@tiptap/extension-table": "2.0.0-beta.49",
"@tiptap/extension-table-cell": "2.0.0-beta.20",
"@tiptap/extension-table-header": "2.0.0-beta.22",
"@tiptap/extension-table-row": "2.0.0-beta.19",
"@tiptap/extension-task-item": "2.0.0-beta.32",
"@tiptap/extension-task-list": "2.0.0-beta.26",
"@tiptap/extension-text-align": "2.0.0-beta.29",
"@tiptap/extension-text-style": "2.0.0-beta.23",
"@tiptap/extension-typography": "2.0.0-beta.20",
"@tiptap/starter-kit": "2.0.0-beta.185",
"@tiptap/vue-3": "2.0.0-beta.91",
"@apollo/client": "3.7.7",
"@lezer/common": "1.0.2",
"@quasar/extras": "1.15.10",
"@tiptap/core": "2.0.0-beta.212",
"@tiptap/extension-code-block": "2.0.0-beta.212",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.212",
"@tiptap/extension-color": "2.0.0-beta.212",
"@tiptap/extension-dropcursor": "2.0.0-beta.212",
"@tiptap/extension-font-family": "2.0.0-beta.212",
"@tiptap/extension-gapcursor": "2.0.0-beta.212",
"@tiptap/extension-hard-break": "2.0.0-beta.212",
"@tiptap/extension-highlight": "2.0.0-beta.212",
"@tiptap/extension-history": "2.0.0-beta.212",
"@tiptap/extension-image": "2.0.0-beta.212",
"@tiptap/extension-mention": "2.0.0-beta.212",
"@tiptap/extension-placeholder": "2.0.0-beta.212",
"@tiptap/extension-table": "2.0.0-beta.212",
"@tiptap/extension-table-cell": "2.0.0-beta.212",
"@tiptap/extension-table-header": "2.0.0-beta.212",
"@tiptap/extension-table-row": "2.0.0-beta.212",
"@tiptap/extension-task-item": "2.0.0-beta.212",
"@tiptap/extension-task-list": "2.0.0-beta.212",
"@tiptap/extension-text-align": "2.0.0-beta.212",
"@tiptap/extension-text-style": "2.0.0-beta.212",
"@tiptap/extension-typography": "2.0.0-beta.212",
"@tiptap/pm": "2.0.0-beta.212",
"@tiptap/starter-kit": "2.0.0-beta.212",
"@tiptap/vue-3": "2.0.0-beta.212",
"apollo-upload-client": "17.0.0",
"browser-fs-access": "0.31.1",
"browser-fs-access": "0.31.2",
"clipboard": "2.0.11",
"codemirror": "6.0.1",
"filesize": "10.0.5",
"codemirror": "5.65.11",
"codemirror-asciidoc": "1.0.4",
"filesize": "10.0.6",
"filesize-parser": "1.5.0",
"fuse.js": "6.6.2",
"graphql": "16.6.0",
......@@ -69,39 +52,47 @@
"js-cookie": "3.0.1",
"jwt-decode": "3.1.2",
"lodash-es": "4.17.21",
"luxon": "3.1.0",
"pinia": "2.0.23",
"lowlight": "2.8.1",
"luxon": "3.2.1",
"pinia": "2.0.30",
"prosemirror-commands": "1.5.0",
"prosemirror-history": "1.3.0",
"prosemirror-keymap": "1.2.0",
"prosemirror-model": "1.19.0",
"prosemirror-schema-list": "1.2.2",
"prosemirror-state": "1.4.2",
"prosemirror-transform": "1.7.1",
"prosemirror-view": "1.30.1",
"pug": "3.0.2",
"quasar": "2.10.1",
"quasar": "2.11.5",
"slugify": "1.6.5",
"socket.io-client": "4.5.3",
"socket.io-client": "4.5.4",
"tippy.js": "6.3.7",
"uuid": "9.0.0",
"v-network-graph": "0.6.10",
"vue": "3.2.41",
"vue-codemirror": "6.1.1",
"v-network-graph": "0.8.1",
"vue": "3.2.47",
"vue-i18n": "9.2.2",
"vue-router": "4.1.6",
"vue3-otp-input": "0.3.6",
"vuedraggable": "4.1.0",
"xterm": "5.0.0",
"xterm": "5.1.0",
"zxcvbn": "4.4.2"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "6.0.3",
"@quasar/app-vite": "1.1.3",
"@types/lodash": "4.14.188",
"@volar/vue-language-plugin-pug": "1.0.9",
"@intlify/unplugin-vue-i18n": "0.8.1",
"@quasar/app-vite": "1.2.0",
"@types/lodash": "4.14.191",
"@volar/vue-language-plugin-pug": "1.0.24",
"browserlist": "latest",
"eslint": "8.27.0",
"eslint": "8.33.0",
"eslint-config-standard": "17.0.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-n": "15.5.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "15.6.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.7.0"
"eslint-plugin-vue": "9.9.0"
},
"engines": {
"node": "^18 || ^16",
"node": "^18",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
},
......
......@@ -50,7 +50,8 @@ module.exports = configure(function (/* ctx */) {
extras: [
// 'ionicons-v4',
// 'mdi-v5',
'fontawesome-v6',
'mdi-v7',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
'line-awesome'
......@@ -91,11 +92,17 @@ module.exports = configure(function (/* ctx */) {
// /^\/_site\//
// ]
// }
viteConf.optimizeDeps.include = [
'prosemirror-state',
'prosemirror-transform',
'prosemirror-model',
'prosemirror-view'
]
},
// viteVuePluginOptions: {},
vitePlugins: [
['@intlify/vite-plugin-vue-i18n', {
['@intlify/unplugin-vue-i18n/vite', {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,
......@@ -157,7 +164,7 @@ module.exports = configure(function (/* ctx */) {
}
},
iconSet: 'fontawesome-v6', // Quasar icon set
iconSet: 'mdi-v7', // Quasar icon set
lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
......
<template lang="pug">
.quill-container
.editor-markdown
.editor-markdown-main
.editor-markdown-sidebar X
.editor-markdown-editor
textarea(ref='cmRef')
transition(name='editor-markdown-preview')
.editor-markdown-preview(v-if='state.previewShown')
.editor-markdown-preview-content.contents(ref='editorPreviewContainer')
div(
ref='editorPreview'
v-html='state.previewHTML'
)
</template>
<script>
<script setup>
import { reactive, ref, shallowRef, onBeforeMount, onMounted, watch } from 'vue'
import { useMeta, useQuasar, setCssVar } from 'quasar'
import { useI18n } from 'vue-i18n'
export default {
data () {
return {
import { useEditorStore } from 'src/stores/editor'
// Code Mirror
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
import '../css/codemirror.scss'
// Language
import 'codemirror/mode/markdown/markdown.js'
// Addons
import 'codemirror/addon/selection/active-line.js'
import 'codemirror/addon/display/fullscreen.js'
import 'codemirror/addon/display/fullscreen.css'
import 'codemirror/addon/selection/mark-selection.js'
import 'codemirror/addon/search/searchcursor.js'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
// QUASAR
const $q = useQuasar()
// STORES
const editorStore = useEditorStore()
// I18N
const { t } = useI18n()
// STATE
const cm = shallowRef(null)
const cmRef = ref(null)
const state = reactive({
previewShown: true,
previewHTML: ''
})
// Platform detection
const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
// MOUNTED
onMounted(async () => {
// -> Setup Editor View
editorStore.$patch({
hideSideNav: true
})
// -> Initialize CodeMirror
cm.value = CodeMirror.fromTextArea(cmRef.value, {
tabSize: 2,
mode: 'text/markdown',
theme: 'wikijs-dark',
lineNumbers: true,
lineWrapping: true,
line: true,
styleActiveLine: true,
highlightSelectionMatches: {
annotateScrollbar: true
},
viewportMargin: 50,
inputStyle: 'contenteditable',
allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'],
// direction: siteConfig.rtl ? 'rtl' : 'ltr',
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
})
cm.value.setValue(state.content)
cm.value.on('change', c => {
editorStore.$patch({
content: c.getValue()
})
// onCmInput(editorStore.content)
})
cm.value.setSize(null, 'calc(100vh - 150px)')
// -> Set Keybindings
const keyBindings = {
'F11' (c) {
c.setOption('fullScreen', !c.getOption('fullScreen'))
},
'Esc' (c) {
if (c.getOption('fullScreen')) {
c.setOption('fullScreen', false)
}
},
[`${CtrlKey}-S`] (c) {
// save()
return false
},
[`${CtrlKey}-B`] (c) {
// toggleMarkup({ start: '**' })
return false
},
[`${CtrlKey}-I`] (c) {
// toggleMarkup({ start: '*' })
return false
},
[`${CtrlKey}-Alt-Right`] (c) {
// let lvl = getHeaderLevel(c)
// if (lvl >= 6) { lvl = 5 }
// setHeaderLine(lvl + 1)
return false
},
[`${CtrlKey}-Alt-Left`] (c) {
// let lvl = getHeaderLevel(c)
// if (lvl <= 1) { lvl = 2 }
// setHeaderLine(lvl - 1)
return false
}
}
}
cm.value.setOption('extraKeys', keyBindings)
// this.cm.on('inputRead', this.autocomplete)
// // Handle cursor movement
// this.cm.on('cursorActivity', c => {
// this.positionSync(c)
// this.scrollSync(c)
// })
// // Handle special paste
// this.cm.on('paste', this.onCmPaste)
// // Render initial preview
// this.processContent(this.$store.get('editor/content'))
// this.refresh()
// this.$root.$on('editorInsert', opts => {
// switch (opts.kind) {
// case 'IMAGE':
// let img = `![${opts.text}](${opts.path})`
// if (opts.align && opts.align !== '') {
// img += `{.align-${opts.align}}`
// }
// this.insertAtCursor({
// content: img
// })
// break
// case 'BINARY':
// this.insertAtCursor({
// content: `[${opts.text}](${opts.path})`
// })
// break
// case 'DIAGRAM':
// const selStartLine = this.cm.getCursor('from').line
// const selEndLine = this.cm.getCursor('to').line + 1
// this.cm.doc.replaceSelection('```diagram\n' + opts.text + '\n```\n', 'start')
// this.processMarkers(selStartLine, selEndLine)
// break
// }
// })
// // Handle save conflict
// this.$root.$on('saveConflict', () => {
// this.toggleModal(`editorModalConflict`)
// })
// this.$root.$on('overwriteEditorContent', () => {
// this.cm.setValue(this.$store.get('editor/content'))
// })
})
onBeforeMount(() => {
// if (editor.value) {
// editor.value.destroy()
// }
})
</script>
<style lang="scss">
$editor-height: calc(100vh - 112px - 24px);
$editor-height-mobile: calc(100vh - 112px - 16px);
.editor-markdown {
&-main {
display: flex;
width: 100%;
}
&-editor {
background-color: $dark-6;
flex: 1 1 50%;
display: block;
height: $editor-height;
position: relative;
// @include until($tablet) {
// height: $editor-height-mobile;
// }
}
&-preview {
flex: 1 1 50%;
background-color: $grey-2;
position: relative;
height: $editor-height;
overflow: hidden;
padding: 1rem;
@at-root .theme--dark & {
background-color: $grey-9;
}
// @include until($tablet) {
// display: none;
// }
&-enter-active, &-leave-active {
transition: max-width .5s ease;
max-width: 50vw;
.editor-code-preview-content {
width: 50vw;
overflow:hidden;
}
}
&-enter, &-leave-to {
max-width: 0;
}
&-content {
height: $editor-height;
overflow-y: scroll;
padding: 0;
width: calc(100% + 17px);
// -ms-overflow-style: none;
// &::-webkit-scrollbar {
// width: 0px;
// background: transparent;
// }
// @include until($tablet) {
// height: $editor-height-mobile;
// }
> div {
outline: none;
}
p.line {
overflow-wrap: break-word;
}
.tabset {
background-color: $teal-7;
color: $teal-2 !important;
padding: 5px 12px;
font-size: 14px;
font-weight: 500;
border-radius: 5px 0 0 0;
font-style: italic;
&::after {
display: none;
}
&-header {
background-color: $teal-5;
color: #FFF !important;
padding: 5px 12px;
font-size: 14px;
font-weight: 500;
margin-top: 0 !important;
&::after {
display: none;
}
}
&-content {
border-left: 5px solid $teal-5;
background-color: $teal-1;
padding: 0 15px 15px;
overflow: hidden;
@at-root .theme--dark & {
background-color: rgba($teal-5, .1);
}
}
}
}
}
&-toolbar {
background-color: $blue-7;
background-image: linear-gradient(to bottom, $blue-7 0%, $blue-8 100%);
color: #FFF;
.v-toolbar__content {
padding-left: 64px;
// @include until($tablet) {
// padding-left: 8px;
// }
}
}
&-sidebar {
background-color: $dark-4;
border-right: 1px solid $dark-3;
color: #FFF;
width: 56px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 24px 0;
}
&-sysbar {
padding-left: 0;
&-locale {
background-color: rgba(255,255,255,.25);
display:inline-flex;
padding: 0 12px;
height: 24px;
width: 63px;
justify-content: center;
align-items: center;
}
}
// ==========================================
// CODE MIRROR
// ==========================================
.CodeMirror {
height: auto;
font-family: 'Roboto Mono', monospace;
font-size: .9rem;
.cm-header-1 {
font-size: 1.5rem;
}
.cm-header-2 {
font-size: 1.25rem;
}
.cm-header-3 {
font-size: 1.15rem;
}
.cm-header-4 {
font-size: 1.1rem;
}
.cm-header-5 {
font-size: 1.05rem;
}
.cm-header-6 {
font-size: 1.025rem;
}
}
.CodeMirror-wrap pre.CodeMirror-line, .CodeMirror-wrap pre.CodeMirror-line-like {
word-break: break-word;
}
.CodeMirror-focused .cm-matchhighlight {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
background-position: bottom;
background-repeat: repeat-x;
}
.cm-matchhighlight {
background-color: $grey-8;
}
.CodeMirror-selection-highlight-scrollbar {
background-color: $green-6;
}
}
// HINT DROPDOWN
.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;
margin: 0;
padding: 1px;
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
border: 1px solid $grey-7;
background: $grey-9;
font-family: 'Roboto Mono', monospace;
font-size: .9rem;
max-height: 150px;
overflow-y: auto;
min-width: 250px;
max-width: 80vw;
}
.CodeMirror-hint {
margin: 0;
padding: 0 4px;
white-space: pre;
color: #FFF;
cursor: pointer;
}
li.CodeMirror-hint-active {
background: $blue-5;
color: #FFF;
}
</style>
<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(
v-else-if='menuItem.type === `dropdown`'
:key='menuItem.key'
:key='`ddn-` + menuItem.key'
flat
:icon='menuItem.icon'
padding='xs'
......@@ -27,7 +27,7 @@
q-separator.q-my-sm(v-if='child.type === `divider`')
q-item(
v-else
:key='menuItem.key + `-` + child.key'
:key='child.key'
clickable
@click='child.action'
:active='child.isActive && child.isActive()'
......@@ -43,12 +43,12 @@
q-item-label {{child.title}}
q-btn-group(
v-else-if='menuItem.type === `btngroup`'
:key='menuItem.key'
:key='`btngrp-` + menuItem.key'
flat
)
q-btn(
v-for='child of menuItem.children'
:key='menuItem.key + `-` + child.key'
:key='child.key'
flat
:icon='child.icon'
padding='xs'
......@@ -60,6 +60,7 @@
)
q-btn(
v-else
:key='`btn-` + menuItem.key'
flat
:icon='menuItem.icon'
padding='xs'
......@@ -69,24 +70,24 @@
:aria-label='menuItem.title'
:disabled='menuItem.disabled && menuItem.disabled()'
)
q-space
q-btn(
size='sm'
unelevated
color='red'
label='Test'
@click='snapshot'
)
q-scroll-area(
:thumb-style='thumbStyle'
:bar-style='barStyle'
style='height: 100%;'
)
//- q-space
//- q-btn(
//- size='sm'
//- unelevated
//- color='red'
//- label='Test'
//- @click='snapshot'
//- )
//- q-scroll-area(
//- :thumb-style='thumbStyle'
//- :bar-style='barStyle'
//- style='height: 100%;'
//- )
editor-content(:editor='editor')
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
// import Collaboration from '@tiptap/extension-collaboration'
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 TextStyle from '@tiptap/extension-text-style'
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 { IndexeddbPersistence } from 'y-indexeddb'
// import { WebsocketProvider } from 'y-websocket'
export default {
components: {
EditorContent
},
data () {
return {
editor: null,
ydoc: null,
thumbStyle: {
import { useMeta, useQuasar, setCssVar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { useEditorStore } from 'src/stores/editor'
// QUASAR
const $q = useQuasar()
// STORES
const editorStore = useEditorStore()
// I18N
const { t } = useI18n()
// STATE
const state = reactive({
// editor: null,
ydoc: null
})
let editor = null
const thumbStyle = {
right: '2px',
borderRadius: '5px',
backgroundColor: '#000',
width: '5px',
opacity: 0.15
},
barStyle: {
}
const barStyle = {
backgroundColor: '#FAFAFA',
width: '9px',
opacity: 1
},
menuBar: [
}
const menuBar = [
{
key: 'bold',
icon: 'mdi-format-bold',
title: 'Bold',
action: () => this.editor.chain().focus().toggleBold().run(),
isActive: () => this.editor.isActive('bold')
action: () => editor.value.chain().focus().toggleBold().run(),
isActive: () => editor.value.isActive('bold')
},
{
key: 'italic',
icon: 'mdi-format-italic',
title: 'Italic',
action: () => this.editor.chain().focus().toggleItalic().run(),
isActive: () => this.editor.isActive('italic')
action: () => editor.value.chain().focus().toggleItalic().run(),
isActive: () => editor.value.isActive('italic')
},
{
key: 'strikethrough',
icon: 'mdi-format-strikethrough',
title: 'Strike',
action: () => this.editor.chain().focus().toggleStrike().run(),
isActive: () => this.editor.isActive('strike')
action: () => editor.value.chain().focus().toggleStrike().run(),
isActive: () => editor.value.isActive('strike')
},
{
key: 'code',
icon: 'mdi-code-tags',
title: 'Code',
action: () => this.editor.chain().focus().toggleCode().run(),
isActive: () => this.editor.isActive('code')
action: () => editor.value.chain().focus().toggleCode().run(),
isActive: () => editor.value.isActive('code')
},
{
key: 'fontfamily',
icon: 'mdi-format-font',
title: 'Font Family',
type: 'dropdown',
isActive: () => this.editor.isActive('fontFamily'),
isActive: () => editor.value.isActive('fontFamily'),
children: [
{
key: 'fontunset',
icon: 'mdi-format-font',
title: 'Sans-Serif',
action: () => this.editor.chain().focus().unsetFontFamily().run()
action: () => editor.value.chain().focus().unsetFontFamily().run()
},
{
key: 'monospace',
icon: 'mdi-format-font',
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',
title: 'Text Color',
type: 'dropdown',
isActive: () => this.editor.isActive('color'),
isActive: () => editor.value.isActive('color'),
children: [
{
key: 'blue',
key: 'color-blue',
icon: 'mdi-palette',
title: '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',
title: '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',
title: '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',
title: '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',
title: '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',
title: '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',
title: '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',
title: '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',
title: 'Yellow',
color: 'yellow',
action: () => this.editor.chain().focus().toggleHighlight().run()
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
type: 'divider'
},
{
key: 'remove',
icon: 'mdi-water-off',
key: 'color-remove',
icon: 'mdi-palette',
title: 'Default',
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',
title: 'Highlight',
type: 'dropdown',
isActive: () => this.editor.isActive('highlight'),
isActive: () => editor.value.isActive('highlight'),
children: [
{
key: 'yellow',
key: 'highlight-yellow',
icon: 'mdi-marker',
title: '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',
title: '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',
title: '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',
title: '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',
title: 'Orange',
color: 'orange',
action: () => this.editor.chain().focus().toggleHighlight().run()
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
type: 'divider'
},
{
key: 'remove',
key: 'highlight-remove',
icon: 'mdi-marker-cancel',
title: 'Remove',
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',
title: 'Header',
type: 'dropdown',
isActive: () => this.editor.isActive('heading'),
isActive: () => editor.value.isActive('heading'),
children: [
{
key: 'h1',
icon: 'mdi-format-header-1',
title: 'Header 1',
action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => this.editor.isActive('heading', { level: 1 })
action: () => editor.value.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => editor.value.isActive('heading', { level: 1 })
},
{
key: 'h2',
icon: 'mdi-format-header-2',
title: 'Header 2',
action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => this.editor.isActive('heading', { level: 2 })
action: () => editor.value.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => editor.value.isActive('heading', { level: 2 })
},
{
key: 'h3',
icon: 'mdi-format-header-3',
title: 'Header 3',
action: () => this.editor.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => this.editor.isActive('heading', { level: 3 })
action: () => editor.value.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => editor.value.isActive('heading', { level: 3 })
},
{
key: 'h4',
icon: 'mdi-format-header-4',
title: 'Header 4',
action: () => this.editor.chain().focus().toggleHeading({ level: 4 }).run(),
isActive: () => this.editor.isActive('heading', { level: 4 })
action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(),
isActive: () => editor.value.isActive('heading', { level: 4 })
},
{
key: 'h5',
icon: 'mdi-format-header-5',
title: 'Header 5',
action: () => this.editor.chain().focus().toggleHeading({ level: 5 }).run(),
isActive: () => this.editor.isActive('heading', { level: 5 })
action: () => editor.value.chain().focus().toggleHeading({ level: 5 }).run(),
isActive: () => editor.value.isActive('heading', { level: 5 })
},
{
key: 'h6',
icon: 'mdi-format-header-6',
title: 'Header 6',
action: () => this.editor.chain().focus().toggleHeading({ level: 6 }).run(),
isActive: () => this.editor.isActive('heading', { level: 6 })
action: () => editor.value.chain().focus().toggleHeading({ level: 6 }).run(),
isActive: () => editor.value.isActive('heading', { level: 6 })
}
]
},
......@@ -373,8 +394,8 @@ export default {
key: 'paragraph',
icon: 'mdi-format-paragraph',
title: 'Paragraph',
action: () => this.editor.chain().focus().setParagraph().run(),
isActive: () => this.editor.isActive('paragraph')
action: () => editor.value.chain().focus().setParagraph().run(),
isActive: () => editor.value.isActive('paragraph')
},
{
type: 'divider'
......@@ -384,32 +405,32 @@ export default {
type: 'btngroup',
children: [
{
key: 'left',
key: 'align-left',
icon: 'mdi-format-align-left',
title: 'Left Align',
action: () => this.editor.chain().focus().setTextAlign('left').run(),
isActive: () => this.editor.isActive({ textAlign: 'left' })
action: () => editor.value.chain().focus().setTextAlign('left').run(),
isActive: () => editor.value.isActive({ textAlign: 'left' })
},
{
key: 'center',
key: 'align-center',
icon: 'mdi-format-align-center',
title: 'Center Align',
action: () => this.editor.chain().focus().setTextAlign('center').run(),
isActive: () => this.editor.isActive({ textAlign: 'center' })
action: () => editor.value.chain().focus().setTextAlign('center').run(),
isActive: () => editor.value.isActive({ textAlign: 'center' })
},
{
key: 'right',
key: 'align-right',
icon: 'mdi-format-align-right',
title: 'Right Align',
action: () => this.editor.chain().focus().setTextAlign('right').run(),
isActive: () => this.editor.isActive({ textAlign: 'right' })
action: () => editor.value.chain().focus().setTextAlign('right').run(),
isActive: () => editor.value.isActive({ textAlign: 'right' })
},
{
key: 'justify',
key: 'align-justify',
icon: 'mdi-format-align-justify',
title: 'Justify Align',
action: () => this.editor.chain().focus().setTextAlign('justify').run(),
isActive: () => this.editor.isActive({ textAlign: 'justify' })
action: () => editor.value.chain().focus().setTextAlign('justify').run(),
isActive: () => editor.value.isActive({ textAlign: 'justify' })
}
]
},
......@@ -420,22 +441,22 @@ export default {
key: 'bulletlist',
icon: 'mdi-format-list-bulleted',
title: 'Bullet List',
action: () => this.editor.chain().focus().toggleBulletList().run(),
isActive: () => this.editor.isActive('bulletList')
action: () => editor.value.chain().focus().toggleBulletList().run(),
isActive: () => editor.value.isActive('bulletList')
},
{
key: 'orderedlist',
icon: 'mdi-format-list-numbered',
title: 'Ordered List',
action: () => this.editor.chain().focus().toggleOrderedList().run(),
isActive: () => this.editor.isActive('orderedList')
action: () => editor.value.chain().focus().toggleOrderedList().run(),
isActive: () => editor.value.isActive('orderedList')
},
{
key: 'tasklist',
icon: 'mdi-format-list-checkbox',
icon: 'mdi-format-list-checks',
title: 'Task List',
action: () => this.editor.chain().focus().toggleTaskList().run(),
isActive: () => this.editor.isActive('taskList')
action: () => editor.value.chain().focus().toggleTaskList().run(),
isActive: () => editor.value.isActive('taskList')
},
{
type: 'divider'
......@@ -444,25 +465,25 @@ export default {
key: 'codeblock',
icon: 'mdi-code-json',
title: 'Code Block',
action: () => this.editor.chain().focus().toggleCodeBlock().run(),
isActive: () => this.editor.isActive('codeBlock')
action: () => editor.value.chain().focus().toggleCodeBlock().run(),
isActive: () => editor.value.isActive('codeBlock')
},
{
key: 'blockquote',
icon: 'mdi-format-quote-close',
icon: 'mdi-format-quote-open',
title: 'Blockquote',
action: () => this.editor.chain().focus().toggleBlockquote().run(),
isActive: () => this.editor.isActive('blockquote')
action: () => editor.value.chain().focus().toggleBlockquote().run(),
isActive: () => editor.value.isActive('blockquote')
},
{
key: 'rule',
icon: 'mdi-minus',
title: 'Horizontal Rule',
action: () => this.editor.chain().focus().setHorizontalRule().run()
action: () => editor.value.chain().focus().setHorizontalRule().run()
},
{
key: 'link',
icon: 'mdi-link-plus',
icon: 'mdi-link-variant',
title: 'Link',
action: () => {
// TODO: insert link
......@@ -478,122 +499,122 @@ export default {
},
{
key: 'table',
icon: 'mdi-table-large',
icon: 'mdi-table',
title: 'Table',
type: 'dropdown',
isActive: () => this.editor.isActive('table'),
isActive: () => editor.value.isActive('table'),
children: [
{
key: 'insert',
key: 'table-insert',
icon: 'mdi-table-large-plus',
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'
},
{
key: 'addcolumnbefore',
key: 'table-addcolumnbefore',
icon: 'mdi-table-column-plus-before',
title: 'Add Column Before',
action: () => this.editor.chain().focus().addColumnBefore().run(),
disabled: () => !this.editor.can().addColumnBefore()
action: () => editor.value.chain().focus().addColumnBefore().run(),
disabled: () => !editor.value.can().addColumnBefore()
},
{
key: 'addcolumnafter',
key: 'table-addcolumnafter',
icon: 'mdi-table-column-plus-after',
title: 'Add Column After',
action: () => this.editor.chain().focus().addColumnAfter().run(),
disabled: () => !this.editor.can().addColumnAfter()
action: () => editor.value.chain().focus().addColumnAfter().run(),
disabled: () => !editor.value.can().addColumnAfter()
},
{
key: 'deletecolumn',
key: 'table-deletecolumn',
icon: 'mdi-table-column-remove',
title: 'Remove Column',
action: () => this.editor.chain().focus().deleteColumn().run(),
disabled: () => !this.editor.can().deleteColumn()
action: () => editor.value.chain().focus().deleteColumn().run(),
disabled: () => !editor.value.can().deleteColumn()
},
{
type: 'divider'
},
{
key: 'addrowbefore',
key: 'table-addrowbefore',
icon: 'mdi-table-row-plus-before',
title: 'Add Row Before',
action: () => this.editor.chain().focus().addRowBefore().run(),
disabled: () => !this.editor.can().addRowBefore()
action: () => editor.value.chain().focus().addRowBefore().run(),
disabled: () => !editor.value.can().addRowBefore()
},
{
key: 'addrowafter',
key: 'table-addrowafter',
icon: 'mdi-table-row-plus-after',
title: 'Add Row After',
action: () => this.editor.chain().focus().addRowAfter().run(),
disabled: () => !this.editor.can().addRowAfter()
action: () => editor.value.chain().focus().addRowAfter().run(),
disabled: () => !editor.value.can().addRowAfter()
},
{
key: 'deleterow',
key: 'table-deleterow',
icon: 'mdi-table-row-remove',
title: 'Remove Row',
action: () => this.editor.chain().focus().deleteRow().run(),
disabled: () => !this.editor.can().deleteRow()
action: () => editor.value.chain().focus().deleteRow().run(),
disabled: () => !editor.value.can().deleteRow()
},
{
type: 'divider'
},
{
key: 'merge',
key: 'table-merge',
icon: 'mdi-table-merge-cells',
title: 'Merge Cells',
action: () => this.editor.chain().focus().mergeCells().run(),
disabled: () => !this.editor.can().mergeCells()
action: () => editor.value.chain().focus().mergeCells().run(),
disabled: () => !editor.value.can().mergeCells()
},
{
key: 'split',
key: 'table-split',
icon: 'mdi-table-split-cell',
title: 'Split Cell',
action: () => this.editor.chain().focus().splitCell().run(),
disabled: () => !this.editor.can().splitCell()
action: () => editor.value.chain().focus().splitCell().run(),
disabled: () => !editor.value.can().splitCell()
},
{
type: 'divider'
},
{
key: 'toggleHeaderColumn',
key: 'table-toggleHeaderColumn',
icon: 'mdi-table-column',
title: 'Toggle Header Column',
action: () => this.editor.chain().focus().toggleHeaderColumn().run(),
disabled: () => !this.editor.can().toggleHeaderColumn()
action: () => editor.value.chain().focus().toggleHeaderColumn().run(),
disabled: () => !editor.value.can().toggleHeaderColumn()
},
{
key: 'toggleHeaderRow',
key: 'table-toggleHeaderRow',
icon: 'mdi-table-row',
title: 'Toggle Header Row',
action: () => this.editor.chain().focus().toggleHeaderRow().run(),
disabled: () => !this.editor.can().toggleHeaderRow()
action: () => editor.value.chain().focus().toggleHeaderRow().run(),
disabled: () => !editor.value.can().toggleHeaderRow()
},
{
key: 'toggleHeaderCell',
key: 'table-toggleHeaderCell',
icon: 'mdi-crop-square',
title: 'Toggle Header Cell',
action: () => this.editor.chain().focus().toggleHeaderCell().run(),
disabled: () => !this.editor.can().toggleHeaderCell()
action: () => editor.value.chain().focus().toggleHeaderCell().run(),
disabled: () => !editor.value.can().toggleHeaderCell()
},
{
type: 'divider'
},
{
key: 'fix',
key: 'table-fix',
icon: 'mdi-table-heart',
title: 'Fix Table',
action: () => this.editor.chain().focus().fixTables().run(),
disabled: () => !this.editor.can().fixTables()
action: () => editor.value.chain().focus().fixTables().run(),
disabled: () => !editor.value.can().fixTables()
},
{
key: 'remove',
key: 'table-remove',
icon: 'mdi-table-large-remove',
title: 'Delete Table',
action: () => this.editor.chain().focus().deleteTable().run(),
disabled: () => !this.editor.can().deleteTable()
action: () => editor.value.chain().focus().deleteTable().run(),
disabled: () => !editor.value.can().deleteTable()
}
]
},
......@@ -604,13 +625,13 @@ export default {
key: 'pagebreak',
icon: 'mdi-format-page-break',
title: 'Hard Break',
action: () => this.editor.chain().focus().setHardBreak().run()
action: () => editor.value.chain().focus().setHardBreak().run()
},
{
key: 'clearformat',
icon: 'mdi-format-clear',
title: 'Clear Format',
action: () => this.editor.chain()
action: () => editor.value.chain()
.focus()
.clearNodes()
.unsetAllMarks()
......@@ -623,30 +644,27 @@ export default {
key: 'undo',
icon: 'mdi-undo-variant',
title: 'Undo',
action: () => this.editor.chain().focus().undo().run(),
disabled: () => !this.editor.can().undo()
action: () => editor.value.chain().focus().undo().run(),
disabled: () => !editor.value.can().undo()
},
{
key: 'redo',
icon: 'mdi-redo-variant',
title: 'Redo',
action: () => this.editor.chain().focus().redo().run(),
disabled: () => !this.editor.can().redo()
}
]
}
},
mounted () {
if (!import.meta.env.SSR) {
this.init()
action: () => editor.value.chain().focus().redo().run(),
disabled: () => !editor.value.can().redo()
}
},
beforeUnmount () {
this.editor.destroy()
},
methods: {
init () {
console.info('BOOP')
]
// METHODS
function init () {
// -> Setup Editor View
editorStore.$patch({
hideSideNav: false
})
// -> Init Live Collab
// this.ydoc = new Y.Doc()
/* 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)
/* eslint-enable no-unused-vars */
this.editor = new Editor({
content: this.$store.get('page/render'),
// -> Initialize TipTap
editor = useEditor({
content: '<p>I’m running Tiptap with Vue.js. 🎉</p>', // editorStore.content,
extensions: [
StarterKit.configure({
codeBlock: false,
......@@ -663,7 +682,9 @@ export default {
depth: 500
}
}),
CodeBlockLowlight,
CodeBlockLowlight.configure({
lowlight
}),
Color,
// Collaboration.configure({
// document: this.ydoc
......@@ -692,18 +713,29 @@ export default {
Typography
],
onUpdate: () => {
this.$store.set('page/render', this.editor.getHTML())
// this.$store.set('page/render', editor.getHTML())
}
})
},
insertTable () {
}
function insertTable () {
// this.ql.getModule('table').insertTable(3, 3)
},
snapshot () {
}
function snapshot () {
// console.info(Y.encodeStateVector(this.ydoc))
}
}
}
// MOUNTED
onMounted(() => {
// init()
})
onBeforeUnmount(() => {
editor.value.destroy()
})
init()
</script>
<style lang="scss">
......
......@@ -81,8 +81,8 @@ const { t } = useI18n()
// METHODS
function create (editor) {
window.location.assign('/_edit/new')
// pageStore.pageCreate({ editor })
// window.location.assign('/_edit/new')
pageStore.pageCreate({ editor })
}
function openFileManager () {
......
<template lang="pug">
.util-code-editor(
ref='editorRef'
)
.util-code-editor
textarea(ref='cmRef')
</template>
<script setup>
/* eslint no-unused-vars: "off" */
import { keymap, EditorView, lineNumbers } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { ref, shallowRef, onBeforeMount, onMounted, watch } from 'vue'
// Code Mirror
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// Language
import 'codemirror/mode/markdown/markdown.js'
import 'codemirror/mode/htmlmixed/htmlmixed.js'
import 'codemirror/mode/css/css.js'
// Addons
import 'codemirror/addon/selection/active-line.js'
// PROPS
const props = defineProps({
......@@ -38,72 +44,77 @@ const emit = defineEmits([
// STATE
const editor = shallowRef(null)
const editorRef = ref(null)
const cm = shallowRef(null)
const cmRef = ref(null)
// WATCHERS
watch(() => props.modelValue, (newVal) => {
// Ignore loopback changes while editing
if (!editor.value.hasFocus) {
editor.value.dispatch({
changes: { from: 0, to: editor.value.state.length, insert: newVal }
})
if (!cm.value.hasFocus()) {
cm.value.setValue(newVal)
}
})
// MOUNTED
onMounted(async () => {
let langModule = null
let langMode = null
switch (props.language) {
case 'css': {
langModule = (await import('@codemirror/lang-css')).css
langMode = 'text/css'
break
}
case 'html': {
langModule = (await import('@codemirror/lang-html')).html
langMode = 'text/html'
break
}
case 'javascript': {
langModule = (await import('@codemirror/lang-javascript')).javascript
langMode = 'text/javascript'
break
}
case 'json': {
langModule = (await import('@codemirror/lang-json')).json
langMode = {
name: 'javascript',
json: true
}
break
}
case 'markdown': {
langModule = (await import('@codemirror/lang-markdown')).markdown
langMode = 'text/markdown'
break
}
default: {
langMode = null
break
}
editor.value = new EditorView({
state: EditorState.create({
doc: props.modelValue,
extensions: [
history(),
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
lineNumbers(),
EditorView.theme({
'.cm-content, .cm-gutter': { minHeight: `${props.minHeight}px` }
}),
...langModule && [langModule()],
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of(v => {
if (v.docChanged) {
emit('update:modelValue', v.state.doc.toString())
}
// -> 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'
})
]
}),
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(() => {
if (editor.value) {
editor.value.destroy()
if (cm.value) {
cm.value.destroy()
}
})
</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 @@
"admin.dev.voyager.title": "Voyager",
"admin.editors.apiDescription": "Document your REST / GraphQL APIs.",
"admin.editors.apiName": "API Docs Editor",
"admin.editors.asciidocDescription": "Use the AsciiDoc syntax to write content. Includes real-time preview.",
"admin.editors.asciidocName": "AsciiDoc Editor",
"admin.editors.blogDescription": "Write a series of posts over time.",
"admin.editors.blogName": "Blog Editor",
"admin.editors.channelDescription": "Create discussion channels to collaborate in real-time with your team.",
......@@ -1675,5 +1677,6 @@
"welcome.admin": "Administration Area",
"welcome.createHome": "Create the homepage",
"welcome.subtitle": "Let's get started...",
"welcome.title": "Welcome to Wiki.js!"
"welcome.title": "Welcome to Wiki.js!",
"admin.editors.useRenderingPipeline": "Uses the rendering pipeline."
}
......@@ -87,7 +87,7 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rfid-tag.svg')
q-item-section {{ t('admin.blocks.title') }}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white', disabled)
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-cashbook.svg')
q-item-section {{ t('admin.editors.title') }}
......
......@@ -2,7 +2,7 @@
q-layout(view='hHh Lpr lff')
header-nav
q-drawer.bg-sidebar(
v-model='siteStore.showSideNav'
:modelValue='isSidebarShown'
show-if-above
:width='255'
)
......@@ -83,11 +83,12 @@ q-layout(view='hHh Lpr lff')
<script setup>
import { useMeta, useQuasar } from 'quasar'
import { onMounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useSiteStore } from '../stores/site'
import { useEditorStore } from 'src/stores/editor'
import { useSiteStore } from 'src/stores/site'
// COMPONENTS
......@@ -101,6 +102,7 @@ const $q = useQuasar()
// STORES
const editorStore = useEditorStore()
const siteStore = useSiteStore()
// ROUTER
......@@ -136,6 +138,12 @@ const barStyle = {
opacity: 0.1
}
// COMPUTED
const isSidebarShown = computed(() => {
return siteStore.showSideNav && !(editorStore.isActive && editorStore.hideSideNav)
})
// METHODS
function openFileManager () {
......
......@@ -17,7 +17,7 @@ q-page.admin-mail
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -19,28 +19,31 @@ q-page.admin-flags
icon='las la-redo-alt'
flat
color='secondary'
:loading='loading > 0'
@click='load'
:loading='state.loading > 0'
@click='refresh'
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
:disabled='loading > 0'
:disabled='state.loading > 0'
)
q-separator(inset)
.q-pa-md.q-gutter-md
q-card.shadow-1
q-list(separator)
q-item(v-for='editor of editors', :key='editor.id')
template(v-for='editor of editors', :key='editor.id')
q-item(v-if='flagsStore.experimental || !editor.isDisabled')
blueprint-icon(:icon='editor.icon')
q-item-section
q-item-label: strong {{t(`admin.editors.` + editor.id + `Name`)}}
q-item-label.flex.items-center(caption)
q-item-label(caption)
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(
side
)
......@@ -51,11 +54,12 @@ q-page.admin-flags
outline
no-caps
padding='xs md'
@click='openConfig(editor.id)'
)
q-separator.q-ml-md(vertical)
q-item-section(side)
q-toggle.q-pr-sm(
v-model='editor.isActive'
v-model='state.config[editor.id]'
:color='editor.isDisabled ? `grey` : `primary`'
checked-icon='las la-check'
unchecked-icon='las la-times'
......@@ -66,14 +70,24 @@ q-page.admin-flags
</template>
<script setup>
import { useMeta } from 'quasar'
import { useMeta, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
import { defineAsyncComponent, onMounted, reactive, watch } from 'vue'
import gql from 'graphql-tag'
import { cloneDeep } from 'lodash-es'
import { useAdminStore } from 'src/stores/admin'
import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const flagsStore = useFlagsStore()
const siteStore = useSiteStore()
// I18N
......@@ -86,46 +100,146 @@ useMeta({
title: t('admin.editors.title')
})
const loading = ref(false)
const state = reactive({
loading: 0,
config: {
api: false,
asciidoc: false,
blog: false,
channel: false,
markdown: false,
redirect: true,
wysiwyg: false
}
})
const editors = reactive([
{
id: 'wysiwyg',
icon: 'google-presentation',
isActive: true
},
{
id: 'markdown',
icon: 'markdown',
config: {},
isActive: true
id: 'api',
icon: 'api',
isDisabled: true,
useRendering: false
},
{
id: 'channel',
icon: 'chat',
isActive: true
id: 'asciidoc',
icon: 'asciidoc',
hasConfig: true,
useRendering: true
},
{
id: 'blog',
icon: 'typewriter-with-paper',
isActive: true,
isDisabled: true
isDisabled: true,
useRendering: true
},
{
id: 'api',
icon: 'api',
isActive: true,
isDisabled: true
id: 'channel',
icon: 'chat',
isDisabled: true,
useRendering: false
},
{
id: 'markdown',
icon: 'markdown',
hasConfig: true,
useRendering: true
},
{
id: 'redirect',
icon: 'advance',
isActive: true
isDisabled: true,
useRendering: false
},
{
id: 'wysiwyg',
icon: 'google-presentation',
useRendering: true
}
])
const load = async () => {}
const save = () => {}
const refresh = () => {}
// WATCHERS
watch(() => adminStore.currentSiteId, (newValue) => {
$q.loading.show()
load()
})
// METHODS
async function load () {
state.loading++
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query getEditorsState (
$siteId: UUID!
) {
siteById (
id: $siteId
) {
id
editors {
asciidoc {
isActive
}
markdown {
isActive
}
wysiwyg {
isActive
}
}
}
}`,
variables: {
siteId: adminStore.currentSiteId
},
fetchPolicy: 'network-only'
})
const data = cloneDeep(resp?.data?.siteById?.editors)
state.config.asciidoc = data?.asciidoc?.isActive ?? false
state.config.markdown = data?.markdown?.isActive ?? false
state.config.wysiwyg = data?.wysiwyg?.isActive ?? false
} catch (err) {
$q.notify({
type: 'negative',
message: 'Failed to fetch editors state.'
})
}
$q.loading.hide()
state.loading--
}
async function save () {}
async function refresh () {
await load()
}
function openConfig (editorId) {
switch (editorId) {
case 'markdown': {
$q.dialog({
component: defineAsyncComponent(() => import('../components/EditorMarkdownConfigDialog.vue'))
})
break
}
default: {
$q.notify({
type: 'negative',
message: 'Invalid Editor Config Call'
})
}
}
}
// MOUNTED
onMounted(async () => {
$q.loading.show()
if (adminStore.currentSiteId) {
await load()
}
})
</script>
<style lang='scss'>
......
......@@ -24,7 +24,7 @@ q-page.admin-flags
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -24,7 +24,7 @@ q-page.admin-general
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -24,7 +24,7 @@ q-page.admin-icons
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......@@ -145,7 +145,6 @@ const packs = [
key: 'fa',
label: 'Font Awesome',
website: 'https://fontawesome.com',
isMandatory: true,
config: {}
},
{
......@@ -163,7 +162,7 @@ const packs = [
key: 'mdi',
label: 'Material Design Icons',
website: 'https://materialdesignicons.com',
config: {}
isMandatory: true
},
{
key: 'thm',
......
......@@ -33,7 +33,7 @@ q-page.admin-locale
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -24,7 +24,7 @@ q-page.admin-login
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -24,7 +24,7 @@ q-page.admin-mail
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -24,7 +24,7 @@ q-page.admin-navigation
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -24,7 +24,7 @@ q-page.admin-mail
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -36,7 +36,7 @@ q-page.admin-storage
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
......@@ -25,7 +25,7 @@ q-page.admin-system
q-btn.acrylic-btn(
ref='copySysInfoBtn'
flat
icon='fa-regular fa-clipboard'
icon='mdi-clipboard-text-outline'
label='Copy System Info'
color='primary'
@click=''
......
......@@ -24,7 +24,7 @@ q-page.admin-theme
)
q-btn(
unelevated
icon='fa-solid fa-check'
icon='mdi-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
......
<template lang='pug'>
q-page.column
.page-breadcrumbs.q-py-sm.q-px-md.row
.page-breadcrumbs.q-py-sm.q-px-md.row(
v-if='!editorStore.isActive'
)
.col
q-breadcrumbs(
active-color='grey-7'
......@@ -36,7 +38,7 @@ q-page.column
//- PAGE HEADER
.col.q-pa-md
.text-h4.page-header-title {{pageStore.title}}
.text-subtitle2.page-header-subtitle {{pageStore.description}}
.text-subtitle2.page-header-subtitle {{pageStore.description }}
//- PAGE ACTIONS
.col-auto.q-pa-md.flex.items-center.justify-end
......@@ -100,11 +102,16 @@ q-page.column
label='Edit'
aria-label='Edit'
no-caps
:href='editUrl'
@click='editPage'
)
.page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
.col(style='order: 1;')
q-no-ssr(
v-if='editorStore.isActive'
)
component(:is='editorComponents[editorStore.editor]')
q-scroll-area(
v-else
:thumb-style='thumbStyle'
:bar-style='barStyle'
style='height: 100%;'
......@@ -344,6 +351,17 @@ const sideDialogs = {
})
}
const editorComponents = {
markdown: defineAsyncComponent({
loader: () => import('../components/EditorMarkdown.vue'),
loadingComponent: LoadingGeneric
}),
wysiwyg: defineAsyncComponent({
loader: () => import('../components/EditorWysiwyg.vue'),
loadingComponent: LoadingGeneric
})
}
// QUASAR
const $q = useQuasar()
......@@ -399,10 +417,7 @@ const barStyle = {
// COMPUTED
const showSidebar = computed(() => {
return pageStore.showSidebar && siteStore.showSidebar
})
const editorComponent = computed(() => {
return pageStore.editor ? `editor-${pageStore.editor}` : null
return pageStore.showSidebar && siteStore.showSidebar && !editorStore.isActive
})
const relationsLeft = computed(() => {
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'left') : []
......@@ -564,6 +579,13 @@ async function saveChanges () {
}
$q.loading.hide()
}
function editPage () {
editorStore.$patch({
isActive: true,
editor: 'markdown'
})
}
</script>
<style lang="scss">
......@@ -578,6 +600,8 @@ async function saveChanges () {
}
}
.page-header {
min-height: 95px;
@at-root .body--light & {
background: linear-gradient(to bottom, $grey-2 0%, $grey-1 100%);
border-bottom: 1px solid $grey-4;
......
......@@ -2,11 +2,13 @@ import { defineStore } from 'pinia'
export const useEditorStore = defineStore('editor', {
state: () => ({
isActive: false,
editor: '',
content: '',
mode: 'create',
activeModal: '',
activeModalData: null,
hideSideNav: false,
media: {
folderTree: [],
currentFolderId: 0,
......
......@@ -174,9 +174,7 @@ export const usePageStore = defineStore('page', {
* PAGE - CREATE
*/
pageCreate ({ editor, locale, path }) {
// -> Editor View
this.editor = editor
this.editorMode = 'create'
const editorStore = useEditorStore()
// if (['markdown', 'api'].includes(editor)) {
// commit('site/SET_SHOW_SIDE_NAV', false, { root: true })
......@@ -204,13 +202,19 @@ export const usePageStore = defineStore('page', {
this.isPublished = false
this.relations = []
this.tags = []
this.breadcrumbs = []
this.content = ''
this.render = ''
// -> View Mode
this.mode = 'edit'
// -> Editor Mode
editorStore.$patch({
isActive: true,
editor,
mode: 'create'
})
},
/**
* PAGE SAVE
......
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