Skip to content

Commit

Permalink
Issue #3400542 by Szabocs Páll, volkerk, a.milkovsky, googletorp, dan…
Browse files Browse the repository at this point in the history
…iel.bosen: Split text functionality for CKEditor 5
  • Loading branch information
ol0lll authored Apr 22, 2024
1 parent 7fce304 commit 3451716
Show file tree
Hide file tree
Showing 18 changed files with 4,424 additions and 382 deletions.
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ libraries/**/*
sites/**/libraries/**/*
profiles/**/libraries/**/*
**/js_test_files/**/*
js/build/*
js/ckeditor5_plugins/**/*
webpack.config.js
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v18.9.1
17 changes: 17 additions & 0 deletions README.CKEditor5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## CKEditor 5 plugin development

Based on the [CKEditor 5 plugin development starter template](https://git.drupalcode.org/project/ckeditor5_dev/-/tree/1.0.x/ckeditor5_plugin_starter_template).

Plugin source should be added to
`js/ckeditor5_plugins/{pluginNameDirectory}/src` and the build tools expect this
directory to include an `index.js` file that exports one or more CKEditor 5
plugins. Note that requiring `index.js` to be inside
`{pluginNameDirectory}/src` is not a fixed requirement of CKEditor 5 or Drupal,
and can be changed in `webpack.config.js` as needed.

In the module directory, run `npm install` to set up the necessary assets. The
initial run of `install` may take a few minutes, but subsequent builds will be
faster.

After installing dependencies, plugins can be built with `npm run build` or `npm run
watch`. They will be built to `js/build/{pluginNameDirectory}.js`. co
3 changes: 3 additions & 0 deletions css/split_paragraph.admin.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.ckeditor5-toolbar-button-splitParagraph {
background-image: url('../icons/split.svg');
}
7 changes: 7 additions & 0 deletions icons/split.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions js/build/split_paragraph.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions js/ckeditor5_plugins/split_paragraph/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* @file
* Split paragraph plugin.
*/

import { Plugin } from 'ckeditor5/src/core';
import { ButtonView } from 'ckeditor5/src/ui';
import SplitParagraphCommand from './splitparagraphcommand';
import icon from '../../../../icons/split.svg';

class SplitParagraph extends Plugin {
init() {
// Register splitParagraph toolbar button.
this.editor.ui.componentFactory.add('splitParagraph', (locale) => {
const command = this.editor.commands.get('splitParagraph');
const buttonView = new ButtonView(locale);

// Create toolbar button.
buttonView.set({
label: this.editor.t('Split Paragraph'),
icon,
tooltip: true,
});

buttonView.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled');

// Execute command when button is clicked.
this.listenTo(buttonView, 'execute', () => this.editor.execute('splitParagraph'));

return buttonView;
});
// Add toolbar button.
this.editor.commands.add(
'splitParagraph',
new SplitParagraphCommand(this.editor),
);
}

afterInit() {
// Set value of the new paragraph.
if (window._splitParagraph) {
if (typeof window._splitParagraph.data.second === 'string') {
const paragraph = this.editor.sourceElement.closest('tr.draggable');
let previousParagraph = paragraph?.previousElementSibling;

while (previousParagraph) {
if (previousParagraph.matches("tr.draggable")) break;
previousParagraph = previousParagraph.previousElementSibling;
}

if (
[...paragraph.parentElement.children].indexOf(previousParagraph) === window._splitParagraph.originalRowIndex &&
this.editor.sourceElement.dataset.drupalSelector.match(window._splitParagraph.selector.replace(/-[0-9]+-?/, '-[0-9]+-'))) {
// Defer to wait until init is complete.
setTimeout(() => {
this.editor.setData(window._splitParagraph.data.second);
window._splitParagraph.data.second = null;
}, 0);
}
}

if (typeof window._splitParagraph.data.first === 'string') {
if (this.editor.sourceElement.dataset.drupalSelector === window._splitParagraph.selector) {
// Defer to wait until init is complete.
setTimeout(() => {
this.editor.setData(window._splitParagraph.data.first);
window._splitParagraph.data.first = null;
}, 0);
}
}
}
}
}

export default {
SplitParagraph,
};
120 changes: 120 additions & 0 deletions js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* @file defines SplitParagraphCommand, which is executed when the splitParagraph
* toolbar button is pressed.
*/

import { Command } from 'ckeditor5/src/core';

export default class SplitParagraphCommand extends Command {
execute() {
const { model, sourceElement } = this.editor;
// @todo Use an html node with display:none instead
const splitMarker = Drupal.t('[Splitting in progress... ⌛]');
const originalText = this.editor.getData();

model.change(writer => {
writer.insertText(splitMarker, model.document.selection.getFirstPosition());
});

const rootElement = (new DOMParser()).parseFromString(this.editor.getData(), 'text/html').body;
const [elementBefore, elementAfter, markerFound] = SplitParagraphCommand.splitNode(rootElement, splitMarker);

if (!markerFound || !elementBefore || !elementAfter) {
this.editor.setData(originalText);
return;
}

// Get paragraph type and position.
const paragraph = sourceElement.closest('.paragraphs-subform').closest('tr.draggable');
const paragraphType = paragraph.querySelector('[data-paragraphs-split-text-type]').dataset.paragraphsSplitTextType;
const paragraphDelta = [...paragraph.parentNode.children].filter(el => el.querySelector('.paragraphs-actions')).indexOf(paragraph) + 1;
const originalRowIndex = [...paragraph.parentNode.children].indexOf(paragraph);

// Store the value of the paragraphs.
window._splitParagraph = {
data: {
first: elementBefore.outerHTML,
second: elementAfter.outerHTML,
},
selector: sourceElement.dataset.drupalSelector,
originalRowIndex: originalRowIndex,
};

// Add new paragraph after current.
const deltaField = sourceElement.closest('.field--widget-paragraphs').querySelector('input.paragraph-type-add-delta.modal');
deltaField.value = paragraphDelta;
const paragraphTypeButtonSelector = deltaField.getAttribute('data-drupal-selector').substr('edit-'.length).replace(/-add-more-add-more-delta$/, '-' + paragraphType + '-add-more').replace(/_/g, '-');
sourceElement.closest('.field--widget-paragraphs').querySelector('[data-drupal-selector^="' + paragraphTypeButtonSelector + '"]').dispatchEvent(new Event('mousedown'));
}

refresh() {
// Disable "Split Paragraph" button when not in paragraphs context.
this.isEnabled = !!this.editor.sourceElement.closest('.field--widget-paragraphs')?.querySelector('input.paragraph-type-add-delta.modal');
}

static splitNode(node, splitMarker) {
const nestedSplitter = (n) => {
if (n.nodeType === Node.TEXT_NODE) {
// Split position within text node.
const markerPos = n.data.indexOf(splitMarker);
if (markerPos >= 0) {
const textBeforeSplit = n.data.substring(0, markerPos);
const textAfterSplit = n.data.substring(markerPos + splitMarker.length);

return [
textBeforeSplit ? document.createTextNode(textBeforeSplit) : null,
textAfterSplit ? document.createTextNode(textAfterSplit) : null,
true,
];
}

return [n, null, false];
}

const childNodesBefore = [];
const childNodesAfter = [];
let found = false;
n.childNodes.forEach((childNode) => {
// Split not yet reached.
if (!found) {
const [childNodeBefore, childNodeAfter, markerFound] = nestedSplitter(childNode);
found = markerFound;

if (childNodeBefore) {
childNodesBefore.push(childNodeBefore);
}

if (childNodeAfter) {
childNodesAfter.push(childNodeAfter);
}
} else {
childNodesAfter.push(childNode);
}
});

// Node was not split.
if (!found) {
return [n, null, false];
}

const nodeBefore = n.cloneNode();
const nodeAfter = n.cloneNode();

childNodesBefore.forEach((childNode) => {
nodeBefore.appendChild(childNode);
});

childNodesAfter.forEach((childNode) => {
nodeAfter.appendChild(childNode);
});

return [
nodeBefore.childNodes.length > 0 ? nodeBefore : null,
nodeAfter.childNodes.length > 0 ? nodeAfter : null,
found,
];
};

return nestedSplitter(node);
}
}
5 changes: 2 additions & 3 deletions js/paragraphs-features.scroll-to-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
*/
Drupal.AjaxCommands.prototype.scrollToElement = function (ajax, response, status) {
var resizeObserver = new ResizeObserver(function () {
document
.querySelector('[data-drupal-selector=' + response.drupalElementSelector + ']')
.scrollIntoView({block: 'center'});
const elem = document.querySelector('[data-drupal-selector=' + response.drupalElementSelector + ']');
if (elem) {elem.scrollIntoView({block: 'center'});}
});

var parent = document.querySelector('[data-drupal-selector=' + response.drupalParentSelector + ']');
Expand Down
Loading

0 comments on commit 3451716

Please sign in to comment.