From b64689c29558dd4d22a425b57c8cacc6ff48a7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20P=C3=A1ll?= Date: Wed, 25 Oct 2023 15:30:17 +0200 Subject: [PATCH 1/9] Issue #3324781: Add split text functionality to CKEditor 5 --- css/split_paragraph.admin.css | 3 + icons/split.svg | 7 + js/build/split_paragraph.js | 1 + .../split_paragraph/src/index.js | 73 +++++++++++ .../src/splitparagraphcommand.js | 123 ++++++++++++++++++ package.json | 17 ++- paragraphs_features.ckeditor5.yml | 14 ++ paragraphs_features.info.yml | 1 + paragraphs_features.libraries.yml | 11 ++ 9 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 css/split_paragraph.admin.css create mode 100644 icons/split.svg create mode 100644 js/build/split_paragraph.js create mode 100644 js/ckeditor5_plugins/split_paragraph/src/index.js create mode 100644 js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js create mode 100644 paragraphs_features.ckeditor5.yml diff --git a/css/split_paragraph.admin.css b/css/split_paragraph.admin.css new file mode 100644 index 0000000..69ac0fd --- /dev/null +++ b/css/split_paragraph.admin.css @@ -0,0 +1,3 @@ +.ckeditor5-toolbar-button-splitParagraph { + background-image: url('../icons/split.svg'); +} diff --git a/icons/split.svg b/icons/split.svg new file mode 100644 index 0000000..4497d7a --- /dev/null +++ b/icons/split.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/js/build/split_paragraph.js b/js/build/split_paragraph.js new file mode 100644 index 0000000..b1016e6 --- /dev/null +++ b/js/build/split_paragraph.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.split_paragraph=e())}(self,(()=>(()=>{var t={"ckeditor5/src/core.js":(t,e,r)=>{t.exports=r("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(t,e,r)=>{t.exports=r("dll-reference CKEditor5.dll")("./src/ui.js")},"dll-reference CKEditor5.dll":t=>{"use strict";t.exports=CKEditor5.dll}},e={};function r(o){var s=e[o];if(void 0!==s)return s.exports;var i=e[o]={exports:{}};return t[o](i,i.exports,r),i.exports}r.d=(t,e)=>{for(var o in e)r.o(e,o)&&!r.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var o={};return(()=>{"use strict";r.d(o,{default:()=>a});var t=r("ckeditor5/src/core.js"),e=r("ckeditor5/src/ui.js");class s extends t.Command{execute(){const{model:t,sourceElement:e}=this.editor,[r,o]=t.document.selection.getFirstPosition().path,s=(new DOMParser).parseFromString(this.editor.getData(),"text/html").body.children,i=[],a=[],n=t=>t.reduce(((t,e)=>t+e.outerHTML),"");let l=0;Array.from(s).forEach((t=>{if(lr&&a.push(t),l+=1}));const d=n(i),p=n(a);window._splitParagraph={data:{first:d,second:p},selector:e.dataset.drupalSelector},e.closest(".paragraph-type--text").nextElementSibling.querySelector(".paragraphs-features__add-in-between__button").click()}refresh(){this.isEnabled=!0}static splitNode(t,e){let r=0;const o=t=>{if(t.nodeType===Node.TEXT_NODE){if(t.data.length>e-r){const o=t.data.substring(0,e-r),s=t.data.substring(e-r);return[o?document.createTextNode(o):null,s?document.createTextNode(s):null]}return r+=t.data.length,[t,null]}const s=[],i=[];if(t.childNodes.forEach((t=>{if(0===i.length){const[e,r]=o(t);e&&s.push(e),r&&i.push(r)}else i.push(t)})),0===i.length)return[t,null];const a=t.cloneNode(),n=t.cloneNode();return s.forEach((t=>{a.appendChild(t)})),i.forEach((t=>{n.appendChild(t)})),[a.childNodes.length>0?a:null,n.childNodes.length>0?n:null]};return o(t)}}class i extends t.Plugin{init(){null!=this.editor.sourceElement.closest(".paragraph-type--text")&&(this.editor.ui.componentFactory.add("splitParagraph",(t=>{const r=this.editor.commands.get("splitParagraph"),o=new e.ButtonView(t);return o.set({label:this.editor.t("Split Paragraph"),icon:'\r\n\r\n\r\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --\x3e\n\r\n\t\r\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>this.editor.execute("splitParagraph"))),o})),this.editor.commands.add("splitParagraph",new s(this.editor)))}afterInit(){if(window._splitParagraph){if("string"==typeof window._splitParagraph.data.second){const t=this.editor.sourceElement.closest(".paragraph-type--text")?.previousElementSibling?.previousElementSibling;t&&t.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.second),window._splitParagraph.data.second=null}),0)}"string"==typeof window._splitParagraph.data.first&&this.editor.sourceElement.dataset.drupalSelector===window._splitParagraph.selector&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.first),window._splitParagraph.data.first=null}),0)}}}const a={SplitParagraph:i}})(),o.default})())); \ No newline at end of file diff --git a/js/ckeditor5_plugins/split_paragraph/src/index.js b/js/ckeditor5_plugins/split_paragraph/src/index.js new file mode 100644 index 0000000..85bd834 --- /dev/null +++ b/js/ckeditor5_plugins/split_paragraph/src/index.js @@ -0,0 +1,73 @@ +/** + * @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() { + // Only split text paragraphs. + if (this.editor.sourceElement.closest('.paragraph-type--text') == null) { + return; + } + + // This will register the splitParagraph toolbar button. + this.editor.ui.componentFactory.add('splitParagraph', (locale) => { + const command = this.editor.commands.get('splitParagraph'); + const buttonView = new ButtonView(locale); + + // Create the toolbar button. + buttonView.set({ + label: this.editor.t('Split Paragraph'), + icon, + tooltip: true, + }); + + buttonView.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled'); + + // Execute the command when the button is clicked. + this.listenTo(buttonView, 'execute', () => this.editor.execute('splitParagraph')); + + return buttonView; + }); + + 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 previousParagraph = this.editor.sourceElement.closest('.paragraph-type--text')?.previousElementSibling?.previousElementSibling; + if (previousParagraph && previousParagraph.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)) { + // 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, +}; diff --git a/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js b/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js new file mode 100644 index 0000000..ab9db22 --- /dev/null +++ b/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js @@ -0,0 +1,123 @@ +/** + * @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; + const [splitElement, splitText] = model.document.selection.getFirstPosition().path; + const elements = (new DOMParser()).parseFromString(this.editor.getData(), 'text/html').body.children; + const elementsBefore = []; + const elementsAfter = []; + const nodeArrayToHTML = (nodeArray) => nodeArray.reduce((data, element) => data + element.outerHTML, ''); + + let i = 0; + Array.from(elements).forEach((el) => { + if (i < splitElement) { + elementsBefore.push(el); + } + + if (i === splitElement) { + const [nodeBefore, nodeAfter] = this.splitNode(el, splitText); + + if (nodeBefore) { + elementsBefore.push(nodeBefore); + } + + if (nodeAfter) { + elementsAfter.push(nodeAfter); + } + } + + if (i > splitElement) { + elementsAfter.push(el); + } + i += 1; + }); + + // store the value of the paragraphs + const firstData = nodeArrayToHTML(elementsBefore); + const secondData = nodeArrayToHTML(elementsAfter); + window._splitParagraph = { + data: { + first: firstData, + second: secondData, + }, + selector: sourceElement.dataset.drupalSelector, + }; + + // add new paragraph below + sourceElement.closest('.paragraph-type--text').nextElementSibling.querySelector('.paragraphs-features__add-in-between__button').click(); + } + + refresh() { + this.isEnabled = true; + } + + static splitNode(node, splitAt) { + let characterCount = 0; + + const nestedSplitter = (n) => { + if (n.nodeType === Node.TEXT_NODE) { + // split position within text node + if (n.data.length > splitAt - characterCount) { + const textBeforeSplit = n.data.substring(0, splitAt - characterCount); + const textAfterSplit = n.data.substring(splitAt - characterCount); + + return [ + textBeforeSplit ? document.createTextNode(textBeforeSplit) : null, + textAfterSplit ? document.createTextNode(textAfterSplit) : null, + ]; + } + + characterCount += n.data.length; + return [n, null]; + } + + const childNodesBefore = []; + const childNodesAfter = []; + n.childNodes.forEach((childNode) => { + // split not yet reached + if (childNodesAfter.length === 0) { + const [childNodeBefore, childNodeAfter] = nestedSplitter(childNode); + + if (childNodeBefore) { + childNodesBefore.push(childNodeBefore); + } + + if (childNodeAfter) { + childNodesAfter.push(childNodeAfter); + } + } else { + childNodesAfter.push(childNode); + } + }); + + // node was not split + if (childNodesAfter.length === 0) { + return [n, null]; + } + + 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, + ]; + }; + + return nestedSplitter(node); + } +} diff --git a/package.json b/package.json index a7dbca4..b911cf6 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,20 @@ { + "name": "drupal-ckeditor5", + "version": "1.0.0", + "description": "Drupal CKEditor 5 integration", + "author": "", + "license": "GPL-2.0-or-later", + "scripts": { + "watch": "webpack --mode development --watch", + "build": "webpack" + }, + "dependencies": { + "ckeditor5": "~34.1.0" + }, "devDependencies": { - "eslint": "^8" + "eslint": "^8", + "raw-loader": "^4.0.2", + "webpack": "^5.51.1", + "webpack-cli": "^4.4.0" } } diff --git a/paragraphs_features.ckeditor5.yml b/paragraphs_features.ckeditor5.yml new file mode 100644 index 0000000..63bbf45 --- /dev/null +++ b/paragraphs_features.ckeditor5.yml @@ -0,0 +1,14 @@ +paragraphs_features_split_paragraph: + ckeditor5: + plugins: + - split_paragraph.SplitParagraph + drupal: + label: Split paragraph + library: paragraphs_features/splitParagraph + admin_library: paragraphs_features/splitParagraph.admin + toolbar_items: + splitParagraph: + label: Split paragraph + elements: + -

+ -

\ No newline at end of file diff --git a/paragraphs_features.info.yml b/paragraphs_features.info.yml index 16c28c0..b3094b0 100644 --- a/paragraphs_features.info.yml +++ b/paragraphs_features.info.yml @@ -7,3 +7,4 @@ configure: paragraphs_features.settings_form dependencies: - paragraphs:paragraphs - drupal:field + - drupal:ckeditor5 diff --git a/paragraphs_features.libraries.yml b/paragraphs_features.libraries.yml index 216d0cd..f7a0760 100644 --- a/paragraphs_features.libraries.yml +++ b/paragraphs_features.libraries.yml @@ -33,3 +33,14 @@ scroll_to_element: js/paragraphs-features.scroll-to-element.js: {} dependencies: - core/drupal + +splitParagraph: + js: + js/build/split_paragraph.js: { preprocess: false, minified: true } + dependencies: + - core/ckeditor5 + +splitParagraph.admin: + css: + theme: + css/split_paragraph.admin.css: {} \ No newline at end of file From a108da4d2e6be08af5b5774f46de416ac5246810 Mon Sep 17 00:00:00 2001 From: Szabolcs Date: Thu, 9 Nov 2023 16:21:35 +0100 Subject: [PATCH 2/9] Update paragraphs_features.ckeditor5.yml --- paragraphs_features.ckeditor5.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paragraphs_features.ckeditor5.yml b/paragraphs_features.ckeditor5.yml index 63bbf45..6017403 100644 --- a/paragraphs_features.ckeditor5.yml +++ b/paragraphs_features.ckeditor5.yml @@ -11,4 +11,4 @@ paragraphs_features_split_paragraph: label: Split paragraph elements: -

- -

\ No newline at end of file + -
From c54e7b5488b6d97400a4f3cb546cf14bc6dfb0c9 Mon Sep 17 00:00:00 2001 From: Szabolcs Date: Thu, 9 Nov 2023 16:22:03 +0100 Subject: [PATCH 3/9] Update paragraphs_features.libraries.yml --- paragraphs_features.libraries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paragraphs_features.libraries.yml b/paragraphs_features.libraries.yml index f7a0760..93bf71c 100644 --- a/paragraphs_features.libraries.yml +++ b/paragraphs_features.libraries.yml @@ -43,4 +43,4 @@ splitParagraph: splitParagraph.admin: css: theme: - css/split_paragraph.admin.css: {} \ No newline at end of file + css/split_paragraph.admin.css: {} From d75047b4c6386223495b28cf9bffacbb7214793e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20P=C3=A1ll?= Date: Thu, 16 Nov 2023 14:52:42 +0100 Subject: [PATCH 4/9] Fix issues and generalize type --- .../split_paragraph/src/index.js | 15 +++++----- .../src/splitparagraphcommand.js | 30 +++++++++++++++++-- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/js/ckeditor5_plugins/split_paragraph/src/index.js b/js/ckeditor5_plugins/split_paragraph/src/index.js index 85bd834..60fe3e9 100644 --- a/js/ckeditor5_plugins/split_paragraph/src/index.js +++ b/js/ckeditor5_plugins/split_paragraph/src/index.js @@ -10,31 +10,32 @@ import icon from '../../../../icons/split.svg'; class SplitParagraph extends Plugin { init() { - // Only split text paragraphs. - if (this.editor.sourceElement.closest('.paragraph-type--text') == null) { + // Only split paragraphs. + if (this.editor.sourceElement.closest('[class*="paragraph-type--"]') == null) { return; } - // This will register the splitParagraph toolbar button. + // Register splitParagraph toolbar button. this.editor.ui.componentFactory.add('splitParagraph', (locale) => { const command = this.editor.commands.get('splitParagraph'); const buttonView = new ButtonView(locale); - // Create the toolbar button. + // Create toolbar button. buttonView.set({ - label: this.editor.t('Split Paragraph'), + label: this.editor.t('Simple Split Paragraph'), icon, tooltip: true, }); buttonView.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled'); - // Execute the command when the button is clicked. + // 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), @@ -45,7 +46,7 @@ class SplitParagraph extends Plugin { // set value of the new paragraph if (window._splitParagraph) { if (typeof window._splitParagraph.data.second === 'string') { - const previousParagraph = this.editor.sourceElement.closest('.paragraph-type--text')?.previousElementSibling?.previousElementSibling; + const previousParagraph = this.editor.sourceElement.closest('[class*="paragraph-type--"]')?.previousElementSibling?.previousElementSibling; if (previousParagraph && previousParagraph.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)) { // defer to wait until init is complete setTimeout(() => { diff --git a/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js b/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js index ab9db22..b167e04 100644 --- a/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js +++ b/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js @@ -10,6 +10,23 @@ export default class SplitParagraphCommand extends Command { const { model, sourceElement } = this.editor; const [splitElement, splitText] = model.document.selection.getFirstPosition().path; const elements = (new DOMParser()).parseFromString(this.editor.getData(), 'text/html').body.children; + + // cursor is at the start + if (splitElement === 0 && splitText === 0) { + return; + } + + // all lines are empty + if (elements.length === 0) { + return; + } + + // cursor is at the end + const sanitizedInnerTextOfLastElement = elements[elements.length - 1].innerText.replace(String.fromCharCode(160), '').length; + if (splitElement === elements.length - 1 && splitText === sanitizedInnerTextOfLastElement) { + return; + } + const elementsBefore = []; const elementsAfter = []; const nodeArrayToHTML = (nodeArray) => nodeArray.reduce((data, element) => data + element.outerHTML, ''); @@ -21,7 +38,7 @@ export default class SplitParagraphCommand extends Command { } if (i === splitElement) { - const [nodeBefore, nodeAfter] = this.splitNode(el, splitText); + const [nodeBefore, nodeAfter] = SplitParagraphCommand.splitNode(el, splitText); if (nodeBefore) { elementsBefore.push(nodeBefore); @@ -38,6 +55,12 @@ export default class SplitParagraphCommand extends Command { i += 1; }); + // get paragraph type and position + const findParagraphClass = p => [...p.classList].find(c => /^paragraph-type/.test(c)); + const paragraph = sourceElement.closest('[class*="paragraph-type--"]'); + const paragraphType = findParagraphClass(paragraph).replace('paragraph-type--','').replace('-', '_'); + const paragraphDelta = [...paragraph.parentNode.children].filter(findParagraphClass).indexOf(paragraph) + 1; + // store the value of the paragraphs const firstData = nodeArrayToHTML(elementsBefore); const secondData = nodeArrayToHTML(elementsAfter); @@ -49,8 +72,9 @@ export default class SplitParagraphCommand extends Command { selector: sourceElement.dataset.drupalSelector, }; - // add new paragraph below - sourceElement.closest('.paragraph-type--text').nextElementSibling.querySelector('.paragraphs-features__add-in-between__button').click(); + // add new paragragraph after current + document.querySelector('input.paragraph-type-add-delta.modal').value = paragraphDelta; + document.getElementsByName(`field_paragraphs_${paragraphType}_add_more`)[0].dispatchEvent(new Event('mousedown')); } refresh() { From 6ea22e3802baa13d83029f5136b33613857ac87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20P=C3=A1ll?= Date: Thu, 16 Nov 2023 15:02:45 +0100 Subject: [PATCH 5/9] Add README --- README.CKEditor5.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 README.CKEditor5.md diff --git a/README.CKEditor5.md b/README.CKEditor5.md new file mode 100644 index 0000000..16a29ac --- /dev/null +++ b/README.CKEditor5.md @@ -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 From 9258dd47c699734d70489453d1c0dbaee5ee5aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20P=C3=A1ll?= Date: Thu, 16 Nov 2023 15:04:28 +0100 Subject: [PATCH 6/9] Update build --- js/build/split_paragraph.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/build/split_paragraph.js b/js/build/split_paragraph.js index b1016e6..1cf5e13 100644 --- a/js/build/split_paragraph.js +++ b/js/build/split_paragraph.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.split_paragraph=e())}(self,(()=>(()=>{var t={"ckeditor5/src/core.js":(t,e,r)=>{t.exports=r("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(t,e,r)=>{t.exports=r("dll-reference CKEditor5.dll")("./src/ui.js")},"dll-reference CKEditor5.dll":t=>{"use strict";t.exports=CKEditor5.dll}},e={};function r(o){var s=e[o];if(void 0!==s)return s.exports;var i=e[o]={exports:{}};return t[o](i,i.exports,r),i.exports}r.d=(t,e)=>{for(var o in e)r.o(e,o)&&!r.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var o={};return(()=>{"use strict";r.d(o,{default:()=>a});var t=r("ckeditor5/src/core.js"),e=r("ckeditor5/src/ui.js");class s extends t.Command{execute(){const{model:t,sourceElement:e}=this.editor,[r,o]=t.document.selection.getFirstPosition().path,s=(new DOMParser).parseFromString(this.editor.getData(),"text/html").body.children,i=[],a=[],n=t=>t.reduce(((t,e)=>t+e.outerHTML),"");let l=0;Array.from(s).forEach((t=>{if(lr&&a.push(t),l+=1}));const d=n(i),p=n(a);window._splitParagraph={data:{first:d,second:p},selector:e.dataset.drupalSelector},e.closest(".paragraph-type--text").nextElementSibling.querySelector(".paragraphs-features__add-in-between__button").click()}refresh(){this.isEnabled=!0}static splitNode(t,e){let r=0;const o=t=>{if(t.nodeType===Node.TEXT_NODE){if(t.data.length>e-r){const o=t.data.substring(0,e-r),s=t.data.substring(e-r);return[o?document.createTextNode(o):null,s?document.createTextNode(s):null]}return r+=t.data.length,[t,null]}const s=[],i=[];if(t.childNodes.forEach((t=>{if(0===i.length){const[e,r]=o(t);e&&s.push(e),r&&i.push(r)}else i.push(t)})),0===i.length)return[t,null];const a=t.cloneNode(),n=t.cloneNode();return s.forEach((t=>{a.appendChild(t)})),i.forEach((t=>{n.appendChild(t)})),[a.childNodes.length>0?a:null,n.childNodes.length>0?n:null]};return o(t)}}class i extends t.Plugin{init(){null!=this.editor.sourceElement.closest(".paragraph-type--text")&&(this.editor.ui.componentFactory.add("splitParagraph",(t=>{const r=this.editor.commands.get("splitParagraph"),o=new e.ButtonView(t);return o.set({label:this.editor.t("Split Paragraph"),icon:'\r\n\r\n\r\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --\x3e\n\r\n\t\r\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>this.editor.execute("splitParagraph"))),o})),this.editor.commands.add("splitParagraph",new s(this.editor)))}afterInit(){if(window._splitParagraph){if("string"==typeof window._splitParagraph.data.second){const t=this.editor.sourceElement.closest(".paragraph-type--text")?.previousElementSibling?.previousElementSibling;t&&t.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.second),window._splitParagraph.data.second=null}),0)}"string"==typeof window._splitParagraph.data.first&&this.editor.sourceElement.dataset.drupalSelector===window._splitParagraph.selector&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.first),window._splitParagraph.data.first=null}),0)}}}const a={SplitParagraph:i}})(),o.default})())); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.split_paragraph=t())}(self,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,r)=>{e.exports=r("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(e,t,r)=>{e.exports=r("dll-reference CKEditor5.dll")("./src/ui.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function r(o){var a=t[o];if(void 0!==a)return a.exports;var s=t[o]={exports:{}};return e[o](s,s.exports,r),s.exports}r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var o={};return(()=>{"use strict";r.d(o,{default:()=>i});var e=r("ckeditor5/src/core.js"),t=r("ckeditor5/src/ui.js");class a extends e.Command{execute(){const{model:e,sourceElement:t}=this.editor,[r,o]=e.document.selection.getFirstPosition().path,s=(new DOMParser).parseFromString(this.editor.getData(),"text/html").body.children;if(0===r&&0===o)return;if(0===s.length)return;const i=s[s.length-1].innerText.replace(String.fromCharCode(160),"").length;if(r===s.length-1&&o===i)return;const n=[],l=[],d=e=>e.reduce(((e,t)=>e+t.outerHTML),"");let p=0;Array.from(s).forEach((e=>{if(pr&&l.push(e),p+=1}));const c=e=>[...e.classList].find((e=>/^paragraph-type/.test(e))),h=t.closest('[class*="paragraph-type--"]'),u=c(h).replace("paragraph-type--","").replace("-","_"),g=[...h.parentNode.children].filter(c).indexOf(h)+1,f=d(n),w=d(l);window._splitParagraph={data:{first:f,second:w},selector:t.dataset.drupalSelector},document.querySelector("input.paragraph-type-add-delta.modal").value=g,document.getElementsByName(`field_paragraphs_${u}_add_more`)[0].dispatchEvent(new Event("mousedown"))}refresh(){this.isEnabled=!0}static splitNode(e,t){let r=0;const o=e=>{if(e.nodeType===Node.TEXT_NODE){if(e.data.length>t-r){const o=e.data.substring(0,t-r),a=e.data.substring(t-r);return[o?document.createTextNode(o):null,a?document.createTextNode(a):null]}return r+=e.data.length,[e,null]}const a=[],s=[];if(e.childNodes.forEach((e=>{if(0===s.length){const[t,r]=o(e);t&&a.push(t),r&&s.push(r)}else s.push(e)})),0===s.length)return[e,null];const i=e.cloneNode(),n=e.cloneNode();return a.forEach((e=>{i.appendChild(e)})),s.forEach((e=>{n.appendChild(e)})),[i.childNodes.length>0?i:null,n.childNodes.length>0?n:null]};return o(e)}}class s extends e.Plugin{init(){null!=this.editor.sourceElement.closest('[class*="paragraph-type--"]')&&(this.editor.ui.componentFactory.add("splitParagraph",(e=>{const r=this.editor.commands.get("splitParagraph"),o=new t.ButtonView(e);return o.set({label:this.editor.t("Simple Split Paragraph"),icon:'\r\n\r\n\r\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --\x3e\r\n\r\n\t\r\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>this.editor.execute("splitParagraph"))),o})),this.editor.commands.add("splitParagraph",new a(this.editor)))}afterInit(){if(window._splitParagraph){if("string"==typeof window._splitParagraph.data.second){const e=this.editor.sourceElement.closest('[class*="paragraph-type--"]')?.previousElementSibling?.previousElementSibling;e&&e.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.second),window._splitParagraph.data.second=null}),0)}"string"==typeof window._splitParagraph.data.first&&this.editor.sourceElement.dataset.drupalSelector===window._splitParagraph.selector&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.first),window._splitParagraph.data.first=null}),0)}}}const i={SplitParagraph:s}})(),o.default})())); \ No newline at end of file From 2fd9a40c46a9b33e4fb1726b091223d511617f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20P=C3=A1ll?= Date: Thu, 16 Nov 2023 15:09:03 +0100 Subject: [PATCH 7/9] Add nvmrc --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..b04cd97 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.9.1 From 0adbebf21a012384e516c6ff9ccfe4c528b517de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20P=C3=A1ll?= Date: Mon, 20 Nov 2023 11:18:39 +0100 Subject: [PATCH 8/9] Fix selectors --- js/build/split_paragraph.js | 2 +- .../split_paragraph/src/index.js | 11 +++---- .../src/splitparagraphcommand.js | 29 +++++++++---------- paragraphs_features.module | 13 +++++++++ 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/js/build/split_paragraph.js b/js/build/split_paragraph.js index 1cf5e13..5272875 100644 --- a/js/build/split_paragraph.js +++ b/js/build/split_paragraph.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.split_paragraph=t())}(self,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,r)=>{e.exports=r("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(e,t,r)=>{e.exports=r("dll-reference CKEditor5.dll")("./src/ui.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function r(o){var a=t[o];if(void 0!==a)return a.exports;var s=t[o]={exports:{}};return e[o](s,s.exports,r),s.exports}r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var o={};return(()=>{"use strict";r.d(o,{default:()=>i});var e=r("ckeditor5/src/core.js"),t=r("ckeditor5/src/ui.js");class a extends e.Command{execute(){const{model:e,sourceElement:t}=this.editor,[r,o]=e.document.selection.getFirstPosition().path,s=(new DOMParser).parseFromString(this.editor.getData(),"text/html").body.children;if(0===r&&0===o)return;if(0===s.length)return;const i=s[s.length-1].innerText.replace(String.fromCharCode(160),"").length;if(r===s.length-1&&o===i)return;const n=[],l=[],d=e=>e.reduce(((e,t)=>e+t.outerHTML),"");let p=0;Array.from(s).forEach((e=>{if(pr&&l.push(e),p+=1}));const c=e=>[...e.classList].find((e=>/^paragraph-type/.test(e))),h=t.closest('[class*="paragraph-type--"]'),u=c(h).replace("paragraph-type--","").replace("-","_"),g=[...h.parentNode.children].filter(c).indexOf(h)+1,f=d(n),w=d(l);window._splitParagraph={data:{first:f,second:w},selector:t.dataset.drupalSelector},document.querySelector("input.paragraph-type-add-delta.modal").value=g,document.getElementsByName(`field_paragraphs_${u}_add_more`)[0].dispatchEvent(new Event("mousedown"))}refresh(){this.isEnabled=!0}static splitNode(e,t){let r=0;const o=e=>{if(e.nodeType===Node.TEXT_NODE){if(e.data.length>t-r){const o=e.data.substring(0,t-r),a=e.data.substring(t-r);return[o?document.createTextNode(o):null,a?document.createTextNode(a):null]}return r+=e.data.length,[e,null]}const a=[],s=[];if(e.childNodes.forEach((e=>{if(0===s.length){const[t,r]=o(e);t&&a.push(t),r&&s.push(r)}else s.push(e)})),0===s.length)return[e,null];const i=e.cloneNode(),n=e.cloneNode();return a.forEach((e=>{i.appendChild(e)})),s.forEach((e=>{n.appendChild(e)})),[i.childNodes.length>0?i:null,n.childNodes.length>0?n:null]};return o(e)}}class s extends e.Plugin{init(){null!=this.editor.sourceElement.closest('[class*="paragraph-type--"]')&&(this.editor.ui.componentFactory.add("splitParagraph",(e=>{const r=this.editor.commands.get("splitParagraph"),o=new t.ButtonView(e);return o.set({label:this.editor.t("Simple Split Paragraph"),icon:'\r\n\r\n\r\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --\x3e\r\n\r\n\t\r\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>this.editor.execute("splitParagraph"))),o})),this.editor.commands.add("splitParagraph",new a(this.editor)))}afterInit(){if(window._splitParagraph){if("string"==typeof window._splitParagraph.data.second){const e=this.editor.sourceElement.closest('[class*="paragraph-type--"]')?.previousElementSibling?.previousElementSibling;e&&e.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.second),window._splitParagraph.data.second=null}),0)}"string"==typeof window._splitParagraph.data.first&&this.editor.sourceElement.dataset.drupalSelector===window._splitParagraph.selector&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.first),window._splitParagraph.data.first=null}),0)}}}const i={SplitParagraph:s}})(),o.default})())); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.split_paragraph=t())}(self,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,r)=>{e.exports=r("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(e,t,r)=>{e.exports=r("dll-reference CKEditor5.dll")("./src/ui.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function r(o){var a=t[o];if(void 0!==a)return a.exports;var s=t[o]={exports:{}};return e[o](s,s.exports,r),s.exports}r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var o={};return(()=>{"use strict";r.d(o,{default:()=>i});var e=r("ckeditor5/src/core.js"),t=r("ckeditor5/src/ui.js");class a extends e.Command{execute(){const{model:e,sourceElement:t}=this.editor,[r,o]=e.document.selection.getFirstPosition().path,s=(new DOMParser).parseFromString(this.editor.getData(),"text/html").body.children;if(0===r&&0===o)return;if(0===s.length)return;const i=s[s.length-1].innerText.replace(String.fromCharCode(160),"").length;if(r===s.length-1&&o===i)return;const n=[],l=[],d=e=>e.reduce(((e,t)=>e+t.outerHTML),"");let p=0;Array.from(s).forEach((e=>{if(pr&&l.push(e),p+=1}));const c=t.closest(".paragraphs-subform").closest("tr"),h=c.querySelector("[data-paragraphs-split-text-type]").dataset.paragraphsSplitTextType,u=[...c.parentNode.children].filter((e=>e.querySelector(".paragraphs-subform"))).indexOf(c)+1,g=d(n),f=d(l);window._splitParagraph={data:{first:g,second:f},selector:t.dataset.drupalSelector},t.closest(".paragraphs-container").querySelector("input.paragraph-type-add-delta.modal").value=u,t.closest(".paragraphs-container").querySelector(`input[data-paragraph-type="${h}"].field-add-more-submit`).dispatchEvent(new Event("mousedown"))}refresh(){this.isEnabled=!0}static splitNode(e,t){let r=0;const o=e=>{if(e.nodeType===Node.TEXT_NODE){if(e.data.length>t-r){const o=e.data.substring(0,t-r),a=e.data.substring(t-r);return[o?document.createTextNode(o):null,a?document.createTextNode(a):null]}return r+=e.data.length,[e,null]}const a=[],s=[];if(e.childNodes.forEach((e=>{if(0===s.length){const[t,r]=o(e);t&&a.push(t),r&&s.push(r)}else s.push(e)})),0===s.length)return[e,null];const i=e.cloneNode(),n=e.cloneNode();return a.forEach((e=>{i.appendChild(e)})),s.forEach((e=>{n.appendChild(e)})),[i.childNodes.length>0?i:null,n.childNodes.length>0?n:null]};return o(e)}}class s extends e.Plugin{init(){null!=this.editor.sourceElement.closest(".paragraphs-container").querySelector("input.paragraph-type-add-delta.modal")&&(this.editor.ui.componentFactory.add("splitParagraph",(e=>{const r=this.editor.commands.get("splitParagraph"),o=new t.ButtonView(e);return o.set({label:this.editor.t("Simple Split Paragraph"),icon:'\r\n\r\n\r\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --\x3e\r\n\r\n\t\r\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>this.editor.execute("splitParagraph"))),o})),this.editor.commands.add("splitParagraph",new a(this.editor)))}afterInit(){if(window._splitParagraph){if("string"==typeof window._splitParagraph.data.second){const e=this.editor.sourceElement.closest(".paragraphs-subform").closest("tr"),t=e?.previousElementSibling?.previousElementSibling;t&&t.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.second),window._splitParagraph.data.second=null}),0)}"string"==typeof window._splitParagraph.data.first&&this.editor.sourceElement.dataset.drupalSelector===window._splitParagraph.selector&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.first),window._splitParagraph.data.first=null}),0)}}}const i={SplitParagraph:s}})(),o.default})())); \ No newline at end of file diff --git a/js/ckeditor5_plugins/split_paragraph/src/index.js b/js/ckeditor5_plugins/split_paragraph/src/index.js index 60fe3e9..459880a 100644 --- a/js/ckeditor5_plugins/split_paragraph/src/index.js +++ b/js/ckeditor5_plugins/split_paragraph/src/index.js @@ -11,7 +11,7 @@ import icon from '../../../../icons/split.svg'; class SplitParagraph extends Plugin { init() { // Only split paragraphs. - if (this.editor.sourceElement.closest('[class*="paragraph-type--"]') == null) { + if (this.editor.sourceElement.closest('.paragraphs-container').querySelector('input.paragraph-type-add-delta.modal') == null) { return; } @@ -43,12 +43,13 @@ class SplitParagraph extends Plugin { } afterInit() { - // set value of the new paragraph + // Set value of the new paragraph. if (window._splitParagraph) { if (typeof window._splitParagraph.data.second === 'string') { - const previousParagraph = this.editor.sourceElement.closest('[class*="paragraph-type--"]')?.previousElementSibling?.previousElementSibling; + const paragraph = this.editor.sourceElement.closest('.paragraphs-subform').closest('tr'); + const previousParagraph = paragraph?.previousElementSibling?.previousElementSibling; if (previousParagraph && previousParagraph.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)) { - // defer to wait until init is complete + // Defer to wait until init is complete. setTimeout(() => { this.editor.setData(window._splitParagraph.data.second); window._splitParagraph.data.second = null; @@ -58,7 +59,7 @@ class SplitParagraph extends Plugin { if (typeof window._splitParagraph.data.first === 'string') { if (this.editor.sourceElement.dataset.drupalSelector === window._splitParagraph.selector) { - // defer to wait until init is complete + // Defer to wait until init is complete. setTimeout(() => { this.editor.setData(window._splitParagraph.data.first); window._splitParagraph.data.first = null; diff --git a/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js b/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js index b167e04..b0f74c0 100644 --- a/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js +++ b/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js @@ -11,17 +11,17 @@ export default class SplitParagraphCommand extends Command { const [splitElement, splitText] = model.document.selection.getFirstPosition().path; const elements = (new DOMParser()).parseFromString(this.editor.getData(), 'text/html').body.children; - // cursor is at the start + // Cursor is at the start. if (splitElement === 0 && splitText === 0) { return; } - // all lines are empty + // All lines are empty. if (elements.length === 0) { return; } - // cursor is at the end + // Cursor is at the end. const sanitizedInnerTextOfLastElement = elements[elements.length - 1].innerText.replace(String.fromCharCode(160), '').length; if (splitElement === elements.length - 1 && splitText === sanitizedInnerTextOfLastElement) { return; @@ -55,13 +55,12 @@ export default class SplitParagraphCommand extends Command { i += 1; }); - // get paragraph type and position - const findParagraphClass = p => [...p.classList].find(c => /^paragraph-type/.test(c)); - const paragraph = sourceElement.closest('[class*="paragraph-type--"]'); - const paragraphType = findParagraphClass(paragraph).replace('paragraph-type--','').replace('-', '_'); - const paragraphDelta = [...paragraph.parentNode.children].filter(findParagraphClass).indexOf(paragraph) + 1; + // Get paragraph type and position. + const paragraph = sourceElement.closest('.paragraphs-subform').closest('tr'); + const paragraphType = paragraph.querySelector('[data-paragraphs-split-text-type]').dataset.paragraphsSplitTextType; + const paragraphDelta = [...paragraph.parentNode.children].filter(el => el.querySelector('.paragraphs-subform')).indexOf(paragraph) + 1; - // store the value of the paragraphs + // Store the value of the paragraphs. const firstData = nodeArrayToHTML(elementsBefore); const secondData = nodeArrayToHTML(elementsAfter); window._splitParagraph = { @@ -72,9 +71,9 @@ export default class SplitParagraphCommand extends Command { selector: sourceElement.dataset.drupalSelector, }; - // add new paragragraph after current - document.querySelector('input.paragraph-type-add-delta.modal').value = paragraphDelta; - document.getElementsByName(`field_paragraphs_${paragraphType}_add_more`)[0].dispatchEvent(new Event('mousedown')); + // Add new paragragraph after current. + sourceElement.closest('.paragraphs-container').querySelector('input.paragraph-type-add-delta.modal').value = paragraphDelta; + sourceElement.closest('.paragraphs-container').querySelector(`input[data-paragraph-type="${paragraphType}"].field-add-more-submit`).dispatchEvent(new Event('mousedown')); } refresh() { @@ -86,7 +85,7 @@ export default class SplitParagraphCommand extends Command { const nestedSplitter = (n) => { if (n.nodeType === Node.TEXT_NODE) { - // split position within text node + // Split position within text node. if (n.data.length > splitAt - characterCount) { const textBeforeSplit = n.data.substring(0, splitAt - characterCount); const textAfterSplit = n.data.substring(splitAt - characterCount); @@ -104,7 +103,7 @@ export default class SplitParagraphCommand extends Command { const childNodesBefore = []; const childNodesAfter = []; n.childNodes.forEach((childNode) => { - // split not yet reached + // Split not yet reached. if (childNodesAfter.length === 0) { const [childNodeBefore, childNodeAfter] = nestedSplitter(childNode); @@ -120,7 +119,7 @@ export default class SplitParagraphCommand extends Command { } }); - // node was not split + // Node was not split. if (childNodesAfter.length === 0) { return [n, null]; } diff --git a/paragraphs_features.module b/paragraphs_features.module index 5c0575f..a4a671a 100644 --- a/paragraphs_features.module +++ b/paragraphs_features.module @@ -46,6 +46,19 @@ function paragraphs_features_field_widget_third_party_settings_form(WidgetInterf * Implements hook_paragraphs_widget_actions_alter(). */ function paragraphs_features_paragraphs_widget_actions_alter(array &$widget_actions, array &$context) { + /** @var \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity */ + $paragraphs_entity = $context['paragraphs_entity']; + + foreach ($widget_actions as $grouping => $buttons) { + foreach ($buttons as $button_id => $button_element) { + if ($button_id === 'remove_button') { + $widget_actions[$grouping][$button_id]['#attributes']['data-paragraphs-split-text-type'] = $paragraphs_entity->getType(); + + break 2; + } + } + } + /* Render single option for dropdown as button. */ // Get configuration setting for reducing dropdown to button on single option. From 84afd93db92dc5328267512cb9627fba00f87127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20P=C3=A1ll?= Date: Tue, 28 Nov 2023 16:31:42 +0100 Subject: [PATCH 9/9] Re-implement splitting using a marker --- js/build/split_paragraph.js | 2 +- .../split_paragraph/src/index.js | 2 +- .../src/splitparagraphcommand.js | 85 ++++++------------- 3 files changed, 29 insertions(+), 60 deletions(-) diff --git a/js/build/split_paragraph.js b/js/build/split_paragraph.js index 5272875..71c3fad 100644 --- a/js/build/split_paragraph.js +++ b/js/build/split_paragraph.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.split_paragraph=t())}(self,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,r)=>{e.exports=r("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(e,t,r)=>{e.exports=r("dll-reference CKEditor5.dll")("./src/ui.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function r(o){var a=t[o];if(void 0!==a)return a.exports;var s=t[o]={exports:{}};return e[o](s,s.exports,r),s.exports}r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var o={};return(()=>{"use strict";r.d(o,{default:()=>i});var e=r("ckeditor5/src/core.js"),t=r("ckeditor5/src/ui.js");class a extends e.Command{execute(){const{model:e,sourceElement:t}=this.editor,[r,o]=e.document.selection.getFirstPosition().path,s=(new DOMParser).parseFromString(this.editor.getData(),"text/html").body.children;if(0===r&&0===o)return;if(0===s.length)return;const i=s[s.length-1].innerText.replace(String.fromCharCode(160),"").length;if(r===s.length-1&&o===i)return;const n=[],l=[],d=e=>e.reduce(((e,t)=>e+t.outerHTML),"");let p=0;Array.from(s).forEach((e=>{if(pr&&l.push(e),p+=1}));const c=t.closest(".paragraphs-subform").closest("tr"),h=c.querySelector("[data-paragraphs-split-text-type]").dataset.paragraphsSplitTextType,u=[...c.parentNode.children].filter((e=>e.querySelector(".paragraphs-subform"))).indexOf(c)+1,g=d(n),f=d(l);window._splitParagraph={data:{first:g,second:f},selector:t.dataset.drupalSelector},t.closest(".paragraphs-container").querySelector("input.paragraph-type-add-delta.modal").value=u,t.closest(".paragraphs-container").querySelector(`input[data-paragraph-type="${h}"].field-add-more-submit`).dispatchEvent(new Event("mousedown"))}refresh(){this.isEnabled=!0}static splitNode(e,t){let r=0;const o=e=>{if(e.nodeType===Node.TEXT_NODE){if(e.data.length>t-r){const o=e.data.substring(0,t-r),a=e.data.substring(t-r);return[o?document.createTextNode(o):null,a?document.createTextNode(a):null]}return r+=e.data.length,[e,null]}const a=[],s=[];if(e.childNodes.forEach((e=>{if(0===s.length){const[t,r]=o(e);t&&a.push(t),r&&s.push(r)}else s.push(e)})),0===s.length)return[e,null];const i=e.cloneNode(),n=e.cloneNode();return a.forEach((e=>{i.appendChild(e)})),s.forEach((e=>{n.appendChild(e)})),[i.childNodes.length>0?i:null,n.childNodes.length>0?n:null]};return o(e)}}class s extends e.Plugin{init(){null!=this.editor.sourceElement.closest(".paragraphs-container").querySelector("input.paragraph-type-add-delta.modal")&&(this.editor.ui.componentFactory.add("splitParagraph",(e=>{const r=this.editor.commands.get("splitParagraph"),o=new t.ButtonView(e);return o.set({label:this.editor.t("Simple Split Paragraph"),icon:'\r\n\r\n\r\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --\x3e\r\n\r\n\t\r\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>this.editor.execute("splitParagraph"))),o})),this.editor.commands.add("splitParagraph",new a(this.editor)))}afterInit(){if(window._splitParagraph){if("string"==typeof window._splitParagraph.data.second){const e=this.editor.sourceElement.closest(".paragraphs-subform").closest("tr"),t=e?.previousElementSibling?.previousElementSibling;t&&t.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.second),window._splitParagraph.data.second=null}),0)}"string"==typeof window._splitParagraph.data.first&&this.editor.sourceElement.dataset.drupalSelector===window._splitParagraph.selector&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.first),window._splitParagraph.data.first=null}),0)}}}const i={SplitParagraph:s}})(),o.default})())); \ No newline at end of file +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.split_paragraph=e())}(self,(()=>(()=>{var t={"ckeditor5/src/core.js":(t,e,r)=>{t.exports=r("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(t,e,r)=>{t.exports=r("dll-reference CKEditor5.dll")("./src/ui.js")},"dll-reference CKEditor5.dll":t=>{"use strict";t.exports=CKEditor5.dll}},e={};function r(o){var a=e[o];if(void 0!==a)return a.exports;var s=e[o]={exports:{}};return t[o](s,s.exports,r),s.exports}r.d=(t,e)=>{for(var o in e)r.o(e,o)&&!r.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var o={};return(()=>{"use strict";r.d(o,{default:()=>i});var t=r("ckeditor5/src/core.js"),e=r("ckeditor5/src/ui.js");class a extends t.Command{execute(){const{model:t,sourceElement:e}=this.editor,r=Drupal.t("[Splitting in progress... ⌛]"),o=this.editor.getData();t.change((e=>{e.insertText(r,t.document.selection.getFirstPosition())}));const s=(new DOMParser).parseFromString(this.editor.getData(),"text/html").body,[i,n,d]=a.splitNode(s,r);if(!d||!i||!n)return void this.editor.setData(o);const l=e.closest(".paragraphs-subform").closest("tr"),p=l.querySelector("[data-paragraphs-split-text-type]").dataset.paragraphsSplitTextType,c=[...l.parentNode.children].filter((t=>t.querySelector(".paragraphs-actions"))).indexOf(l)+1;window._splitParagraph={data:{first:i.outerHTML,second:n.outerHTML},selector:e.dataset.drupalSelector},e.closest(".paragraphs-container").querySelector("input.paragraph-type-add-delta.modal").value=c,e.closest(".paragraphs-container").querySelector(`input[data-paragraph-type="${p}"].field-add-more-submit`).dispatchEvent(new Event("mousedown"))}refresh(){this.isEnabled=!0}static splitNode(t,e){const r=t=>{if(t.nodeType===Node.TEXT_NODE){const r=t.data.indexOf(e);if(r>=0){const o=t.data.substring(0,r),a=t.data.substring(r+e.length);return[o?document.createTextNode(o):null,a?document.createTextNode(a):null,!0]}return[t,null,!1]}const o=[],a=[];let s=!1;if(t.childNodes.forEach((t=>{if(s)a.push(t);else{const[e,i,n]=r(t);s=n,e&&o.push(e),i&&a.push(i)}})),!s)return[t,null,!1];const i=t.cloneNode(),n=t.cloneNode();return o.forEach((t=>{i.appendChild(t)})),a.forEach((t=>{n.appendChild(t)})),[i.childNodes.length>0?i:null,n.childNodes.length>0?n:null,s]};return r(t)}}class s extends t.Plugin{init(){null!=this.editor.sourceElement.closest(".paragraphs-container")?.querySelector("input.paragraph-type-add-delta.modal")&&(this.editor.ui.componentFactory.add("splitParagraph",(t=>{const r=this.editor.commands.get("splitParagraph"),o=new e.ButtonView(t);return o.set({label:this.editor.t("Simple Split Paragraph"),icon:'\r\n\r\n\r\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --\x3e\r\n\r\n\t\r\n',tooltip:!0}),o.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(o,"execute",(()=>this.editor.execute("splitParagraph"))),o})),this.editor.commands.add("splitParagraph",new a(this.editor)))}afterInit(){if(window._splitParagraph){if("string"==typeof window._splitParagraph.data.second){const t=this.editor.sourceElement.closest(".paragraphs-subform").closest("tr"),e=t?.previousElementSibling?.previousElementSibling;e&&e.querySelector(`[data-drupal-selector="${window._splitParagraph.selector}"]`)&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.second),window._splitParagraph.data.second=null}),0)}"string"==typeof window._splitParagraph.data.first&&this.editor.sourceElement.dataset.drupalSelector===window._splitParagraph.selector&&setTimeout((()=>{this.editor.setData(window._splitParagraph.data.first),window._splitParagraph.data.first=null}),0)}}}const i={SplitParagraph:s}})(),o.default})())); \ No newline at end of file diff --git a/js/ckeditor5_plugins/split_paragraph/src/index.js b/js/ckeditor5_plugins/split_paragraph/src/index.js index 459880a..f60ba28 100644 --- a/js/ckeditor5_plugins/split_paragraph/src/index.js +++ b/js/ckeditor5_plugins/split_paragraph/src/index.js @@ -11,7 +11,7 @@ import icon from '../../../../icons/split.svg'; class SplitParagraph extends Plugin { init() { // Only split paragraphs. - if (this.editor.sourceElement.closest('.paragraphs-container').querySelector('input.paragraph-type-add-delta.modal') == null) { + if (this.editor.sourceElement.closest('.paragraphs-container')?.querySelector('input.paragraph-type-add-delta.modal') == null) { return; } diff --git a/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js b/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js index b0f74c0..6d4b542 100644 --- a/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js +++ b/js/ckeditor5_plugins/split_paragraph/src/splitparagraphcommand.js @@ -8,65 +8,32 @@ import { Command } from 'ckeditor5/src/core'; export default class SplitParagraphCommand extends Command { execute() { const { model, sourceElement } = this.editor; - const [splitElement, splitText] = model.document.selection.getFirstPosition().path; - const elements = (new DOMParser()).parseFromString(this.editor.getData(), 'text/html').body.children; + // @todo Use an html node with display:none instead + const splitMarker = Drupal.t('[Splitting in progress... ⌛]'); + const originalText = this.editor.getData(); - // Cursor is at the start. - if (splitElement === 0 && splitText === 0) { - return; - } + model.change(writer => { + writer.insertText(splitMarker, model.document.selection.getFirstPosition()); + }); - // All lines are empty. - if (elements.length === 0) { - return; - } + const rootElement = (new DOMParser()).parseFromString(this.editor.getData(), 'text/html').body; + const [elementBefore, elementAfter, markerFound] = SplitParagraphCommand.splitNode(rootElement, splitMarker); - // Cursor is at the end. - const sanitizedInnerTextOfLastElement = elements[elements.length - 1].innerText.replace(String.fromCharCode(160), '').length; - if (splitElement === elements.length - 1 && splitText === sanitizedInnerTextOfLastElement) { + if (!markerFound || !elementBefore || !elementAfter) { + this.editor.setData(originalText); return; } - const elementsBefore = []; - const elementsAfter = []; - const nodeArrayToHTML = (nodeArray) => nodeArray.reduce((data, element) => data + element.outerHTML, ''); - - let i = 0; - Array.from(elements).forEach((el) => { - if (i < splitElement) { - elementsBefore.push(el); - } - - if (i === splitElement) { - const [nodeBefore, nodeAfter] = SplitParagraphCommand.splitNode(el, splitText); - - if (nodeBefore) { - elementsBefore.push(nodeBefore); - } - - if (nodeAfter) { - elementsAfter.push(nodeAfter); - } - } - - if (i > splitElement) { - elementsAfter.push(el); - } - i += 1; - }); - // Get paragraph type and position. const paragraph = sourceElement.closest('.paragraphs-subform').closest('tr'); const paragraphType = paragraph.querySelector('[data-paragraphs-split-text-type]').dataset.paragraphsSplitTextType; - const paragraphDelta = [...paragraph.parentNode.children].filter(el => el.querySelector('.paragraphs-subform')).indexOf(paragraph) + 1; + const paragraphDelta = [...paragraph.parentNode.children].filter(el => el.querySelector('.paragraphs-actions')).indexOf(paragraph) + 1; // Store the value of the paragraphs. - const firstData = nodeArrayToHTML(elementsBefore); - const secondData = nodeArrayToHTML(elementsAfter); window._splitParagraph = { data: { - first: firstData, - second: secondData, + first: elementBefore.outerHTML, + second: elementAfter.outerHTML, }, selector: sourceElement.dataset.drupalSelector, }; @@ -80,32 +47,33 @@ export default class SplitParagraphCommand extends Command { this.isEnabled = true; } - static splitNode(node, splitAt) { - let characterCount = 0; - + static splitNode(node, splitMarker) { const nestedSplitter = (n) => { if (n.nodeType === Node.TEXT_NODE) { // Split position within text node. - if (n.data.length > splitAt - characterCount) { - const textBeforeSplit = n.data.substring(0, splitAt - characterCount); - const textAfterSplit = n.data.substring(splitAt - characterCount); + 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, ]; } - characterCount += n.data.length; - return [n, null]; + return [n, null, false]; } const childNodesBefore = []; const childNodesAfter = []; + let found = false; n.childNodes.forEach((childNode) => { // Split not yet reached. - if (childNodesAfter.length === 0) { - const [childNodeBefore, childNodeAfter] = nestedSplitter(childNode); + if (!found) { + const [childNodeBefore, childNodeAfter, markerFound] = nestedSplitter(childNode); + found = markerFound; if (childNodeBefore) { childNodesBefore.push(childNodeBefore); @@ -120,8 +88,8 @@ export default class SplitParagraphCommand extends Command { }); // Node was not split. - if (childNodesAfter.length === 0) { - return [n, null]; + if (!found) { + return [n, null, false]; } const nodeBefore = n.cloneNode(); @@ -138,6 +106,7 @@ export default class SplitParagraphCommand extends Command { return [ nodeBefore.childNodes.length > 0 ? nodeBefore : null, nodeAfter.childNodes.length > 0 ? nodeAfter : null, + found, ]; };