diff --git a/packages/neos-ui-ckeditor5-bindings/src/ckEditorApi.js b/packages/neos-ui-ckeditor5-bindings/src/ckEditorApi.js index 481568e01f..a7b65759e4 100644 --- a/packages/neos-ui-ckeditor5-bindings/src/ckEditorApi.js +++ b/packages/neos-ui-ckeditor5-bindings/src/ckEditorApi.js @@ -2,6 +2,7 @@ import debounce from 'lodash.debounce'; import DecoupledEditor from '@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor'; import {actions} from '@neos-project/neos-ui-redux-store'; import {cleanupContentBeforeCommit} from './cleanupContentBeforeCommit' +import './placeholder.vanilla-css'; // eslint-disable-line no-unused-vars let currentEditor = null; let editorConfig = {}; diff --git a/packages/neos-ui-ckeditor5-bindings/src/cleanupContentBeforeCommit.js b/packages/neos-ui-ckeditor5-bindings/src/cleanupContentBeforeCommit.js index 4efa9cb2f6..a9b6d0c3f7 100644 --- a/packages/neos-ui-ckeditor5-bindings/src/cleanupContentBeforeCommit.js +++ b/packages/neos-ui-ckeditor5-bindings/src/cleanupContentBeforeCommit.js @@ -1,28 +1,9 @@ -// We remove opening and closing span tags that are produced by the inlineMode plugin + +// TODO: remove when this is fixed: https://github.com/ckeditor/ckeditor5/issues/401 /** @param {String} content */ export const cleanupContentBeforeCommit = content => { - // TODO: remove when this is fixed: https://github.com/ckeditor/ckeditor5/issues/401 if (content.match(/^<([a-z][a-z0-9]*)\b[^>]*> <\/\1>$/)) { return ''; } - - if (content.includes('')) { - let contentWithoutOuterInlineWrapper = content; - - if (content.startsWith('') && content.endsWith('')) { - contentWithoutOuterInlineWrapper = content - .replace(/^/, '') - .replace(/<\/neos-inline-wrapper>$/, ''); - } - - if (contentWithoutOuterInlineWrapper.includes('')) { - // in the case, multiple root paragraph elements were inserted into the ckeditor (wich is currently not prevented if the html is modified from outside) - // we have multiple root elements of type . We will convert all of them into spans. - return content - .replace(//g, '') - .replace(/<\/neos-inline-wrapper>/g, ''); - } - return contentWithoutOuterInlineWrapper; - } return content; }; diff --git a/packages/neos-ui-ckeditor5-bindings/src/cleanupContentBeforeCommit.spec.js b/packages/neos-ui-ckeditor5-bindings/src/cleanupContentBeforeCommit.spec.js index 08ab6aad9e..08e5603a64 100644 --- a/packages/neos-ui-ckeditor5-bindings/src/cleanupContentBeforeCommit.spec.js +++ b/packages/neos-ui-ckeditor5-bindings/src/cleanupContentBeforeCommit.spec.js @@ -1,4 +1,4 @@ -import {cleanupContentBeforeCommit} from './cleanupContentBeforeCommit' +import {cleanupContentBeforeCommit} from './cleanupContentBeforeCommit'; const assertCleanedUpContent = (input, expected) => { expect(cleanupContentBeforeCommit(input)).toBe(expected); @@ -8,29 +8,3 @@ test('remove empty nbsp', () => { assertCleanedUpContent('

 

', ''); assertCleanedUpContent(' ', ''); }) - -describe('ckeditor inline mode hack, cleanup ', () => { - test('noop', () => { - assertCleanedUpContent('

', '

'); - - assertCleanedUpContent('', ''); - }) - - test('cleanup single ', () => { - assertCleanedUpContent('', ''); - assertCleanedUpContent('foo', 'foo'); - - assertCleanedUpContent('foo', 'foo'); - }) - - test('cleanup multiple ', () => { - assertCleanedUpContent('foobar', 'foobar'); - - assertCleanedUpContent('foobar', 'foobar'); - }) - - test('cleanup after other root', () => { - // in the case you had multiple paragraphs and a headline and switched to autoparagrahp: false - assertCleanedUpContent('

foo

bar', '

foo

bar'); - }) -}) diff --git a/packages/neos-ui-ckeditor5-bindings/src/manifest.config.js b/packages/neos-ui-ckeditor5-bindings/src/manifest.config.js index 09bbb788be..718fe1399a 100644 --- a/packages/neos-ui-ckeditor5-bindings/src/manifest.config.js +++ b/packages/neos-ui-ckeditor5-bindings/src/manifest.config.js @@ -1,7 +1,7 @@ import CkEditorConfigRegistry from './registry/CkEditorConfigRegistry'; import {$add, $get, $or} from 'plow-js'; +import {stripTags} from '@neos-project/utils-helpers'; -import NeosPlaceholder from './plugins/neosPlaceholder'; import InlineMode from './plugins/inlineMode'; import Sub from './plugins/sub'; import Sup from './plugins/sup'; @@ -81,13 +81,18 @@ export default ckEditorRegistry => { // // Base CKE configuration + // - configuration of language + // - and placeholder feature see https://ckeditor.com/docs/ckeditor5/16.0.0/api/module_core_editor_editorconfig-EditorConfig.html#member-placeholder // config.set('baseConfiguration', (ckEditorConfiguration, {globalRegistry, editorOptions, userPreferences}) => { const i18nRegistry = globalRegistry.get('i18n'); - return Object.assign(ckEditorConfiguration, { - language: String($get('interfaceLanguage', userPreferences)), - neosPlaceholder: unescape(i18nRegistry.translate($get('placeholder', editorOptions) || '')) - }); + const placeholder = $get('placeholder', editorOptions); + return { + ...ckEditorConfiguration, + // stripTags, because we allow `

Edit text here

` as placeholder for legacy + placeholder: placeholder ? stripTags(i18nRegistry.translate(placeholder)) : undefined, + language: String($get('interfaceLanguage', userPreferences)) + }; }); // @@ -96,7 +101,6 @@ export default ckEditorRegistry => { config.set('essentials', addPlugin(Essentials)); config.set('paragraph', addPlugin(Paragraph)); config.set('inlineMode', addPlugin(InlineMode, disableParagraph)); - config.set('neosPlaceholder', addPlugin(NeosPlaceholder)); config.set('sub', addPlugin(Sub, $get('formatting.sub'))); config.set('sup', addPlugin(Sup, $get('formatting.sup'))); config.set('bold', addPlugin(Bold, $get('formatting.strong'))); diff --git a/packages/neos-ui-ckeditor5-bindings/src/placeholder.vanilla-css b/packages/neos-ui-ckeditor5-bindings/src/placeholder.vanilla-css new file mode 100644 index 0000000000..7249952164 --- /dev/null +++ b/packages/neos-ui-ckeditor5-bindings/src/placeholder.vanilla-css @@ -0,0 +1,8 @@ +.ck.ck-placeholder:before, .ck .ck-placeholder:before { + content: attr(data-placeholder); + + /* See ckeditor/ckeditor5#469. */ + pointer-events: none; + + color: #999; +} diff --git a/packages/neos-ui-ckeditor5-bindings/src/plugins/cleanupNeosInlineWrapper.js b/packages/neos-ui-ckeditor5-bindings/src/plugins/cleanupNeosInlineWrapper.js new file mode 100644 index 0000000000..24498ce2d1 --- /dev/null +++ b/packages/neos-ui-ckeditor5-bindings/src/plugins/cleanupNeosInlineWrapper.js @@ -0,0 +1,27 @@ +/** + * We remove opening and closing span tags that are produced by the inlineMode plugin + * + * @private only exported for testing + * @param {String} content + */ +export const cleanupNeosInlineWrapper = content => { + if (content.includes('')) { + let contentWithoutOuterInlineWrapper = content; + + if (content.startsWith('') && content.endsWith('')) { + contentWithoutOuterInlineWrapper = content + .replace(/^/, '') + .replace(/<\/neos-inline-wrapper>$/, ''); + } + + if (contentWithoutOuterInlineWrapper.includes('')) { + // in the case, multiple root paragraph elements were inserted into the ckeditor (wich is currently not prevented if the html is modified from outside) + // we have multiple root elements of type . We will convert all of them into spans. + return content + .replace(//g, '') + .replace(/<\/neos-inline-wrapper>/g, ''); + } + return contentWithoutOuterInlineWrapper; + } + return content; +}; diff --git a/packages/neos-ui-ckeditor5-bindings/src/plugins/cleanupNeosInlineWrapper.spec.js b/packages/neos-ui-ckeditor5-bindings/src/plugins/cleanupNeosInlineWrapper.spec.js new file mode 100644 index 0000000000..4aa01d60c0 --- /dev/null +++ b/packages/neos-ui-ckeditor5-bindings/src/plugins/cleanupNeosInlineWrapper.spec.js @@ -0,0 +1,31 @@ +import {cleanupNeosInlineWrapper} from './cleanupNeosInlineWrapper'; + +const assertCleanedUpContent = (input, expected) => { + expect(cleanupNeosInlineWrapper(input)).toBe(expected); +} + +describe('ckeditor inline mode hack, cleanup ', () => { + test('noop', () => { + assertCleanedUpContent('

', '

'); + + assertCleanedUpContent('', ''); + }) + + test('cleanup single ', () => { + assertCleanedUpContent('', ''); + assertCleanedUpContent('foo', 'foo'); + + assertCleanedUpContent('foo', 'foo'); + }) + + test('cleanup multiple ', () => { + assertCleanedUpContent('foobar', 'foobar'); + + assertCleanedUpContent('foobar', 'foobar'); + }) + + test('cleanup after other root', () => { + // in the case you had multiple paragraphs and a headline and switched to autoparagrahp: false + assertCleanedUpContent('

foo

bar', '

foo

bar'); + }) +}) diff --git a/packages/neos-ui-ckeditor5-bindings/src/plugins/inlineMode.js b/packages/neos-ui-ckeditor5-bindings/src/plugins/inlineMode.js index 54540cb393..17b749d0d5 100644 --- a/packages/neos-ui-ckeditor5-bindings/src/plugins/inlineMode.js +++ b/packages/neos-ui-ckeditor5-bindings/src/plugins/inlineMode.js @@ -1,4 +1,5 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import {cleanupNeosInlineWrapper} from './cleanupNeosInlineWrapper'; /** * HACK, since there is yet no native support @@ -14,8 +15,7 @@ export default class InlineMode extends Plugin { // we map paragraph model into plain element in edit mode editor.conversion.for('editingDowncast').elementToElement({model: 'paragraph', view: 'span', converterPriority: 'high'}); - // to avoid having a wrapping "span" tag, we will convert the outmost 'paragraph' and strip the custom tag 'neos-inline-wrapper' - // in a hacky cleanup in cleanupContentBeforeCommit + // to avoid having a wrapping "span" tag, we will convert the outmost 'paragraph' ... // see https://neos-project.slack.com/archives/C07QEQ1U2/p1687952441254759 - i could find a better solution editor.conversion.for('dataDowncast').elementToElement({model: 'paragraph', view: ( modelElement, viewWriter ) => { const parentIsRoot = modelElement.parent.is('$root'); @@ -26,6 +26,12 @@ export default class InlineMode extends Plugin { return viewWriter.createContainerElement('neos-inline-wrapper'); }, converterPriority: 'high'}); + // ... and strip the custom tag 'neos-inline-wrapper' in a hacky cleanup in cleanupData + editor.data.decorate('get'); + editor.data.on('get', (event) => { + event.return = cleanupNeosInlineWrapper(event.return) + }); + // we redefine enter key to create soft breaks (
) instead of new paragraphs editor.editing.view.document.on('enter', (evt, data) => { editor.execute('shiftEnter'); diff --git a/packages/neos-ui-ckeditor5-bindings/src/plugins/neosPlaceholder.js b/packages/neos-ui-ckeditor5-bindings/src/plugins/neosPlaceholder.js deleted file mode 100644 index 50cce23673..0000000000 --- a/packages/neos-ui-ckeditor5-bindings/src/plugins/neosPlaceholder.js +++ /dev/null @@ -1,75 +0,0 @@ -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import {stripTags} from '@neos-project/utils-helpers'; -import styles from './neosPlaceholder.vanilla-css'; // eslint-disable-line no-unused-vars - -// If the data is "empty" (BR, P) or the placeholder then return an empty string. -// Otherwise return the original data -const htmlIsEmptyish = data => { - if (!data) { - return true; - } - - if (data.length > 20) { - return false; - } - const value = data.replace(/[\n|\t|\u200b]*/g, '').toLowerCase().trim(); - const a = ( - !value || - value === '
' || - value === ' ' || - value === '

 

' || - value === '

 

' || - value === '


' || - value === ' ' || - value === ' 
' || - value === '
' || - value.match(/^<([a-z0-9]+)>
<\/\1>$/) - ); - return a; -}; - -export default class NeosPlaceholder extends Plugin { - static get pluginName() { - return 'NeosPlaceholder'; - } - - getPlaceholder() { - return stripTags(this.editor.config.get('neosPlaceholder')); - } - - addPlaceholder() { - this.editor.editing.view.change(writer => { - writer.setAttribute('data-neos-placeholder', this.getPlaceholder(), this.editor.editing.view.document.getRoot()); - }); - } - - removePlaceholder() { - this.editor.editing.view.change(writer => { - writer.removeAttribute('data-neos-placeholder', this.editor.editing.view.document.getRoot()); - }); - } - - updatePlaceholder() { - if (htmlIsEmptyish(this.editor.getData({trim: 'none'})) && !this.editor.ui.focusTracker.isFocused) { - this.addPlaceholder(); - } else { - this.removePlaceholder(); - } - } - - init() { - if (this.editor.config.get('neosPlaceholder')) { - this.updatePlaceholder(); - - const model = this.editor.data.model; - - model.on('applyOperation', () => { - this.updatePlaceholder(); - return true; - }); - this.editor.ui.focusTracker.on('change:isFocused', () => { - this.updatePlaceholder(); - }); - } - } -} diff --git a/packages/neos-ui-ckeditor5-bindings/src/plugins/neosPlaceholder.vanilla-css b/packages/neos-ui-ckeditor5-bindings/src/plugins/neosPlaceholder.vanilla-css deleted file mode 100644 index a70a2b13ef..0000000000 --- a/packages/neos-ui-ckeditor5-bindings/src/plugins/neosPlaceholder.vanilla-css +++ /dev/null @@ -1,5 +0,0 @@ -[data-neos-placeholder]:before { - color: #999; - content: attr(data-neos-placeholder); - cursor: text; -} diff --git a/packages/neos-ui-ckeditor5-bindings/tests/manual/index.html b/packages/neos-ui-ckeditor5-bindings/tests/manual/index.html index 2f9831bb83..1f2bf8b006 100644 --- a/packages/neos-ui-ckeditor5-bindings/tests/manual/index.html +++ b/packages/neos-ui-ckeditor5-bindings/tests/manual/index.html @@ -3,6 +3,7 @@ + CKEditor Manual Test