feat: implement ckeditor

Signed-off-by: Vitor Mattos <vitor@php.rio>
This commit is contained in:
Vitor Mattos 2025-03-31 10:18:56 -03:00
parent 6dd6a862cc
commit fae5dbfa96
No known key found for this signature in database
GPG key ID: B7AB4B76A7CA7318
6 changed files with 3528 additions and 201 deletions

3350
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,16 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"@ckeditor/ckeditor5-build-decoupled-document": "^44.3.0",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
"@ckeditor/ckeditor5-editor-decoupled": "^44.3.0",
"@ckeditor/ckeditor5-essentials": "^44.3.0",
"@ckeditor/ckeditor5-image": "^44.3.0",
"@ckeditor/ckeditor5-mention": "^44.3.0",
"@ckeditor/ckeditor5-paragraph": "^44.3.0",
"@ckeditor/ckeditor5-theme-lark": "^44.3.0",
"@ckeditor/ckeditor5-ui": "^44.3.0",
"@ckeditor/ckeditor5-vue2": "^3.0.1",
"@fontsource/dancing-script": "^5.2.5",
"@libresign/vue-pdf-editor": "^1.4.5",
"@marionebl/option": "^1.0.8",
@ -46,6 +56,9 @@
"debounce": "^2.2.0",
"js-confetti": "^0.12.0",
"pinia": "^2.3.1",
"postcss": "^8.5.3",
"postcss-loader": "^8.1.1",
"raw-loader": "^4.0.2",
"v-perfect-signature": "^1.4.0",
"vue": "^2.7.16",
"vue-advanced-cropper": "^1.11.7",
@ -72,6 +85,7 @@
"babel-loader-exclude-node-modules-except": "^1.2.1",
"esbuild-loader": "^4.3.0",
"openapi-typescript": "^7.6.1",
"svg-inline-loader": "^0.8.2",
"vue-template-compiler": "^2.7.16"
}
}

View file

@ -0,0 +1,262 @@
<template>
<div>
<div ref="containerToolbar" class="toolbar" />
<ckeditor v-if="ready"
:value="value"
:config="config"
:editor="editor"
class="editor"
@input="onEditorInput"
@ready="onEditorReady" />
</div>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import CKEditor from '@ckeditor/ckeditor5-vue2'
import Editor from '@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor.js'
import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials.js'
import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph.js'
import InsertVariablePlugin from '../../ckeditor/InsertVariablePlugin'
import { DropdownView } from '@ckeditor/ckeditor5-ui'
import { getLanguage } from '@nextcloud/l10n'
import logger from '../../logger.js'
export default {
name: 'TextEditor',
components: {
ckeditor: CKEditor.component,
},
props: {
value: {
type: String,
required: true,
},
},
data() {
return {
ready: false,
editor: Editor,
config: {
licenseKey: 'GPL',
autoParagraph: false,
plugins: [
EssentialsPlugin,
ParagraphPlugin,
InsertVariablePlugin,
],
toolbar: {
items: ['undo', 'redo', 'insertVariable'],
},
variaveis: loadState('libresign', 'signature_available_variables'),
language: 'en',
},
}
},
watch: {
value(val) {
console.log('Value mudou:', val)
}
},
beforeMount() {
this.loadEditorTranslations(getLanguage())
},
mounted() {
console.log('Editor carregado:', this.editor);
},
methods: {
overrideDropdownPositionsToNorth(editor, toolbarView) {
const {
south, north, southEast, southWest, northEast, northWest,
southMiddleEast, southMiddleWest, northMiddleEast, northMiddleWest,
} = DropdownView.defaultPanelPositions
let panelPositions
if (editor.locale.uiLanguageDirection !== 'rtl') {
panelPositions = [
northEast, northWest, northMiddleEast, northMiddleWest, north,
southEast, southWest, southMiddleEast, southMiddleWest, south,
]
} else {
panelPositions = [
northWest, northEast, northMiddleWest, northMiddleEast, north,
southWest, southEast, southMiddleWest, southMiddleEast, south,
]
}
for (const item of toolbarView.items) {
if (!(item instanceof DropdownView)) {
continue
}
item.on('change:isOpen', () => {
if (!item.isOpen) {
return
}
item.panelView.position = DropdownView._getOptimalPosition({
element: item.panelView.element,
target: item.buttonView.element,
fitInViewport: true,
positions: panelPositions,
}).name
})
}
},
overrideTooltipPositions(toolbarView) {
for (const item of toolbarView.items) {
if (item.buttonView) {
item.buttonView.tooltipPosition = 'n'
} else if (item.tooltipPosition) {
item.tooltipPosition = 'n'
}
}
},
async loadEditorTranslations(language) {
if (language === 'en') {
// The default, nothing to fetch
return this.showEditor('en')
}
try {
logger.debug(`loading ${language} translations for CKEditor`)
await import(
/* webpackMode: "lazy-once" */
/* webpackPrefetch: true */
/* webpackPreload: true */
`@ckeditor/ckeditor5-build-decoupled-document/build/translations/${language}`
)
this.showEditor(language)
} catch (error) {
logger.error(`could not find CKEditor translations for "${language}"`, { error })
this.showEditor('en')
}
},
showEditor(language) {
logger.debug(`using "${language}" as CKEditor language`)
this.config.language = language
this.ready = true
},
/**
* @param {module:core/editor/editor~Editor} editor editor the editor instance
*/
onEditorReady(editor) {
logger.debug('TextEditor is ready', { editor })
// https://ckeditor.com/docs/ckeditor5/latest/examples/builds-custom/bottom-toolbar-editor.html
if (editor.ui) {
this.$refs.containerToolbar.appendChild(editor.ui.view.toolbar.element)
this.overrideDropdownPositionsToNorth(editor, editor.ui.view.toolbar)
this.overrideTooltipPositions(editor.ui.view.toolbar)
}
this.editorInstance = editor
editor.setData(this.value.replace(/\n/g, '<br />'))
this.$emit('ready', editor)
},
onEditorInput(text) {
if (text !== this.value) {
logger.debug(`TextEditor input changed to <${text}>`)
this.$emit('input', text)
}
},
},
}
</script>
<style lang="scss" scoped>
.editor {
width: 100%;
min-height: 150px;
height: calc(100% - 75px);
overflow: scroll;
margin-bottom: 10px;
&.ck {
border: none !important;
box-shadow: none !important;
padding: 0;
}
}
:deep(a) {
color: #07d;
}
:deep(p) {
cursor: text;
margin: 0 !important;
}
</style>
<style>
/*
Overwrite the default z-index for CKEditor
https://github.com/ckeditor/ckeditor5/issues/1142
*/
.ck .ck-reset {
background: var(--color-main-background) !important;
}
/* Default ckeditor value of padding-inline-start, to overwrite the global styling from server */
.ck-content ul, .ck-content ol {
padding-inline-start: 40px;
}
.ck-list__item {
.ck-off {
background:var(--color-main-background) !important;
}
.ck-on {
background:var(--color-primary-element-light) !important;
}
}
.custom-item-username {
color: var(--color-main-text) !important;
}
.link-title{
color: var(--color-main-text) !important;
margin-left: var(--default-grid-baseline) !important;
}
.link-icon {
width: 16px !important;
}
.custom-item {
width : 100% !important;
border-radius : 8px !important;
padding : 4px 8px !important;
display :block;
background:var(--color-main-background)!important;
}
.custom-item:hover {
background:var(--color-primary-element-light)!important;
}
.link-container{
border-radius :8px !important;
padding :4px 8px !important;
display : block;
width : 100% !important;
background:var(--color-main-background)!important;
}
.link-container:hover {
background:var(--color-primary-element-light)!important;
}
:root {
--ck-z-default: 10000;
--ck-balloon-border-width: 0;
}
.ck.ck-toolbar.ck-rounded-corners {
border-radius: var(--border-radius-large) !important;
}
.ck-rounded-corners .ck.ck-dropdown__panel, .ck.ck-dropdown__panel.ck-rounded-corners {
border-radius: var(--border-radius-large) !important;
overflow: hidden;
}
.ck.ck-button {
border-radius: var(--border-radius-element) !important;
}
.ck-powered-by-balloon {
display: none !important;
}
</style>

View file

@ -0,0 +1,42 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'
import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import Model from '@ckeditor/ckeditor5-ui/src/model'
export default class InsertVariablePlugin extends Plugin {
init() {
const editor = this.editor;
editor.ui.componentFactory.add('insertVariable', locale => {
const dropdownView = createDropdown(locale);
dropdownView.buttonView.set({
label: 'Insert Variable',
tooltip: true,
withText: true,
});
const items = new Collection();
const buttonModel = new Model({
withText: true,
label: 'Foo',
});
buttonModel.on('execute', () => {
console.log('Bar executed')
editor.model.change(writer => {
const position = editor.model.document.selection.getFirstPosition();
writer.insertText('{{bar}}', position);
});
});
items.add({
type: 'button',
model: buttonModel,
});
addListToDropdown(dropdownView, items);
return dropdownView;
});
}
}

View file

@ -15,7 +15,9 @@
</ul>
<div class="content">
<div class="content__row">
<NcTextArea ref="textareaEditor"
<TextEditor id="template"
v-model="inputValue" />
<!-- <NcTextArea ref="textareaEditor"
:value.sync="inputValue"
:label="t('libresign', 'Signature text template')"
:placeholder="t('libresign', 'Signature text template')"
@ -25,7 +27,7 @@
@keydown.enter="save"
@blur="save"
@mousemove="resizeHeight"
@keypress="resizeHeight" />
@keypress="resizeHeight" /> -->
<NcButton v-if="showResetTemplate"
type="tertiary"
:aria-label="t('libresign', 'Reset to default')"
@ -68,6 +70,7 @@
</NcSettingsSection>
</template>
<script>
import TextEditor from '../../Components/TextEditor/TextEditor.vue'
import debounce from 'debounce'
import Undo from 'vue-material-design-icons/UndoVariant.vue'
@ -86,6 +89,7 @@ import NcTextField from '@nextcloud/vue/components/NcTextField'
export default {
name: 'SignatureTextTemplate',
components: {
TextEditor,
NcButton,
NcNoteCard,
NcSettingsSection,
@ -97,7 +101,7 @@ export default {
return {
name: t('libresign', 'Signature text template'),
description: t('libresign', 'This template will be mixed to signature.'),
defaultSignatureTextTemplate: loadState('libresign', 'default_signature_text_template'),
defaultSignatureTextTemplate: '<p>' + loadState('libresign', 'default_signature_text_template').replace(/\n/g, '<br>') + '</p>',
defaultSignatureFontSize: loadState('libresign', 'default_signature_font_size'),
signatureTextTemplate: loadState('libresign', 'signature_text_template'),
fontSize: loadState('libresign', 'signature_font_size'),
@ -182,6 +186,18 @@ export default {
display: flex;
gap: 0 4px;
}
#template {
width: 100%;
min-height: 100px;
border: 1px solid var(--color-border);
&:active,
&:focus,
&:hover {
border-color: var(--color-primary-element) !important;
}
}
}
.text-pre-line {
white-space: pre-line;

View file

@ -4,11 +4,20 @@
*/
const { merge } = require('webpack-merge')
const path = require('path')
const CKEditorWebpackPlugin = require('@ckeditor/ckeditor5-dev-webpack-plugin')
const { styles } = require('@ckeditor/ckeditor5-dev-utils')
const BabelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-modules-except')
const { EsbuildPlugin } = require('esbuild-loader')
const nextcloudWebpackConfig = require('@nextcloud/webpack-vue-config')
const CopyPlugin = require('copy-webpack-plugin');
function getPostCssConfig(ckEditorOpts) {
// CKEditor is not compatbile with postcss@8 and postcss-loader@4 despite stating so.
// Adapted from https://github.com/ckeditor/ckeditor5/issues/8112#issuecomment-960579351
const { plugins, ...rest } = styles.getPostCssConfig(ckEditorOpts);
return { postcssOptions: { plugins }, ...rest };
};
module.exports = merge(nextcloudWebpackConfig, {
entry: {
init: path.resolve(path.join('src', 'init.js')),
@ -49,12 +58,14 @@ module.exports = merge(nextcloudWebpackConfig, {
target: 'es2020',
},
exclude: BabelLoaderExcludeNodeModulesExcept([
'@ckeditor',
'@nextcloud/event-bus',
]),
},
{
test: /\.(ttf|otf|eot|woff|woff2)$/,
type: 'asset/inline',
exclude: /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/,
},
// Load raw SVGs to be able to inject them via v-html
{
@ -69,10 +80,38 @@ module.exports = merge(nextcloudWebpackConfig, {
test: /pdf\.worker(\.min)?\.mjs$/,
type: 'asset/resource'
},
{
test: /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/,
type: 'asset/source'
},
{
test: /\.(svg)$/i,
use: [
{
loader: 'svg-inline-loader',
},
],
exclude: path.join(__dirname, 'node_modules', '@ckeditor'),
},
{
test: /ckeditor5-[^/\\]+[/\\].+\.css$/,
loader: 'postcss-loader',
options: getPostCssConfig({
themeImporter: {
themePath: require.resolve('@ckeditor/ckeditor5-theme-lark'),
},
minify: true,
}),
},
],
},
cache: true,
plugins: [
// CKEditor needs its own plugin to be built using webpack.
new CKEditorWebpackPlugin({
// See https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html
language: 'en',
}),
new CopyPlugin({
patterns: [
{