mirror of
https://github.com/LibreSign/libresign.git
synced 2025-12-17 21:12:16 +01:00
feat: implement ckeditor
Signed-off-by: Vitor Mattos <vitor@php.rio>
This commit is contained in:
parent
6dd6a862cc
commit
fae5dbfa96
6 changed files with 3528 additions and 201 deletions
3350
package-lock.json
generated
3350
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
262
src/Components/TextEditor/TextEditor.vue
Normal file
262
src/Components/TextEditor/TextEditor.vue
Normal 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>
|
||||
42
src/ckeditor/InsertVariablePlugin.js
Normal file
42
src/ckeditor/InsertVariablePlugin.js
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue