diff --git a/MAINTENANCE.md b/MAINTENANCE.md index d79e05ac66aa148fc008eb148bc433fada495cba..1f486a9befcefa8b359929730e35b3ec2f509e49 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -1,2 +1,2 @@ # Edit+ Maintenanc -Edit+ includes [CKEditors Inline Editor](https://ckeditor.com/docs/ckeditor5/latest/examples/builds/inline-editor.html). Update the library to match Drupal Core's version by running `yarn vender-update` +Edit+ includes [CKEditors Inline Editor](https://ckeditor.com/docs/ckeditor5/latest/examples/builds/inline-editor.html). Update the library to match Drupal Core's version by running `yarn vendor-update` diff --git a/README.md b/README.md index d37777656baf71a90b14e38072b662ceb521e8f2..85e9b2bacf84c3daf34966ceb4bd2edd557a069f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,30 @@ -## Edit + +# Edit + +# Troubleshooting +Edit+ works by loading the relevant entity form into the sidebar as a "context" sidebar. The form items are hidden initially, but when an editable element is clicked on the page the form item is swapped into place for inline editing. + +### Associating the Form Item with the Page element +This should happen automatically for most field items. If there are custom things going on it needs to satisfy a few things. To make the UI aware of an editable page element it needs to end up with the following markup or provide its own Edit+ field widget JS plugin. There can of course be more to this markup, but these are whats required for inline editing. Please see `edit_plus_preprocess_field()` for details. +```html +<div data-edit-plus-field-value-wrapper="block_content::bc74a654-2ba6-447a-a8a0-497776cb2ba4::field_links_links::value"> + <div class="edit-plus-field-value" data-edit-plus-id="block_content::bc74a654-2ba6-447a-a8a0-497776cb2ba4::field_links_links::0::en::default::link::link::value" data-edit-plus-page-element-id="block_content::bc74a654-2ba6-447a-a8a0-497776cb2ba4::field_links_links::0::value"> + <a href="https://www.puvaphesw.gov">https://www.puvaphesw.gov</a> + </div> +</div> +``` +To relate the above editable page element to the form item that was loaded when it was clicked we need the following markup on the form item. +```html +<!-- @todo add an example 😅 --> +``` + +### Inline Editing z-index +If the inline editing z-index is undesirable for your theme override it with. Since the inline editor is visually moved out of the sidebar, it inherits whatever the sidebar z-index is. +```html +.toolbar-plus-sidebar { + z-index: 2; +} +``` + +### Pop out width +Say you have an inline editable element, like a paragraph or a media item, and the form item widget is too big to fit inline. You can form alter a `data-pop-out-width=618` attribute onto the form item wrapper and when ever the space to edit in is smaller than the form item will show up in a pop out. -### diff --git a/assets/pencil-cursor.svg b/assets/pencil-cursor.svg new file mode 100644 index 0000000000000000000000000000000000000000..ef98c02f86d35add78c0be5c11b3eba605572cf0 --- /dev/null +++ b/assets/pencil-cursor.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="#000000" viewBox="0 0 256 256"><path transform="rotate(90, 120, 120)" d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg> diff --git a/assets/pencil.svg b/assets/pencil.svg new file mode 100644 index 0000000000000000000000000000000000000000..278113c374a7b3814cb3c38bded009b0a47a1a59 --- /dev/null +++ b/assets/pencil.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 256 256"><path fill="currentColor" d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM51.31,160,136,75.31,152.69,92,68,176.68ZM48,179.31,76.69,208H48Zm48,25.38L79.31,188,164,103.31,180.69,120Zm96-96L147.31,64l24-24L216,84.68Z"></path></svg> diff --git a/assets/text-toolbar.svg b/assets/text-toolbar.svg deleted file mode 100644 index 8288c30ed220570ee36f46db8bfa42f17800e60b..0000000000000000000000000000000000000000 --- a/assets/text-toolbar.svg +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg id="pencil" xmlns="http://www.w3.org/2000/svg" width="29.85" height="34.88" viewBox="0 0 19.85 25.88"> - <defs> - <style> - .cls-1 { - stroke: #000; - } - - .cls-1, .cls-2 { - stroke-miterlimit: 10; - } - - .cls-2 { - fill: #fff; - stroke: #fff; - } - </style> - </defs> - <g id="white"> - <rect class="cls-2" x="7.94" y="5.63" width="5.01" height="14.29" transform="translate(-5.54 8.68) rotate(-36.35)"/> - <polygon class="cls-2" points="7.42 4.92 3.84 7.55 2.83 2.46 7.42 4.92"/> - <rect class="cls-2" x="12.92" y="17.85" width="5" height="3.36" transform="translate(-8.57 12.94) rotate(-36.35)"/> - </g> - <g id="black"> - <rect class="cls-1" x="8.6" y="6.18" width="3.07" height="12.47" transform="translate(-5.39 8.43) rotate(-36.35)"/> - <polygon class="cls-1" points="6.32 5.19 4.42 6.59 3.87 3.86 6.32 5.19"/> - <rect class="cls-1" x="13.84" y="18.82" width="3.04" height="1.47" transform="translate(-8.6 12.91) rotate(-36.35)"/> - </g> -</svg> diff --git a/assets/vendor/ckeditor5/editor-inline.js b/assets/vendor/ckeditor5/editor-inline.js index 3bd7334f589b7a960cfffeec412bb68deb72b6c0..cc9e4ad9e4d9e6ebdcd6d8c47afc14196a41ab16 100644 --- a/assets/vendor/ckeditor5/editor-inline.js +++ b/assets/vendor/ckeditor5/editor-inline.js @@ -1,4 +1,4 @@ /*! - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. - */(()=>{var t={704:(t,e,o)=>{t.exports=o(79)("./src/core.js")},492:(t,e,o)=>{t.exports=o(79)("./src/engine.js")},273:(t,e,o)=>{t.exports=o(79)("./src/ui.js")},209:(t,e,o)=>{t.exports=o(79)("./src/utils.js")},434:(t,e,o)=>{t.exports=o(79)("./src/watchdog.js")},79:t=>{"use strict";t.exports=CKEditor5.dll}},e={};function o(i){var r=e[i];if(void 0!==r)return r.exports;var n=e[i]={exports:{}};return t[i](n,n.exports,o),n.exports}o.d=(t,e)=>{for(var i in e)o.o(e,i)&&!o.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},o.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),o.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var i={};(()=>{"use strict";o.r(i),o.d(i,{InlineEditor:()=>D});var t=o(704),e=o(209),r=o(434),n=o(273),s=o(492);class l extends n.EditorUI{constructor(t,e){super(t),this.view=e,this._toolbarConfig=(0,n.normalizeToolbarConfig)(t.config.get("toolbar"))}get element(){return this.view.editable.element}init(){const t=this.editor,e=this.view,o=t.editing.view,i=e.editable,r=o.document.getRoot();i.name=r.rootName,e.render();const n=i.element;this.setEditableElement(i.name,n),i.bind("isFocused").to(this.focusTracker),o.attachDomRoot(n),this._initPlaceholder(),this._initToolbar(),this.fire("ready")}destroy(){super.destroy();const t=this.view;this.editor.editing.view.detachDomRoot(t.editable.name),t.destroy()}_initToolbar(){const t=this.editor,e=this.view,o=e.editable.element,i=e.toolbar;e.panel.bind("isVisible").to(this.focusTracker,"isFocused"),e.bind("viewportTopOffset").to(this,"viewportOffset",(({top:t})=>t||0)),e.listenTo(t.ui,"update",(()=>{e.panel.isVisible&&e.panel.pin({target:o,positions:e.panelPositions})})),i.fillFromConfig(this._toolbarConfig,this.componentFactory),this.addToolbar(i)}_initPlaceholder(){const t=this.editor,e=t.editing.view,o=e.document.getRoot(),i=t.config.get("placeholder");if(i){const t="string"==typeof i?i:i[o.rootName];t&&(o.placeholder=t)}(0,s.enablePlaceholder)({view:e,element:o,isDirectHost:!1,keepOnFocus:!0})}}const a=(0,e.toUnit)("px");class c extends n.EditorUIView{constructor(t,e,o,i={}){super(t);const r=t.t;this.toolbar=new n.ToolbarView(t,{shouldGroupWhenFull:i.shouldToolbarGroupWhenFull,isFloating:!0}),this.set("viewportTopOffset",0),this.panel=new n.BalloonPanelView(t),this.panelPositions=this._getPanelPositions(),this.panel.extendTemplate({attributes:{class:"ck-toolbar-container"}}),this.editable=new n.InlineEditableUIView(t,e,o,{label:t=>r("Rich Text Editor. Editing area: %0",t.name)}),this._resizeObserver=null}render(){super.render(),this.body.add(this.panel),this.registerChild(this.editable),this.panel.content.add(this.toolbar);if(this.toolbar.options.shouldGroupWhenFull){const t=this.editable.element;this._resizeObserver=new e.ResizeObserver(t,(()=>{this.toolbar.maxWidth=a(new e.Rect(t).width)}))}}destroy(){super.destroy(),this._resizeObserver&&this._resizeObserver.destroy()}_getPanelPositionTop(t,e){let o;return o=t.top>e.height+this.viewportTopOffset?t.top-e.height:t.bottom>e.height+this.viewportTopOffset+50?this.viewportTopOffset:t.bottom,o}_getPanelPositions(){const t=[(t,e)=>({top:this._getPanelPositionTop(t,e),left:t.left,name:"toolbar_west",config:{withArrow:!1}}),(t,e)=>({top:this._getPanelPositionTop(t,e),left:t.left+t.width-e.width,name:"toolbar_east",config:{withArrow:!1}})];return"ltr"===this.locale.uiLanguageDirection?t:t.reverse()}}const d=function(t){return null!=t&&"object"==typeof t};const h="object"==typeof global&&global&&global.Object===Object&&global;var u="object"==typeof self&&self&&self.Object===Object&&self;const p=(h||u||Function("return this")()).Symbol;var b=Object.prototype,f=b.hasOwnProperty,g=b.toString,w=p?p.toStringTag:void 0;const v=function(t){var e=f.call(t,w),o=t[w];try{t[w]=void 0;var i=!0}catch(t){}var r=g.call(t);return i&&(e?t[w]=o:delete t[w]),r};var m=Object.prototype.toString;const y=function(t){return m.call(t)};var O=p?p.toStringTag:void 0;const T=function(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":O&&O in Object(t)?v(t):y(t)};const j=function(t,e){return function(o){return t(e(o))}}(Object.getPrototypeOf,Object);var E=Function.prototype,P=Object.prototype,x=E.toString,_=P.hasOwnProperty,F=x.call(Object);const C=function(t){if(!d(t)||"[object Object]"!=T(t))return!1;var e=j(t);if(null===e)return!0;var o=_.call(e,"constructor")&&e.constructor;return"function"==typeof o&&o instanceof o&&x.call(o)==F};const S=function(t){return d(t)&&1===t.nodeType&&!C(t)};class D extends((0,t.DataApiMixin)((0,t.ElementApiMixin)(t.Editor))){constructor(o,i={}){if(!W(o)&&void 0!==i.initialData)throw new e.CKEditorError("editor-create-initial-data",null);super(i),void 0===this.config.get("initialData")&&this.config.set("initialData",function(t){return W(t)?(0,e.getDataFromElement)(t):t}(o)),this.model.document.createRoot(),W(o)&&(this.sourceElement=o,(0,t.secureSourceElement)(this,o));const r=!this.config.get("toolbar.shouldNotGroupWhenFull"),n=new c(this.locale,this.editing.view,this.sourceElement,{shouldToolbarGroupWhenFull:r});this.ui=new l(this,n),(0,t.attachToForm)(this)}destroy(){const t=this.getData();return this.ui.destroy(),super.destroy().then((()=>{this.sourceElement&&this.updateSourceElement(t)}))}static create(t,o={}){return new Promise((i=>{if(W(t)&&"TEXTAREA"===t.tagName)throw new e.CKEditorError("editor-wrong-element",null);const r=new this(t,o);i(r.initPlugins().then((()=>r.ui.init())).then((()=>r.data.init(r.config.get("initialData")))).then((()=>r.fire("ready"))).then((()=>r)))}))}}function W(t){return S(t)}D.Context=t.Context,D.EditorWatchdog=r.EditorWatchdog,D.ContextWatchdog=r.ContextWatchdog})(),(window.CKEditor5=window.CKEditor5||{}).editorInline=i})(); \ No newline at end of file + */(()=>{var t={782:(t,e,o)=>{t.exports=o(237)("./src/core.js")},783:(t,e,o)=>{t.exports=o(237)("./src/engine.js")},311:(t,e,o)=>{t.exports=o(237)("./src/ui.js")},584:(t,e,o)=>{t.exports=o(237)("./src/utils.js")},602:(t,e,o)=>{t.exports=o(237)("./src/watchdog.js")},237:t=>{"use strict";t.exports=CKEditor5.dll}},e={};function o(i){var r=e[i];if(void 0!==r)return r.exports;var n=e[i]={exports:{}};return t[i](n,n.exports,o),n.exports}o.d=(t,e)=>{for(var i in e)o.o(e,i)&&!o.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},o.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),o.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var i={};(()=>{"use strict";o.r(i),o.d(i,{InlineEditor:()=>W});var t=o(782),e=o(584),r=o(602),n=o(311),s=o(783);class l extends n.EditorUI{constructor(t,e){super(t),this.view=e,this._toolbarConfig=(0,n.normalizeToolbarConfig)(t.config.get("toolbar"))}get element(){return this.view.editable.element}init(){const t=this.editor,e=this.view,o=t.editing.view,i=e.editable,r=o.document.getRoot();i.name=r.rootName,e.render();const n=i.element;this.setEditableElement(i.name,n),i.bind("isFocused").to(this.focusTracker),o.attachDomRoot(n),this._initPlaceholder(),this._initToolbar(),this.fire("ready")}destroy(){super.destroy();const t=this.view;this.editor.editing.view.detachDomRoot(t.editable.name),t.destroy()}_initToolbar(){const t=this.editor,e=this.view,o=e.editable.element,i=e.toolbar;e.panel.bind("isVisible").to(this.focusTracker,"isFocused"),e.bind("viewportTopOffset").to(this,"viewportOffset",(({top:t})=>t||0)),e.listenTo(t.ui,"update",(()=>{e.panel.isVisible&&e.panel.pin({target:o,positions:e.panelPositions})})),i.fillFromConfig(this._toolbarConfig,this.componentFactory),this.addToolbar(i)}_initPlaceholder(){const t=this.editor,e=t.editing.view,o=e.document.getRoot(),i=t.config.get("placeholder");if(i){const t="string"==typeof i?i:i[o.rootName];t&&(o.placeholder=t)}(0,s.enablePlaceholder)({view:e,element:o,isDirectHost:!1,keepOnFocus:!0})}}const a=(0,e.toUnit)("px");class c extends n.EditorUIView{constructor(t,e,o,i={}){super(t);const r=t.t;this.toolbar=new n.ToolbarView(t,{shouldGroupWhenFull:i.shouldToolbarGroupWhenFull,isFloating:!0}),this.set("viewportTopOffset",0),this.panel=new n.BalloonPanelView(t),this.panelPositions=this._getPanelPositions(),this.panel.extendTemplate({attributes:{class:"ck-toolbar-container"}}),this.editable=new n.InlineEditableUIView(t,e,o,{label:t=>r("Rich Text Editor. Editing area: %0",t.name)}),this._resizeObserver=null}render(){super.render(),this.body.add(this.panel),this.registerChild(this.editable),this.panel.content.add(this.toolbar);if(this.toolbar.options.shouldGroupWhenFull){const t=this.editable.element;this._resizeObserver=new e.ResizeObserver(t,(()=>{this.toolbar.maxWidth=a(new e.Rect(t).width)}))}}destroy(){super.destroy(),this._resizeObserver&&this._resizeObserver.destroy()}_getPanelPositionTop(t,e){let o;return o=t.top>e.height+this.viewportTopOffset?t.top-e.height:t.bottom>e.height+this.viewportTopOffset+50?this.viewportTopOffset:t.bottom,o}_getPanelPositions(){const t=[(t,e)=>({top:this._getPanelPositionTop(t,e),left:t.left,name:"toolbar_west",config:{withArrow:!1}}),(t,e)=>({top:this._getPanelPositionTop(t,e),left:t.left+t.width-e.width,name:"toolbar_east",config:{withArrow:!1}})];return"ltr"===this.locale.uiLanguageDirection?t:t.reverse()}}const d=function(t){return null!=t&&"object"==typeof t};const h="object"==typeof global&&global&&global.Object===Object&&global;var u="object"==typeof self&&self&&self.Object===Object&&self;const p=(h||u||Function("return this")()).Symbol;var b=Object.prototype,f=b.hasOwnProperty,g=b.toString,w=p?p.toStringTag:void 0;const v=function(t){var e=f.call(t,w),o=t[w];try{t[w]=void 0;var i=!0}catch(t){}var r=g.call(t);return i&&(e?t[w]=o:delete t[w]),r};var m=Object.prototype.toString;const y=function(t){return m.call(t)};var O=p?p.toStringTag:void 0;const T=function(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":O&&O in Object(t)?v(t):y(t)};const j=function(t,e){return function(o){return t(e(o))}}(Object.getPrototypeOf,Object);var E=Function.prototype,P=Object.prototype,x=E.toString,_=P.hasOwnProperty,F=x.call(Object);const C=function(t){if(!d(t)||"[object Object]"!=T(t))return!1;var e=j(t);if(null===e)return!0;var o=_.call(e,"constructor")&&e.constructor;return"function"==typeof o&&o instanceof o&&x.call(o)==F};const S=function(t){return d(t)&&1===t.nodeType&&!C(t)};class D extends((0,t.ElementApiMixin)(t.Editor)){constructor(o,i={}){if(!R(o)&&void 0!==i.initialData)throw new e.CKEditorError("editor-create-initial-data",null);super(i),void 0===this.config.get("initialData")&&this.config.set("initialData",function(t){return R(t)?(0,e.getDataFromElement)(t):t}(o)),this.model.document.createRoot(),R(o)&&(this.sourceElement=o,(0,t.secureSourceElement)(this,o));const r=!this.config.get("toolbar.shouldNotGroupWhenFull"),n=new c(this.locale,this.editing.view,this.sourceElement,{shouldToolbarGroupWhenFull:r});this.ui=new l(this,n),(0,t.attachToForm)(this)}destroy(){const t=this.getData();return this.ui.destroy(),super.destroy().then((()=>{this.sourceElement&&this.updateSourceElement(t)}))}static create(t,o={}){return new Promise((i=>{if(R(t)&&"TEXTAREA"===t.tagName)throw new e.CKEditorError("editor-wrong-element",null);const r=new this(t,o);i(r.initPlugins().then((()=>r.ui.init())).then((()=>r.data.init(r.config.get("initialData")))).then((()=>r.fire("ready"))).then((()=>r)))}))}}D.Context=t.Context,D.EditorWatchdog=r.EditorWatchdog,D.ContextWatchdog=r.ContextWatchdog;const W=D;function R(t){return S(t)}})(),(window.CKEditor5=window.CKEditor5||{}).editorInline=i})(); \ No newline at end of file diff --git a/css/bottom-bar.css b/css/bottom-bar.css index 563b93aa908a378f0961374d8ff58377277f0a67..dfbe272a17e77ad6cd0a48850c393ed2832d35fe 100644 --- a/css/bottom-bar.css +++ b/css/bottom-bar.css @@ -1,9 +1,9 @@ #edit-plus-bottom-bar { background-color: black; align-items: center; + padding: 0.5em 1em; position: fixed; flex-wrap: wrap; - padding: 0.5em 1em; display: flex; z-index: 500; bottom: 0; @@ -14,13 +14,13 @@ #edit-plus-bottom-bar .button { background-color: black; + text-decoration: none; + margin: 0 1em 0 0; font-size: 18px; + cursor: pointer; color: white; border: none; padding: 1em; - text-decoration: none; - margin: 0 1em 0 0; - cursor: pointer; } #edit-plus-bottom-bar .button:hover { diff --git a/css/ckeditor.css b/css/ckeditor.css index 36c57b98a19f9a534905860966ade56ff4c59aef..df2b433d58197085d486cd3bc94532d4509a7337 100644 --- a/css/ckeditor.css +++ b/css/ckeditor.css @@ -1,12 +1,26 @@ -:not(form) .edit-plus-form-item { - margin: 0; +.edit-plus-form-item .ck-content { + min-height: auto; } -:not(form) .edit-plus-inline-edit.ck.ck-blurred { - padding-left: 0; +.ck-body-wrapper .ck.ck-balloon-panel { + z-index: 2; } -:not(form) .edit-plus-inline-edit.ck.ck-blurred > :first-child, -:not(form) .edit-plus-inline-edit.ck.ck-blurred > :last-child { - margin-top: inherit; +/* The source plugin isn't supported in InlineEditor */ +.ck.ck-button.ck-source-editing-button { + display: none; +} +/* Still allow it in ClassicEditor's though. */ +.edit-plus-form-item .ck.ck-button.ck-source-editing-button { + display: flex; +} + +/* +Fix Inline CKEditor in Safari not focusing because of admin-reset-style.css + :where([data-drupal-admin-styles] :not(svg *)) { + all: revert; + } + */ +[data-drupal-admin-styles] .ck * { + -webkit-user-modify: read-write; } diff --git a/css/edit-plus.css b/css/edit-plus.css index 7121c5919389fb4c49f3e1ae345cb9972a3ae976..0817ad430b74ee6fe137d00166bbe3e6d746f299 100644 --- a/css/edit-plus.css +++ b/css/edit-plus.css @@ -1,13 +1,17 @@ .edit-plus-hidden, input.edit-plus-hidden, div.edit-plus-hidden { - visibility: hidden !important; + visibility: hidden; position: absolute; + /* Reveal elements being edited */ + .edit-plus-editing { + visibility: visible; + } } #edit_plus { position: fixed; - z-index: 999999999999; + z-index: 10; margin-left: 50%; cursor: pointer; padding: 1em; @@ -16,6 +20,20 @@ div.edit-plus-hidden { color: #000; } +.sized-placeholder { + width: auto; +} + +.form-item-popout { + border: 1px solid var(--admin-toolbar-color-gray-100); + background-color: var(--admin-toolbar-color-white); + padding: 10px; + box-shadow: + 0 0 72px rgba(0, 0, 0, 0.2), + 0 0 8px rgba(0, 0, 0, 0.04), + 0 0 40px rgba(0, 0, 0, 0.06); +} + .ck-powered-by-balloon * { display: none; } @@ -34,3 +52,39 @@ div.edit-plus-hidden { from { background-color: rgba(121, 189, 143, 1)} to { background-color: rgba(121, 189, 143, 0)} } + +/* Inline Element update AJAX throbber */ +.disabled-updating { + position: relative; + + .ajax-progress { + position: absolute; + user-select: none; + display: flex; + bottom: 0; + top: 0; + left: 0; + right: 0; + opacity: 0; + font-size: 1rem; + font-weight: 300; + align-items: center; + justify-content: center; + transition: opacity .15s ease-out; + + .throbber, + .message { + display: block; + } + } +} +.element-ajax-throbbing { + /* Disable the forms submit handlers throbber when element throbber is active. */ + .ajax-progress { + display: none; + } + /* Still show the element throbber though. */ + .disabled-updating .ajax-progress { + display: flex; + } +} diff --git a/edit_plus.libraries.yml b/edit_plus.libraries.yml index 983b2abf17743bc43d6b23bf5bbffe46fb2b4690..537ee1e67ad095d5ba43819ad89bd8f73600aaf9 100644 --- a/edit_plus.libraries.yml +++ b/edit_plus.libraries.yml @@ -2,6 +2,7 @@ library: js: assets/vendor/ckeditor5/editor-inline.js: { minified: true } js/edit-plus.js: { attributes: { type: 'module' }} + src/Plugin/Tool/edit-plus.js: { attributes: { type: 'module' }} css: component: css/bottom-bar.css: {} @@ -12,11 +13,12 @@ library: - core/jquery - core/ckeditor5 - core/ckeditor5.basic + - toolbar_plus/toolbar_plus - core/ckeditor5.essentials - core/ckeditor5.autoformat - core/ckeditor5.htmlSupport - core/ckeditor5.sourceEditing - - core/ckeditor5.pasteFromOffice' + - core/ckeditor5.pasteFromOffice - ckeditor5/internal.drupal.ckeditor5 - ckeditor5/internal.drupal.ckeditor5.emphasis - ckeditor5/internal.drupal.ckeditor5.htmlEngine diff --git a/edit_plus.module b/edit_plus.module index 49e7e995a0f636d3aa090af5c9ffd6d2fd51251b..8bc19d0cf8553bfe912448607391fac614d120fe 100644 --- a/edit_plus.module +++ b/edit_plus.module @@ -8,10 +8,8 @@ use Drupal\block_content\Entity\BlockContent; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Template\Attribute; -use Drupal\edit_plus\Controller\MultipleEntityFormController; /** * Implements hook_page_attachments(). @@ -25,6 +23,7 @@ function edit_plus_page_attachments(array &$attachments) { if (\Drupal::service('router.admin_context')->isAdminRoute($route)) { return; } + // Make the filter formats available on editable pages (currently all non-admin pages). $attachments['#cache']['contexts'][] = 'user.permissions'; $formats = Drupal::service('entity_type.manager')->getStorage('filter_format')->loadMultiple(); $editor_attachments = \Drupal::service('plugin.manager.editor')->getAttachments(array_keys($formats)); @@ -53,11 +52,6 @@ function edit_plus_preprocess_field(&$variables) { } } - // Edit+ only supports view modes, not dynamically defined - // "display options" (which \Drupal\Core\Field\FieldItemListInterface::view() - // always names the "_custom" view mode). - // @see \Drupal\Core\Field\FieldItemListInterface::view() - // @see https://www.drupal.org/node/2120335 $view_mode = $element['#view_mode']; if ($view_mode === '_custom') { // Support layout builder field blocks. @@ -65,39 +59,30 @@ function edit_plus_preprocess_field(&$variables) { $view_mode = $element['#third_party_settings']['layout_builder']['view_mode']; } else { + // Layout Builder (and Edit+) only supports view modes, not dynamically defined + // "display options" (which \Drupal\Core\Field\FieldItemListInterface::view() + // always names the "_custom" view mode). + // @see \Drupal\Core\Field\FieldItemListInterface::view() + // @see https://www.drupal.org/node/2120335 return; } } - + // Get the actual view mode. + $view_mode = _toolbar_plus_get_view_mode($entity, $view_mode); $definition = $entity->getFieldDefinition($element['#field_name']); $main_property = method_exists($definition, 'getMainPropertyName') ? $definition->getMainPropertyName() : 'value'; $id = edit_plus_entity_identifier($entity); $variables['attributes']['data-edit-plus-field-value-wrapper'] = sprintf('%s::%s::%s::%s', $entity->getEntityTypeId(), $id, $element['#field_name'], $main_property); - // Consult the form used to control this display. -// static $forms; -// if (empty($forms)) { -// $forms = []; -// } -// $key = $entity->getEntityTypeId() . '::' . $entity->id(); -// if (empty($forms[$key])) { -// $form = Drupal::classResolver()->getInstanceFromDefinition(MultipleEntityFormController::class)->entityForm(Drupal::requestStack()->getCurrentRequest(), $entity->getEntityTypeId(), $entity); -// $forms[$key] = $form; -// } else { -// $form = $forms[$key]; -// } - foreach ($variables['items'] as $delta => &$item) { -// $type = edit_plus_find_widget_type($form['form'][$element['#field_name']], $definition); $item['content'] = [ 'edit_plus_wrapper' => [ '#type' => 'container', '#attributes' => [ 'class' => ['edit-plus-field-value'], 'data-edit-plus-id' => sprintf('%s::%s::%s::%s::%s::%s::%s::%s::%s', $entity->getEntityTypeId(), $id, $element['#field_name'], $delta, $element['#language'], $view_mode, $element['#field_type'], $element['#formatter'], $main_property), -// 'data-edit-plus-id' => sprintf('%s::%s::%s::%s::%s::%s::%s::%s::%s', $entity->getEntityTypeId(), $id, $element['#field_name'], $delta, $element['#language'], $view_mode, $element['#field_type'], $type, $main_property), - 'data-edit-plus-markup-item-id' => sprintf('%s::%s::%s::%s::%s', $entity->getEntityTypeId(), $id, $element['#field_name'], $delta, $main_property), + 'data-edit-plus-page-element-id' => sprintf('%s::%s::%s::%s::%s', $entity->getEntityTypeId(), $id, $element['#field_name'], $delta, $main_property), ], 'content' => $item['content'], '#cache' => [ @@ -108,6 +93,32 @@ function edit_plus_preprocess_field(&$variables) { } } +/** + * Implements hook_preprocess_HOOK(). + */ +function edit_plus_preprocess_block(&$variables) { + $variables['#cache']['contexts'][] = 'user.permissions'; + if (!\Drupal::currentUser()->hasPermission('access inline editing') || $variables['base_plugin_id'] !== 'inline_block') { + return; + } + // Allow block labels to be edited inline. + // @see UpdateBlockForm + $entity = $variables['elements']['content']['#block_content']; + $id = edit_plus_entity_identifier($entity); + $view_mode = _toolbar_plus_get_view_mode($entity, $variables['content']['#view_mode']); + $variables['edit_plus'] = [ + 'id' => $id, + 'type' => $entity->getEntityTypeId(), + 'language' => $entity->language()->getId(), + 'view_mode' => $view_mode, + ]; + $variables['title_prefix']['#markup'] = sprintf('<div data-edit-plus-field-value-wrapper="%s::%s::label::block_property">', $entity->getEntityTypeId(), $id); + $variables['title_suffix']['#markup'] = '</div>'; + $variables['title_attributes']['class'][] = 'edit-plus-field-value'; + $variables['title_attributes']['data-edit-plus-id'] = sprintf('%s::%s::%s::%s::%s::%s::%s::%s::%s', $entity->getEntityTypeId(), $id, 'label', 0, $entity->language()->getId(), $view_mode, 'string', 'string', 'block_property'); + $variables['title_attributes']['data-edit-plus-page-element-id'] = sprintf('%s::%s::%s::%s::%s', $entity->getEntityTypeId(), $id, 'label', 0, 'block_property'); +} + function getCacheTag($entity) { return sprintf('edit_plus:%s.%s', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity)); } @@ -117,13 +128,20 @@ function getCacheTag($entity) { */ function edit_plus_form_alter(&$form, FormStateInterface $form_state, $form_id) { if ($form_state->get('edit_plus_form')) { - \Drupal::service('edit_plus.inline_entity_form')->formAlter($form, $form_state); + \Drupal::service('edit_plus.form_alter.inline_entity_form')->formAlter($form, $form_state); } else { - \Drupal::service('edit_plus.entity_edit_form_alter')->formAlter($form, $form_state); + \Drupal::service('edit_plus.form_alter.entity_edit')->formAlter($form, $form_state); } } +/** + * Implements hook_form_BASE_FORM_ID_alter(). + */ +function edit_plus_form_views_form_media_library_widget_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { + \Drupal::service('edit_plus.form_alter.views_form_media_library_widget')->formAlter($form, $form_state); +} + /** * Implements hook_module_implements_alter(). */ @@ -165,40 +183,6 @@ function template_preprocess_inline_textarea(&$variables) { $variables['inline_editor_attributes'] = new Attribute(['class' => ['edit-plus-inline-edit']]); } -/** - * Implements hook_page_bottom(). - */ -function edit_plus_page_bottom(array &$page_bottom) { - $page_bottom['bottom_bar'] = [ - '#type' => 'container', - '#attributes' => [ - 'id' => 'toolbar-plus-bottom-bar', - ], - ]; - - buildBottomBar($page_bottom); -} - -// @todo This should tie into toolbar +. -function buildBottomBar(array &$page_bottom) { - $actively_used_tempstore_entities = edit_plus_active_tempstore_entities(); - if (!empty($actively_used_tempstore_entities)) { - // Only build the bottom bar if we are on the canonical page of the tempstore - // entity. - $route = \Drupal::service('current_route_match'); - foreach ($actively_used_tempstore_entities as $tempstore_entity) { - $route_entity = $route->getParameter($tempstore_entity->getEntityTypeId()); - if (empty($route_entity) || (!empty($route_entity) && $route_entity->id() !== $tempstore_entity->id())) { - continue; - } - $canonical_route_name = $route_entity->toUrl('canonical')->getRouteName(); - if ($route->getRouteName() === $canonical_route_name) { - $page_bottom['bottom_bar']['edit_plus_bottom_bar'] = Drupal::service('edit_plus.ui')->buildBottomBar($actively_used_tempstore_entities, \Drupal::destination()->get()); - } - } - } -} - /** * Active tempstore entities. * @@ -235,24 +219,9 @@ function edit_plus_element_info_alter(&$types) { $types['inline_textarea']['#pre_render'][] = 'element.edit_plus:preRenderTextFormat'; } -/** - * Implements hook_entity_build_defaults_alter(). - */ -function edit_plus_entity_build_defaults_alter(array &$build, \Drupal\Core\Entity\EntityInterface $entity, $view_mode) { - // Flag that the entity needs a wrapper. This wrapper is used to update the page - // once an action happens. e.g. adding a new field to the page. - // @see EntityTemplate->onTwigRenderTemplate() - // @see EditPlusFormTrait->addEmptyField() - $build['#edit_plus_entity'] = [ - 'entity_type' => $entity->getEntityTypeId(), - 'entity_id' => edit_plus_entity_identifier($entity), - 'bundle' => $entity->bundle(), - 'view_mode' => $view_mode, - ]; -} - function edit_plus_entity_identifier(EntityInterface $entity) { // @todo Can we just use uuid for everything? + // @todo This is also duplicated as toolbar_plus_entity_identifier. return !$entity instanceof BlockContent ? $entity->id() : $entity->uuid(); } @@ -263,30 +232,5 @@ function edit_plus_form_field_config_edit_form_alter(&$form, FormStateInterface \Drupal::service('edit_plus.field_config_form_alter')->formAlter($form, $form_state); } -/** - * Find widget in form. - * - * @param array $form_item - * The form item to find the widget element for. - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The field definition. - * - * @return mixed - * The actual field widget form item. - */ -function edit_plus_find_widget_type(array $form_item, FieldDefinitionInterface $field_definition) { - $property_name = $field_definition->getfieldstoragedefinition()->getmainpropertyname(); - if (!empty($form_item['#theme']) && $form_item['#theme'] == 'inline_textarea') { - return 'inline_textarea'; - } - // @todo Is there a better way to discern what the widget is??? - $widget = $form_item['widget'][0] ?? $form_item['widget'][0][$property_name] ?? $form_item['widget'][$property_name] ?? $form_item['widget'] ?? $form_item[$property_name] ?? NULL; - if (!empty($widget['media_library_selection'])) { - return 'media_library'; - } - if (!empty($widget['#type'])) { - return $widget['#type']; - } - return NULL; -} + diff --git a/edit_plus.routing.yml b/edit_plus.routing.yml index cf3a3a6babf0934d23f00c72ef1b5c4ce7a94975..9abadd1fb7dc104ccb158711e642f3f050c50435 100644 --- a/edit_plus.routing.yml +++ b/edit_plus.routing.yml @@ -4,6 +4,7 @@ edit_plus.entity_form: _title: 'Entity Form' _controller: '\Drupal\edit_plus\Controller\MultipleEntityFormController::entityForm' options: + _admin_route: true parameters: entity: type: entity:{entity_type} diff --git a/edit_plus.services.yml b/edit_plus.services.yml index e000c2a52072d443d39ee5c924ca2e1287311adc..257fa92c99134260a3d115d489fc2c111b8fe266 100644 --- a/edit_plus.services.yml +++ b/edit_plus.services.yml @@ -1,12 +1,20 @@ services: - edit_plus.inline_entity_form: + _defaults: + autoconfigure: true + autowire: true + + edit_plus.form_alter.inline_entity_form: class: Drupal\edit_plus\Form\InlineEntityFormAlter - arguments: ['@entity_display.repository', '@edit_plus.tempstore_repository', '@field_sample_value.generator', '@entity_type.manager', '@event_dispatcher', '@module_handler', '@renderer', '@request_stack', '@edit_plus.ui' ] + arguments: ['@entity_display.repository', '@edit_plus.tempstore_repository', '@field_sample_value.generator', '@entity_type.manager', '@event_dispatcher', '@module_handler', '@renderer', '@request_stack'] - edit_plus.entity_edit_form_alter: + edit_plus.form_alter.entity_edit: class: Drupal\edit_plus\Form\EntityEditFormAlter arguments: [ '@edit_plus.tempstore_repository' , '@current_route_match'] + # @todo Rename the above form alters to match the following new convention. + edit_plus.form_alter.views_form_media_library_widget: + class: Drupal\edit_plus\Form\ViewsFormMediaLibraryWidgetAlter + edit_plus.field_config_form_alter: class: Drupal\edit_plus\Form\FieldConfigFormAlter @@ -23,35 +31,18 @@ services: class: Drupal\edit_plus\InlineEditorElement arguments: [ '@plugin.manager.editor' ] - edit_plus.ui: - class: Drupal\edit_plus\Ui - - edit_plus.event_subscriber.entity_template: - class: Drupal\edit_plus\EventSubscriber\EntityTemplate - tags: - - { name: event_subscriber } - edit_plus.event_subscriber.default_field_attributes: class: Drupal\edit_plus\EventSubscriber\DefaultFieldAttributes - tags: - - { name: event_subscriber } + arguments: [ '@event_dispatcher' ] edit_plus.event_subscriber.handle_field_attribute: class: Drupal\edit_plus\EventSubscriber\HandleFieldAttribute - tags: - - { name: event_subscriber } edit_plus.event_subscriber.inline_editor_field_attributes: class: Drupal\edit_plus\EventSubscriber\InlineEditorFieldAttributes - tags: - - { name: event_subscriber } edit_plus.event_subscriber.media_field_attributes: class: Drupal\edit_plus\EventSubscriber\MediaFieldAttributes - tags: - - { name: event_subscriber } - edit_plus.event_subscriber.clone_field_attribute: - class: Drupal\edit_plus\EventSubscriber\CloneFieldAttribute - tags: - - { name: event_subscriber } + edit_plus.event_subscriber.pop_out_width_field_attribute: + class: Drupal\edit_plus\EventSubscriber\PopOutWidthFieldAttribute diff --git a/js/edit-plus.js b/js/edit-plus.js index 25d43ab1c780b4dc1bce823712bde118e22188dd..027cb2684fb97d25adc3c9d37e61a0709eb59bad 100644 --- a/js/edit-plus.js +++ b/js/edit-plus.js @@ -1,34 +1,21 @@ +import * as effects from './edit_plus/effects.js'; // Don't remove! import * as entityForm from './edit_plus/entity-form.js'; import * as editableElement from './edit_plus/editable-element.js'; import * as pluginManager from './edit_plus/field-plugin-manager.js'; -(($, Drupal, once) => { +(($, Drupal, once, displace) => { Drupal.EditPlus = {}; Drupal.EditPlus.EditMode = 'enabled'; /** - * Focused Form Item. + * Currently Editing Element. * - * Remember the focused form item. Instead of calling replaceEditableElementWithMarkup + * Remember the focused form item. Instead of calling replaceFormItemWithPageElement * onblur, lets track the focused form item so that clicking things in the * sidebar doesn't lose focus on the currently edited form item. */ - Drupal.EditPlus.Focused = null; - - /** - * Only update element. - * - * Flag to only update the markup of the changed field. Usually after an - * element has been edited the entire entity's rendered markup is updated. If - * a user edit's an element and then clicks another editable element which is - * then replaced with the form item for editing, the form item would be - * overwritten/disappear when the whole entity's markup is replaced. In this - * instance only update the field that has changed. - * - * @type {null} - */ - Drupal.EditPlus.onlyUpdateElement = null; + Drupal.EditPlus.CurrentlyEditingElement = null; /** * Editable Element clicked. @@ -37,16 +24,20 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; * The click event. */ Drupal.EditPlus.EditableElementClicked = (e) => { - // Fields are often nested. Only register one mousedown. + if (e.target.hasAttribute('contenteditable')) { + // We are editing a textfield. + // @see textfield.js + return; + } + + // Fields can be nested. Only register one mousedown. e.preventDefault(); e.stopPropagation(); - // Deselect any currently focused fields. - if (Drupal.EditPlus.Focused) { - Drupal.EditPlus.FieldPluginManager.getPlugin(Drupal.EditPlus.Focused.elementInfo).blur(Drupal.EditPlus.Focused); - } // @todo Make the default of which form is loaded initially configurable and - // add a UI to switch between the entity reference forms. + // @todo add a UI to switch between the referenced entities forms. + // @todo Default to the first form, e.g. entity reference or media library + // @todo not the referenced entity form or the media entity form. let fieldValue = e.target.closest('.edit-plus-field-value'); while (fieldValue.parentNode.closest('.edit-plus-field-value')) { fieldValue = fieldValue.parentNode.closest('.edit-plus-field-value'); @@ -54,13 +45,23 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; const EditableElement = new editableElement.EditableElement(fieldValue.dataset.editPlusId); + // Prevent clicks on a form item that is currently being updated. + if (EditableElement.getPageElementHandle().classList.contains('disabled-updating')) { + return; + } + entityForm.getForm(EditableElement).then((response, status) => { + // Set the widget now that the form is loaded. EditableElement.setWidget(); - Drupal.EditPlus.FieldPluginManager.getPlugin(EditableElement).edit(EditableElement); - // if (response.status === 'new_form') { - // entityForm.autosaveNonEditableElements(EditableElement); - // } + if (Drupal.EditPlus.CurrentlyEditingElement) { + Drupal.EditPlus.CurrentlyEditingElement.plugin.blur(Drupal.EditPlus.CurrentlyEditingElement); + } + + EditableElement.plugin.edit(EditableElement); + + }).catch((error) => { + console.error('An error occurred while trying to edit an element:', error); }); } @@ -71,11 +72,6 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; once.remove('EditPlusDisabled', '.edit-plus-field-value', document); once('EditPlusEnabled', '.edit-plus-field-value', document).forEach(editableField => { editableField.addEventListener('mousedown', Drupal.EditPlus.EditableElementClicked); - // Temporarily reveal the sidebar here. // @todo move to toolbar+ - const sidebar = document.querySelector('#toolbar-plus-sidebar'); - if (sidebar.querySelector('.edit-plus-form')) { - sidebar.style.display = 'inherit'; - } }); Drupal.EditPlus.DisableInteractiveElements(); } @@ -83,34 +79,41 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; /** * Disable edit mode */ - Drupal.EditPlus.DisableEditMode = () => { - once.remove('EditPlusEnabled', '.edit-plus-field-value', document); - once('EditPlusDisabled', '.edit-plus-field-value', document).forEach(editableField => { - editableField.removeEventListener('mousedown', Drupal.EditPlus.EditableElementClicked); + Drupal.EditPlus.DisableEditMode = async () => { + await Drupal.EditPlus.notUpdating(); + return new Promise((resolve, reject) => { + once.remove('EditPlusEnabled', '.edit-plus-field-value', document); + once('EditPlusDisabled', '.edit-plus-field-value', document).forEach(editableField => { + editableField.removeEventListener('mousedown', Drupal.EditPlus.EditableElementClicked); + }); + document.querySelectorAll('.edit-plus-form.toolbar-plus-sidebar').forEach(sidebar => { + sidebar.classList.add('toolbar-plus-hidden'); + sidebar.removeAttribute('data-offset-right'); + displace(); + }); + + Drupal.EditPlus.EnableInteractiveElements(); + resolve(); }); - document.querySelector('#toolbar-plus-sidebar').style.display = 'none'; // @todo move to toolbar+ - Drupal.EditPlus.EnableInteractiveElements(); } /** - * Toggle edit mode. + * Not updating. + * + * Wait till edit+ is done updating. * - * This listens for toolbar_plus's toggleToolbarTool event. + * @returns {Promise<unknown>} */ - Drupal.behaviors.EditPlusToggle = { - attach: (context, settings) => { - once('EditPlusToolbarToggle', 'html', context).forEach(element => { - // Toggle Edit+ on and off. - window.addEventListener('toggleToolbarTool', e => { - if (e.detail.tool === 'edit_plus' && e.detail.state === 'on') { - Drupal.EditPlus.EnableEditMode(); - } else { - Drupal.EditPlus.DisableEditMode(); - } - }); - }); - } - }; + Drupal.EditPlus.notUpdating = () => { + return new Promise(resolve => { + const intervalId = setInterval(() => { + if (!entityForm.editPlusIsUpdating) { + clearInterval(intervalId); + resolve(); + } + }, 100); + }); + } /** * Edit + persist state. @@ -120,7 +123,7 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; */ Drupal.behaviors.EditPlusPersistState = { attach: (context, settings) => { - once('EditPlusPersistState', '.edit-plus-entity-wrapper', context).forEach(element => { + once('EditPlusPersistState', '.toolbar-plus-entity-wrapper', context).forEach(element => { // Persist the state. if ( sessionStorage.getItem('toolbarState') === 'open' && @@ -132,7 +135,7 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; } }; - + // Register field plugins. pluginManager.default($, Drupal, once); /** @@ -142,8 +145,9 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; e.preventDefault(); e.stopPropagation(); }; + Drupal.EditPlus.DisableInteractiveElements = () => { - $('.edit-plus-entity-wrapper') + $('.toolbar-plus-entity-wrapper') .find('a') .each((index, link) => { // Disable clicking links while in edit mode. @@ -152,7 +156,7 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; }; Drupal.EditPlus.EnableInteractiveElements = () => { - $('.edit-plus-entity-wrapper') + $('.toolbar-plus-entity-wrapper') .find('a') .each((index, link) => { // Enable clicking links when leaving edit mode. @@ -160,7 +164,7 @@ import * as pluginManager from './edit_plus/field-plugin-manager.js'; }); }; -})(jQuery, Drupal, once); +})(jQuery, Drupal, once, Drupal.displace); diff --git a/js/edit_plus/editable-element.js b/js/edit_plus/editable-element.js index 03995b86f28bb658db707da0f7d758a4ef8cd7de..38fa5d7b850e6f97e90ceba00d4d1f376fc73c19 100644 --- a/js/edit_plus/editable-element.js +++ b/js/edit_plus/editable-element.js @@ -1,15 +1,69 @@ -import * as util from './utilities.js'; +import * as formatterPropertyMap from './formatter-property-map.js'; /** * Editable Element. * - * This class is a state object that represents the binding of the currently + * This class is a state object that represents the relationship of the currently * edited page element to the form element. */ export class EditableElement { + /** + * Info + * + * All the details about this editable element. + * + * @type {{elementId, langcode: *, fieldName: *, mainProperty: *, delta: *, entityTypeId: *, entityId: *, widget: *, viewMode: *, fieldType: *, widget: *}} + */ + info = null; + + /** + * The inline editing field plugin for this EditableElement. + * @see field-plugin-manager.js + * + * @type {FieldPluginBase} + */ + plugin = null; + constructor(id) { - this.info = util.parseMarkupId(id) + this.info = this.parseMarkupId(id); + } + + /** + * Create from form item. + * + * @param element + * An element within a form item wrapper. + * + * @returns {EditableElement} + * A new EditableElement instance from a form item or any element from + * within a form item wrapper. + */ + static createFromFormItem(element) { + // Find the page element + let pageElement; + const wrapper = element.closest('[data-edit-plus-page-element-edit-plus-id]') + if (wrapper) { + pageElement = document.querySelector('[data-edit-plus-id="' + wrapper.dataset.editPlusPageElementEditPlusId + '"]'); + } else { + // Inline Editor + let formItem = element.closest('.edit-plus-form-item'); + pageElement = document.querySelector('[data-edit-plus-page-element-id="' + formItem.dataset.editPlusFormItemId + '"]'); + } + + const editableElement = new EditableElement(pageElement.dataset.editPlusId); + editableElement.setWidget(); + return editableElement; + } + + /** + * Set widget. + * + * Once the form is loaded the widget needs to be set on the EditableElement. + */ + setWidget() { + this.info.widget = this.getFormItemWrapper().dataset.editPlusFormItemWidget; + this.plugin = Drupal.EditPlus.FieldPluginManager.getPlugin(this); } /** @@ -19,11 +73,38 @@ export class EditableElement { * The form for the EditableElement. */ getForm() { - return document.querySelector('[data-edit-plus-form-id="' + util.getFormId(this.info) + '"]'); + const id = [ + this.info.entityTypeId, + this.info.entityId, + ].join('::'); + return document.querySelector('[data-edit-plus-form-id="' + id + '"]'); } - setWidget() { - this.info.widget = this.getFormItemWrapper().dataset.editPlusFormItemWidget; + /** + * Get handle type. + * + * @returns {string} + * Returns a string of either form_item or wrapper. + * + * Users can config whether a field swaps the form item or the entire form + * wrapper into the page for editing. + */ + getHandleType() { + return this.getFormItemWrapper()?.dataset.editPlusHandle ?? 'form_item'; + } + + /** + * Handle is wrapper. + * + * @see getFormItemHandle + * + * @returns {boolean} + * Returns true if the handle is wrapper. false if the handle is form_item. + */ + handleIsWrapper() { + const wrapper = this.getFormItemWrapper(); + const elementToUseAsHandle = wrapper.dataset.editPlusHandle ?? 'form_item'; + return elementToUseAsHandle === 'wrapper'; } /** @@ -37,7 +118,13 @@ export class EditableElement { } getPageElementWrapper() { - return document.querySelector('[data-edit-plus-field-value-wrapper="' + util.getPageElementWrapperId(this.info) + '"]'); + const id = [ + this.info.entityTypeId, + this.info.entityId, + this.info.fieldName, + this.info.mainProperty, + ].join('::'); + return document.querySelector('[data-edit-plus-field-value-wrapper="' + id + '"]'); } getPageElementHandle() { @@ -47,6 +134,22 @@ export class EditableElement { return document.querySelector('[data-edit-plus-id="' + this.info.elementId + '"]'); } + /** + * Get Form Item ID. + * + * @returns {string} + * The form item ID. + */ + getFormItemId() { + return [ + this.info.entityTypeId, + this.info.entityId, + this.info.fieldName, + this.info.delta, + formatterPropertyMap.getProperty(this.info), + ].join('::'); + } + /** * Get form item. * @@ -54,7 +157,7 @@ export class EditableElement { * The form item for the EditableElement. */ getFormItem() { - return document.querySelector('[data-edit-plus-form-item-id="' + util.getFormItemId(this.info) + '"]'); + return document.querySelector('[data-edit-plus-form-item-id="' + this.getFormItemId() + '"]'); } /** @@ -64,7 +167,13 @@ export class EditableElement { * The form item wrapper for the EditableElement. */ getFormItemWrapper() { - return document.querySelector('[data-edit-plus-form-item-wrapper-id="' + util.getFormItemWrapperId(this.info) + '"]'); + const id = [ + this.info.entityTypeId, + this.info.entityId, + this.info.fieldName, + formatterPropertyMap.getProperty(this.info), + ].join('::'); + return document.querySelector('[data-edit-plus-form-item-wrapper-id="' + id + '"]'); } /** @@ -78,20 +187,7 @@ export class EditableElement { if (this.getHandleType() === 'wrapper') { return this.getFormItemWrapper(); } - return document.querySelector('[data-edit-plus-form-item-id="' + util.getFormItemId(this.info) + '"]').closest('.edit-plus-form-item'); - } - - /** - * Get handle type. - * - * @returns {string} - * Returns a string of either form_item or wrapper. - * - * Users can config whether a field swaps the form item or the entire form - * wrapper into the page for editing. - */ - getHandleType() { - return this.getFormItemWrapper().dataset.editPlusHandle ?? 'form_item'; + return document.querySelector('[data-edit-plus-form-item-id="' + this.getFormItemId() + '"]').closest('.edit-plus-form-item'); } /** @@ -104,61 +200,34 @@ export class EditableElement { return this.getFormItemHandle().querySelectorAll('[data-edit-plus-input]'); } - /** - * Get form place marker. - * - * @returns {Element} - * The place marker element for the EditableElement. - * - * When a form item is moved from the form to the page for editing a - * placeholder is created to mark where the form item originated from. - */ - getFormPlaceMarker() { - return document.querySelector('[data-form-item-placeholder="' + util.getFormItemWrapperId(this.info) + '"]'); - } - - /** - * Is cloneable. - * - * @returns {boolean} - * Whether this field has been configured to swap a clone or the actual - * form item into the page for editing. - */ - isCloneable() { - return this.getFormItemWrapper().dataset.editPlusClone === 'true'; - } - /** - * Handle is wrapper. - * - * @see getFormItemHandle - * - * @returns {boolean} - * Returns true if the handle is wrapper. false if the handle is form_item. - */ - handleIsWrapper() { - const wrapper = this.getFormItemWrapper(); - const elementToUseAsHandle = wrapper.dataset.editPlusHandle ?? 'form_item'; - return elementToUseAsHandle === 'wrapper'; + getSizedPlaceholderId() { + return 'sized-placeholder-' + this.info.elementId.replace(/::/g, '-'); } /** - * Create from form item. + * Parse markup ID. * - * @param element - * An element within a form item wrapper. + * @param id + * The Entity Plus ID on the rendered field value. * - * @returns {EditableElement} - * A new EditableElement instance from a form item or any element from - * within a form item wrapper. + * @returns {{elementId, langcode: *, fieldName: *, mainProperty: *, delta: *, entityTypeId: *, entityId: *, widget: *, viewMode: *, fieldType: *}} + * The elementInfo object. */ - static createFromFormItem(element) { - const wrapper = element.closest('.edit-plus-form-item-wrapper'); - if (wrapper) { - return new EditableElement(wrapper.dataset.editPlusPageElementId); - } - const pageElement = document.querySelector('[data-edit-plus-markup-item-id="' + element.dataset.editPlusFormItemId + '"]'); - return new EditableElement(pageElement.dataset.editPlusId); + parseMarkupId(id) { + const parts = id.split('::'); + return { + entityTypeId: parts[0], + entityId: parts[1], + fieldName: parts[2], + delta: parts[3], + langcode: parts[4], + viewMode: parts[5], + fieldType: parts[6], + fieldFormatter: parts[7], + mainProperty: parts[8], + elementId: id, + }; } } diff --git a/js/edit_plus/effects.js b/js/edit_plus/effects.js index 0be0c9111a6faa9a95b3c8c8215e78265c2f4ef4..8466cd059970a08d0c4f93d98ef0c771f77d6687 100644 --- a/js/edit_plus/effects.js +++ b/js/edit_plus/effects.js @@ -1,16 +1,20 @@ -import * as util from './utilities.js'; +import * as editableElement from './editable-element.js'; (($, Drupal, once) => { Drupal.behaviors.EditPlusFieldAddedToPage = { attach: (context, settings) => { once('EditPlusFieldAddedToPage', '.edit-plus-field-added-to-page', context).forEach(formItem => { - let markupItem = util.getMarkupItem(formItem); - if (markupItem) { - // Use the field wrapper if it's available. - markupItem = markupItem.closest('.field') ?? markupItem; - // Animate adding the field to the page. - markupItem.classList.add('edit-plus-field-added-to-page-effect'); - } + + // @todo Review this. If creating the EditableElement here proves unreliable + // we need to implement something like the getMarkupItem that was removed from + // utilities.js + + const EditableElement = editableElement.EditableElement.createFromFormItem(formItem); + const pageElement = EditableElement.getPageElementHandle(); + // Use the field wrapper if it's available. + const element = pageElement.closest('.field') ?? pageElement; + // Animate adding the field to the page. + element.classList.add('edit-plus-field-added-to-page-effect'); }); } }; diff --git a/js/edit_plus/entity-form.js b/js/edit_plus/entity-form.js index 2ad62811f9a0c679744c4cd1e7f1df1fd19e13f6..1e33d4bf380fca40537073422fd94397b72c5c0c 100644 --- a/js/edit_plus/entity-form.js +++ b/js/edit_plus/entity-form.js @@ -1,4 +1,4 @@ -import * as utilities from './utilities.js'; +import * as editableElement from './editable-element.js'; /** * Get form. @@ -13,13 +13,16 @@ import * as utilities from './utilities.js'; export const getForm = (EditableElement) => { // Hide/cache previously loaded forms. document.querySelectorAll('.edit-plus-form').forEach(form => { - form.classList.add('edit-plus-hidden'); + form.classList.add('toolbar-plus-hidden'); + form.removeAttribute('data-offset-right'); }); // Return the requested form if we already have it. const formExists = EditableElement.getForm(); if (formExists) { return new Promise((resolve, reject) => { - formExists.classList.remove('edit-plus-hidden'); + formExists.classList.remove('toolbar-plus-hidden'); + formExists.setAttribute('data-offset-right', ''); + Drupal.displace(); resolve({status: 'existing_form'}); }); } @@ -29,7 +32,7 @@ export const getForm = (EditableElement) => { const layoutBuilderBlock = pageElement.closest('.block-layout-builder'); if (layoutBuilderBlock) { // Use the regular entity form for field blocks. - // @todo Maybe add a field-block class that we don't need to search for... + // @todo Maybe add a different field-block class that we don't need to search for... let isFieldBlock = false; for (let i=0; i < layoutBuilderBlock.classList.length; i++) { isFieldBlock = layoutBuilderBlock.classList[i].includes('field-block'); @@ -69,7 +72,10 @@ const getLayoutBuilderEntityForm = (EditableElement) => { for (let i = storageIds.length - 2; i >= 0; i--) { const layoutBlock = storageIds[i].closest('[data-layout-builder-layout-block]'); const section = layoutBlock.closest('[data-layout-builder-section-delta]'); - nestedStoragePath = `${section.dataset.layoutBuilderSectionDelta}&${layoutBlock.dataset.layoutBuilderBlockUuid}` + nestedStoragePath; + if (nestedStoragePath) { + nestedStoragePath += '&'; + } + nestedStoragePath += `${section.dataset.layoutBuilderSectionDelta}&${layoutBlock.dataset.layoutBuilderBlockUuid}`; } nestedStoragePath = nestedStoragePath ? '/' + nestedStoragePath : nestedStoragePath; let url = Drupal.url(`edit-plus/update/block/${storageType}/${storageId}/${sectionDelta}/${region}/${blockUuid}${nestedStoragePath}`); @@ -94,17 +100,14 @@ const getLayoutBuilderEntityForm = (EditableElement) => { // @todo Is setting the selector here a hack? Consider making a renderer like Drupal\Core\Render\MainContent\AjaxRenderer. if (r.command === 'insert' && r.method === null && r.data && r.data.includes('edit-plus-form')) { r.method = 'append'; - r.selector = '#toolbar-plus-sidebar'; + r.selector = '#toolbar-plus-right-sidebar'; } }), ) .then(() => { Drupal.Ajax.prototype.success.call(ajax, response, status).then(() => { - resolve({status: 'new_form'}); + resolve({ status: 'new_form' }); }); - }) - .then(() => { - jQuery('#toolbar-plus-sidebar').show(); }); } }); @@ -152,24 +155,14 @@ const getEntityForm = (EditableElement) => { }); } -// @todo I think we need to tag auto save items in the sidebar on the backend. -// export const autosaveNonEditableElements = (EditableElement) => { -// EditableElement.getForm().querySelectorAll('[data-edit-plus-input]').forEach(input => { -// const formItem = input.closest('[data-edit-plus-form-item-id]'); -// if (formItem) { -// const pageElement = utilities.getMarkupItem(formItem); -// if (!pageElement) { -// // Auto save changes to sidebar form items. -// input.addEventListener('change', e => { -// const updateButton = input.closest('form').querySelector('.edit-plus-update-button'); -// jQuery(updateButton).mousedown(); -// editPlusIsUpdating = true; -// }); -// } -// } -// }); -// } +/** + * Flag that the page is currently being updated. + * + * @see Drupal.EditPlus.notUpdating + * + * @type {boolean} + */ export let editPlusIsUpdating = false; /** @@ -180,18 +173,189 @@ export let editPlusIsUpdating = false; * @param isEmpty * Whether the field has been emptied or not. Emptied fields are moved to the * sidebar. + * @param form + * Pass the form element directly when an EditableElement isn't available, + * but you still need to submit the form like when a sidebar form item changes. */ -export const updateTempstore = (EditableElement, isEmpty) => { - const form = EditableElement.getFormItemWrapper().closest('form'); +export const updateTempstore = async (EditableElement, isEmpty, form = null) => { + if (updateTempstore.alreadyUpdating) { + return; + } + if (!EditableElement && Drupal.EditPlus.CurrentlyEditingElement) { + // We are saving something other than an EditableElement while an + // EditableElement is focused. Probably something in the sidebar. Let's + // blur the CurrentlyEditingElement to get any changes from it. + // Flag that we are already updating as blur can call updateTempstore again. + updateTempstore.alreadyUpdating = true; + try { + await Drupal.EditPlus.CurrentlyEditingElement.plugin.blur(); + } finally { + updateTempstore.alreadyUpdating = false; + } + } + + if (!form) { + form = EditableElement.getFormItemWrapper().closest('form'); + } // Move emptied fields to the sidebar. - if (isEmpty) { + if (EditableElement && isEmpty) { form.querySelector('#empty-field').value = EditableElement.info.fieldName; } + // Update the hidden view mode input in order to persist the view mode across + // multiple form submissions as it could be changing in this update. + let viewMode; + if (EditableElement) { + // I think we can standardize on getting the view mode from the form > entity + // wrapper relationship detailed in the else, but we might as well get it + // from EditableElement when it's available as it's a tiny bit faster. + viewMode = EditableElement.info.viewMode; + + // Disable editing the item while it's being updated. + const pageElementWrapper = EditableElement.getPageElementWrapper(); + pageElementWrapper.classList.add('disabled-updating'); + // Add an AJAX throbber to indicate the item is being updated. + const throbber = Drupal.theme.ajaxProgressThrobber(Drupal.t('Updating...')); + pageElementWrapper.insertAdjacentHTML('beforeend', throbber); + setTimeout(() => { + const progress = document.querySelector('.disabled-updating .ajax-progress'); + if (progress) { + const backgroundColor = getBackgroundColor(pageElementWrapper); + progress.style.backgroundColor = backgroundColor; + progress.style.color = getTextColorForBackground(backgroundColor); + progress.style.opacity = '1'; + } + }, 50); + // Flag to hide the submit handler ajax throbber in the sidebar. + document.querySelector('body').classList.add('element-ajax-throbbing'); + // Flag to only update the rendered output of this field, not the entire + // entity. This allows users to continue editing other items on the page. + form.querySelector('.edit-plus-only-update-element').value = EditableElement.getPageElement().dataset.editPlusId; + } else { + // EditableElement won't be available when submitting the form via EditPlusEntityFormAutoSave. + const entityWrapper = document.querySelector('[data-toolbar-plus-entity-wrapper^="' + form.closest('.edit-plus-form').dataset.editPlusFormId + '"]'); + const parts = entityWrapper.dataset.toolbarPlusEntityWrapper.split('::'); + viewMode = parts[2]; + // Flag to update the render output of the entire entity. + form.querySelector('.edit-plus-only-update-element').value = null; + } + // Update the hidden view mode input in order to persist the view mode across + // multiple form submissions as it could be changing in this update. + form.querySelector('.edit-plus-view-mode').value = viewMode; + + // Click the update button (formerly the save submit). const updateButton = form.querySelector('.edit-plus-update-button'); jQuery(updateButton).mousedown(); editPlusIsUpdating = true; } +/** + * Edit+ is done updating. + * + * AJAX callback that flags that the update is complete. + */ +jQuery.fn.editPlusIsDoneUpdating = () => { + editPlusIsUpdating = false; +}; + +/** + * Get background color. + * + * @param element + * The element to get the background color for. + * + * @returns {string} + * The inherited background color of this element. + */ +export const getBackgroundColor = (element) => { + let currentElement = element; + while (currentElement) { + const bgColor = window.getComputedStyle(currentElement).backgroundColor; + if (bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { + return bgColor; + } + currentElement = currentElement.parentElement; + } + + return 'white'; +} + +/** + * Get text color for background. + * + * We don't want to have black text on a black background or other similarly + * un-contrasty situations. + * + * @param backgroundColor + * The background color the text will appear on. + * + * @returns {string} + * white or black, which would be a better choice given the background color. + */ +export const getTextColorForBackground = (backgroundColor) => { + // Extract the RGB values from the string. + const rgb = backgroundColor.match(/\d+/g); + const [r, g, b] = rgb ? [parseInt(rgb[0]), parseInt(rgb[1]), parseInt(rgb[2])] : [255, 255, 255]; + // Calculate the luminance of the color. + const a = [r, g, b].map(v => { + v /= 255; + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }); + const luminance = a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; + + return luminance < 0.5 ? 'white' : 'black'; +} + +/** + * Edit plus update markup + * + * This is an AJAX method called after updating the tempstore. + */ +jQuery.fn.EditPlusUpdateMarkup = (selector, content, updatedElementId) => { + if (updatedElementId) { + // Let's update only the element that had changes so users can continue to + // edit other fields. + const EditableElement = new editableElement.EditableElement(updatedElementId); + const contentElement = document.createElement('div'); + contentElement.innerHTML = content; + const elementToUpdateId = EditableElement.getPageElementWrapper().dataset.editPlusFieldValueWrapper; + const elementToUpdateSelector = '[data-edit-plus-field-value-wrapper="' + elementToUpdateId + '"]'; + const updatedElement = contentElement.querySelector(elementToUpdateSelector); + const element = document.querySelector(elementToUpdateSelector); + element.replaceWith(updatedElement) + document.querySelector('body').classList.remove('element-ajax-throbbing'); + } else { + // Update/replace the whole thing. + const entityWrapper = document.querySelector(selector); + entityWrapper.outerHTML = content; + } + Drupal.EditPlus.EnableEditMode(); + // Scenario: you have two forms loaded which both have CKEditors inside. + // when you change one then click to change the other a chain of events happens. + // updateTempstore clicks the save (ajax update) button > beforeSerialize + // detaches behaviors > all CKEditors are removed > then the CKEditor you + // are now editing doesn't have a CKEditor attached. Let's just call + // attachBehaviors again here to get the editor since it is idempotent. + Drupal.attachBehaviors(); +} + +(($, Drupal, once) => { + + /** + * Edit+ entity form auto save. + * + * Auto saves form items in the sidebar when the user changes its value. + */ + Drupal.behaviors.EditPlusEntityFormAutoSave = { + attach: (context, settings) => { + once('EditPlusEntityFormAutoSave', '[data-edit-plus-auto-submit]', context).forEach(formItem => { + formItem.addEventListener('change', e => { + updateTempstore(null, false, e.target.closest('form')); + }); + }); + } + }; +})(jQuery, Drupal, once); + diff --git a/js/edit_plus/field-plugin-manager.js b/js/edit_plus/field-plugin-manager.js index 6e9e0368c536f915997eaa0dab44f6c2611f9dae..e7d6bdf27d13bff5dd7a3684d867dc4705e26248 100644 --- a/js/edit_plus/field-plugin-manager.js +++ b/js/edit_plus/field-plugin-manager.js @@ -1,8 +1,9 @@ -import * as entityReferenceAutocomplete from './fields/entity-reference-autocomplete.js'; -import * as fieldPluginBase from './fields/field-plugin-base.js'; -import * as inlineTextarea from './fields/inline-editor.js'; -import * as defaultPlugin from './fields/default.js'; -import * as mediaPlugin from './fields/media.js'; +import * as entityReferenceAutocomplete from './plugins/entity-reference-autocomplete.js'; +import * as fieldPluginBase from './plugins/field-plugin-base.js'; +import * as inlineTextarea from './plugins/inline-editor.js'; +import * as defaultPlugin from './plugins/default.js'; +import * as textfield from './plugins/textfield.js'; +import * as mediaPlugin from './plugins/media.js'; /** * Field plugin manager. @@ -42,7 +43,7 @@ class FieldPluginManager { Object.values(this.plugins).forEach(plugin => { plugin.supportedWidgets.forEach(widget => { - if (EditableElement.info.widget === widget) { + if (EditableElement.info.widget === widget && plugin.applies(EditableElement)) { pluginForElement = plugin; } }); @@ -62,6 +63,7 @@ export default function ($ = jQuery, Drupal, once) { * Initialize the field plugins. */ entityReferenceAutocomplete.default($, Drupal, once); + textfield.default($, Drupal, once); inlineTextarea.default($, Drupal, once); defaultPlugin.default($, Drupal, once); mediaPlugin.default($, Drupal, once); diff --git a/js/edit_plus/fields/default.js b/js/edit_plus/fields/default.js deleted file mode 100644 index 91e3921d46182f28795ba24f12ad702f116bdadf..0000000000000000000000000000000000000000 --- a/js/edit_plus/fields/default.js +++ /dev/null @@ -1,63 +0,0 @@ -import * as entityForm from '../entity-form.js'; -import * as fieldPluginBase from './field-plugin-base.js'; - -/** - * Default field plugin. - */ -export class DefaultPlugin extends fieldPluginBase.FieldPluginBase { - - supportedWidgets = []; - - focusElement(EditableElement) { - this.originalValue = this.getInputValue(EditableElement); - this.getFormItemInputs(EditableElement)[0].focus(); - } - - blurElement(EditableElement) { - const inputValue = this.getInputValue(EditableElement); - this.replaceEditableElementWithMarkup(EditableElement); - if (JSON.stringify(this.originalValue) !== JSON.stringify(inputValue)) { - entityForm.updateTempstore(EditableElement, inputValue.length === 0); - } - this.originalValue = null; - } - - getFormItemInputs(EditableElement) { - return EditableElement.getFormItemInputs(); - } - - getInputValue(EditableElement) { - const inputs = this.getFormItemInputs(EditableElement); - if (inputs[0].type === 'checkbox') { - let checkboxes = []; - for (let i=0; i < inputs.length; i++) { - const value = inputs[i].checked ? inputs[i].value : null; - if (value) { - checkboxes.push(value); - } - } - return checkboxes; - } - else { - let values = {}; - for (let i=0; i < inputs.length; i++) { - if (inputs[i].value) { - values[i] = inputs[i].value; - } - } - return values; - } - } - -} - -export default function ($ = jQuery, Drupal, once, dropZones) { - - /** - * Register the default field plugin. - */ - window.addEventListener('EditPlusFieldManager.RegisterPlugins', e => { - e.detail.manager.registerPlugin(new DefaultPlugin()); - }); - -} diff --git a/js/edit_plus/fields/field-plugin-base.js b/js/edit_plus/fields/field-plugin-base.js deleted file mode 100644 index 08d118a6dcbe2ee66349f33d32c3e0f21cd5ba87..0000000000000000000000000000000000000000 --- a/js/edit_plus/fields/field-plugin-base.js +++ /dev/null @@ -1,430 +0,0 @@ -import * as editableElement from '../editable-element.js'; - -/** - * Field plugin base. - * - * A base class for field plugins. - */ -export class FieldPluginBase { - - constructor() { - this.isObserverActive = false; - this.originalValue = null; - this.clonedElement = null; - } - - init() {} - - /** - * Supported widgets. - * - * @type {[]} - * An array of field formatter ID's this plugin supports. - */ - supportedWidgets = []; - - /** - * Edit. - * - * @param EditableElement - * The element being edited. - */ - edit(EditableElement) { - this.replacePageMarkupWithEditableElement(EditableElement); - this.focus(EditableElement); - this.revealAncillary(EditableElement); - } - - /** - * Focus. - * - * Focus on the field after it has been clicked. - * - * @param EditableElement - * The element being edited. - */ - focus(EditableElement) { - // Prepare for de-focusing the form item. It would be easy to add a - // formItem.onblur when the form item looses focus, but losing focus on the - // form item doesn't necessarily mean we are done with it. e.g. changing a - // text format in the sidebar. - EditableElement.getFormItemWrapper().classList.add('edit-plus-focused'); - Drupal.EditPlus.Focused = { - formItem: EditableElement.getFormItemHandle(), - elementInfo: EditableElement.info, - // @todo move the above to instead use Editable Element. - editableElement: EditableElement, - }; - document.addEventListener('mousedown', this.watchFocus, true); - - this.focusElement(EditableElement) - } - - /** - * Focus element. - * - * This should be overridden by field plugins to provide field specific - * instructions on how to give the element focus. - * - * @param EditableElement - * - * @param event - */ - focusElement(EditableElement, event) {} - - /** - * Watch focus. - * - * Once an element is being edited, monitor clicks to determine if the click - * should cause the form item to lose focus. Field plugins can provide selectors - * that do not make the form item loose focus when clicked. e.g. things in the - * sidebar like field formatters etc. - * - * @param e - * The mousedown event. - */ - watchFocus(e) { - // Check that the clicked element is something that should cause the form - // item to lose focus. - const plugin = Drupal.EditPlus.FieldPluginManager.getPlugin(Drupal.EditPlus.Focused.editableElement); - const getCurrentlyFocusedSelectors = plugin.getFocusedSelectors(); - let remainFocused = false; - for (let i = 0; i < getCurrentlyFocusedSelectors.length; i++) { - const ignoredClick = e.target.closest(getCurrentlyFocusedSelectors[i]); - if (ignoredClick !== null) { - remainFocused = true; - break; - } - } - if (!remainFocused) { - const fieldValue = e.target.closest('.edit-plus-field-value'); - if (fieldValue) { - // We lost focus when clicking another editable element. Only update the - // rendered page with the last changed element. - Drupal.EditPlus.onlyUpdateElement = Drupal.EditPlus.Focused.editableElement; - } - - plugin.blur(); - document.querySelectorAll('.edit-plus-focused').forEach(element => { - element.classList.remove('edit-plus-focused'); - }); - } - } - - /** - * Blur. - * - * Remove focus from the actively focused form item. - */ - blur() { - const EditableElement = Drupal.EditPlus.Focused.editableElement; - if (Drupal.EditPlus.Focused.editableElement.isCloneable()) { - this.isObserverActive = false; - this.observer.disconnect(); - } - document.removeEventListener('mousedown', this.watchFocus, true); - Drupal.EditPlus.Focused = null; - this.blurElement(EditableElement); - } - - /** - * Blur element. - * - * This should be overridden by field plugins to provide field specific - * instructions on how to remove focus from the element. Since detecting if - * the field has changes after editing it's recommended for field plugins to - * call this.replaceEditableElementWithMarkup(EditableElement) when appropriate. - * - * @param EditableElement - * The element being edited. - */ - blurElement(EditableElement) {} - - /** - * Reveal ancillary. - * - * Reveal things in the sidebar that pertain to this field e.g. formatter - * - * @param EditableElement - * The element being edited. - */ - revealAncillary(EditableElement) {} - - /** - * Hide ancillary. - * - * Reveal things in the sidebar that pertain to this field e.g. formatter - * - * @param EditableElement - * The current form item. - */ - hideAncillary(EditableElement) { - EditableElement.getFormItemWrapper().classList.add('edit-plus-hidden'); - } - - - /** - * Replace page markup with editable element. - * - * Moves a form item from the form to the main page area. Users can configure - * whether the actual form item is swapped for the rendered element on the page - * or a clone of the form item. Some form items like media libraries stop - * working when taken out of the form. In that case a clone is created and the - * clicks on the clone are forwarded to the original form item. - * - * @param EditableElement - * The element being edited. - */ - replacePageMarkupWithEditableElement(EditableElement) { - if (EditableElement.isCloneable()) { - this.replacePageMarkupWithClonedFormItem(EditableElement); - } else { - this.replacePageMarkupWithFormItem(EditableElement); - } - } - - /** - * Replace page markup with form item. - * - * @param EditableElement - * The element being edited. - */ - replacePageMarkupWithFormItem(EditableElement) { - const pageElementHandle = EditableElement.getPageElementHandle(); - const formItemWrapper = EditableElement.getFormItemWrapper(); - const formElementHandle = EditableElement.getFormItemHandle(); - const placeMarker = document.createElement('div'); - placeMarker.dataset.formItemPlaceholder = formItemWrapper.dataset.editPlusFormItemWrapperId; - formElementHandle.parentNode.insertBefore(placeMarker, formElementHandle); - pageElementHandle.classList.add('edit-plus-hidden'); - pageElementHandle.parentNode.insertBefore(formElementHandle, pageElementHandle) - formElementHandle.appendChild(pageElementHandle); - formElementHandle.classList.remove('edit-plus-hidden'); - formElementHandle.classList.add('edit-plus-focused'); - } - - /** - * Replace page markup with cloned form item. - * - * @param EditableElement - * The element being edited. - */ - replacePageMarkupWithClonedFormItem(EditableElement) { - this.isObserverActive = true; - - const pageElementHandle = EditableElement.getPageElementHandle(); - pageElementHandle.classList.add('edit-plus-hidden'); - - const clone = this.cloneElement(EditableElement); - pageElementHandle.parentNode.insertBefore(clone, pageElementHandle); - clone.classList.remove('edit-plus-hidden'); - } - - /** - * Clone element. - * - * @param EditableElement - * The element being edited. - * - * @returns {Node} - * A clone of the EditableElement form item's handle. - */ - cloneElement(EditableElement) { - const formElementHandle = EditableElement.getFormItemHandle(); - // Remove old clones. - document.querySelectorAll('#' + this.getClonedElementId(formElementHandle)).forEach(clone => { - clone.remove(); - }); - // Make a clone of the form item formElementHandle which will be placed in the page for - // editing. - const clone = formElementHandle.cloneNode(true); - // Make an additional clone to remember the current state of the element to - // later detect if any AJAX changes have occurred to the original form item - // so that we can only update the page when necessary. - this.clonedElement = formElementHandle.cloneNode(true); - - // Prepare the cloned form element for being placed on the page by tweaking - // its attributes so the clone is no longer a duplicate of the form item - // which might confuse JS. - clone.id = this.getClonedElementId(clone); - clone.setAttribute('data-edit-plus-clone-id', 'handle'); - const handleChildren = formElementHandle.querySelectorAll('*'); - const cloneChildren = clone.querySelectorAll('*'); - handleChildren.forEach((element, index) => { - // Identify it as a clone. - if (element.id) { - cloneChildren[index].id = this.getClonedElementId(element); - } - - // Ensure we have unique ID's for every div that correlates it back to the - // original so we can transfer clicks from the clone back to the original. - if (element.hasAttribute('data-edit-plus-original-id')) { - element.removeAttribute('data-edit-plus-original-id'); - } - element.setAttribute('data-edit-plus-original-id', index); - cloneChildren[index].setAttribute('data-edit-plus-clone-id', index); - - // Remove any cloned fadeIn's. - if (element.style.opacity) { - element.style.opacity = ''; - } - }); - - // Transfer events on the cloned element (clicks, drags, input, etc) to the - // original form element. - Object.keys(window).forEach(key => { - if (/^on(?!animationiteration|animationstart|mouseenter|mouseout|mouseevent|mousemove|mouseover|mouseleave|mousein|pointerenter|pointerleave|pointerout|pointerup|pointermove|pointerrawupdate|pointerover|pointerdown|transitionrun|transitionstart|transitioncancel|transitionend|scroll|scrollend)/.test(key)) { - clone.addEventListener(key.slice(2), e => { - const cloneId = e.target?.dataset?.editPlusCloneId ?? false; - if (!cloneId) { - return; - } - const originalFormElement = this.getOriginalFromClone(cloneId); - if (e.type === 'input') { - // Transfer input changes. - const clonedInput = e.target; - const originalInput = this.getOriginalFromClone(clonedInput.dataset.editPlusCloneId); - // If the event is a select list change. Update the selected - // attribute so that when it is cloned again the selected option persists. - if (clonedInput.nodeName === 'SELECT') { - originalInput[originalInput.selectedIndex].removeAttribute('selected'); - originalInput[clonedInput.selectedIndex].setAttribute('selected', 'selected'); - } - originalInput.value = clonedInput.value; - } else { - // Transfer any other events. - const forwardEvent = jQuery.Event(e.type); - forwardEvent.originalEvent = e; - jQuery(originalFormElement).trigger(forwardEvent); - } - }); - } - }); - - clone.dataset.editPlusClone = formElementHandle.dataset.editPlusFormItemWrapperId; - - // Pass along the Edit+ ID so recreate EditableElement as changes occur - formElementHandle.dataset.editPlusPageElementId = EditableElement.getPageElement().dataset.editPlusId; - - // Watch for changes to the original element. - this.observer = new MutationObserver((mutations) => { - if (!this.isObserverActive) { - return; - } - const EditableElement = editableElement.EditableElement.createFromFormItem(mutations[0].target); - - // Update the clone if there has been changes to the form element. - if (this.clonedElement.outerHTML !== EditableElement.getFormItemHandle().outerHTML) { - this.replacePageMarkupWithEditableElement(EditableElement); - } - }); - this.observer.observe(formElementHandle, { - attributes: true, - attributeFilter: ['checked'], - childList: true, - subtree: true, - }); - - // Mutation observers can't detect checkboxes. Watch for those changes explicitly. - formElementHandle.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { - checkbox.addEventListener('change', e => { - const EditableElement = editableElement.EditableElement.createFromFormItem(e.target); - this.replacePageMarkupWithEditableElement(EditableElement); - }); - }); - - return clone; - } - - - getOriginalFromClone(cloneId) { - return document.querySelector('[data-edit-plus-original-id="' + cloneId + '"]'); - } - - /** - * Get cloned element ID. - * - * @param element - * A div with an ID inside the form item handle being cloned. - * - * @returns {string} - * An edit plus cloned + id string. - */ - getClonedElementId(element) { - return 'edit-plus-cloned_' + element.id; - } - - /** - * Replace form item with markup. - * - * Moves a form item back to the form from the main page area. - * - * @param EditableElement - * The element being edited. - */ - replaceEditableElementWithMarkup(EditableElement) { - if (EditableElement.isCloneable()) { - this.replaceClonedFormItemWithPageElement(EditableElement); - } else { - this.replaceFormItemWithPageElement(EditableElement); - } - } - - /** - * Replace cloned form item with page element. - * - * @param EditableElement - * The element being edited. - */ - replaceClonedFormItemWithPageElement(EditableElement) { - const formElementHandle = EditableElement.getFormItemHandle(); - - // Remove old clones. - document.querySelectorAll('#' + this.getClonedElementId(formElementHandle)).forEach(clone => { - clone.remove(); - }); - // Remove binding attribute. - document.querySelectorAll('[data-edit-plus-original-id]').forEach(element => { - element.removeAttribute('data-edit-plus-original-id'); - }) - EditableElement.getPageElementHandle().classList.remove('edit-plus-hidden') - } - - /** - * Replace form item with page element. - * - * @param EditableElement - * The element being edited. - */ - replaceFormItemWithPageElement(EditableElement) { - const pageElementHandle = EditableElement.getPageElementHandle(); - const placeMarker = EditableElement.getFormPlaceMarker(); - const formElementHandle = EditableElement.getFormItemHandle(); - formElementHandle.parentNode.insertBefore(pageElementHandle, formElementHandle); - placeMarker.parentNode.insertBefore(formElementHandle, placeMarker); - pageElementHandle.classList.remove('edit-plus-hidden') - placeMarker.remove(); - Drupal.EditPlus.FieldPluginManager.getPlugin(EditableElement).hideAncillary(EditableElement); - } - - /** - * Get focused selectors. - * - * When an input like a text area or an inline editor has focus, we don't always - * want to hide the editor when the user clicks on something else on the page. - * e.g. clicking a Bold icon in the CKEditor or changing a text format in the - * sidebar. - * - * @returns {string[]} - * An array of selectors that will not cause the current form item to loose - * focus if clicked. - */ - getFocusedSelectors() { - return [ - '.edit-plus-focused', - '[data-edit-plus-clone]', - ]; - } - -} - diff --git a/js/edit_plus/formatter-property-map.js b/js/edit_plus/formatter-property-map.js index 257378eadbf2ffdcc59e4441b477d753b6531c1e..cb5fe1a67f9115f15216f664c3a7c5264b619e0b 100644 --- a/js/edit_plus/formatter-property-map.js +++ b/js/edit_plus/formatter-property-map.js @@ -5,6 +5,8 @@ export const formatterPropertyMapping = { /** * Get property. * + * @todo Describe the problem space around this! + * * @param elementInfo * @returns {*|string} */ diff --git a/js/edit_plus/plugins/default.js b/js/edit_plus/plugins/default.js new file mode 100644 index 0000000000000000000000000000000000000000..879c13382e06b38d1f4d991faffe1e614d0ac729 --- /dev/null +++ b/js/edit_plus/plugins/default.js @@ -0,0 +1,107 @@ +import * as entityForm from '../entity-form.js'; +import * as fieldPluginBase from './field-plugin-base.js'; + +/** + * Default field plugin. + */ +export class DefaultPlugin extends fieldPluginBase.FieldPluginBase { + + focusElement(EditableElement) { + this.getInputValue(EditableElement).then(values => { + // Remember the original values so we can tell if there's changes before + // updating when the form item looses focus. + this.originalValue = values; + }); + this.getFormItemInputs(EditableElement)[0].focus(); + } + + blurElement(EditableElement) { + return this.getInputValue(EditableElement).then(values => { + this.replaceFormItemWithPageElement(EditableElement); + // The values have changed. + if (JSON.stringify(this.originalValue) !== JSON.stringify(values)) { + entityForm.updateTempstore(EditableElement, values.length === 0); + } + this.originalValue = null; + }); + } + + getFormItemInputs(EditableElement) { + return EditableElement.getFormItemInputs(); + } + + /** + * Get input value(s). + * + * @param EditableElement + * The editable element. + * + * @returns {Promise<{}|*[]>} + * A promise because we may need to wait on a CKEditor to be attached before + * getting input changes. + */ + async getInputValue(EditableElement) { + const inputs = this.getFormItemInputs(EditableElement); + if (inputs[0].type === 'checkbox') { + let checkboxes = []; + for (let i=0; i < inputs.length; i++) { + const value = inputs[i].checked ? inputs[i].value : null; + if (value) { + checkboxes.push(value); + } + } + return checkboxes; + } + else { + // If there's a ckeditor here it won't be initialized on the first pass, + // then when the values are compared to see if there are changes, there will + // always be changes on the first go. Let's wait for the ckeditor to load. + const waitForCkeditor = (ckeditorId) =>{ + return new Promise((resolve, reject) => { + const ckeditor = Drupal.CKEditor5Instances.get(ckeditorId); + if (ckeditor) { + resolve(ckeditor); + } else { + let count = 0; + const limit = 10; + const interval = setInterval(() => { + const instance = Drupal.CKEditor5Instances.get(ckeditorId); + if (instance) { + clearInterval(interval); + resolve(instance); + } + if (count >= limit) { + clearInterval(interval); + reject(new Error(`Invalid value for ${ckeditorId}`)); + } + }, 10); + } + }); + } + + let values = {}; + for (let i=0; i < inputs.length; i++) { + if (inputs[i].hasAttribute('data-ckeditor5-id')) { + const ckeditorId = inputs[i].dataset.ckeditor5Id; + const ckeditor = await waitForCkeditor(ckeditorId); + values[i] = ckeditor.getData(); + } else { + values[i] = inputs[i].value; + } + } + return values; + } + } + +} + +export default function ($ = jQuery, Drupal, once, dropZones) { + + /** + * Register the default field plugin. + */ + window.addEventListener('EditPlusFieldManager.RegisterPlugins', e => { + e.detail.manager.registerPlugin(new DefaultPlugin()); + }); + +} diff --git a/js/edit_plus/fields/entity-reference-autocomplete.js b/js/edit_plus/plugins/entity-reference-autocomplete.js similarity index 92% rename from js/edit_plus/fields/entity-reference-autocomplete.js rename to js/edit_plus/plugins/entity-reference-autocomplete.js index f5a3dc02a40121a905acf4213238c10be806b325..6ee412da37973ec4c6547a553518344b69e2e941 100644 --- a/js/edit_plus/fields/entity-reference-autocomplete.js +++ b/js/edit_plus/plugins/entity-reference-autocomplete.js @@ -7,7 +7,7 @@ export default function ($ = jQuery, Drupal, once, dropZones) { */ class EntityReferenceAutocompletePlugin extends defaultPlugin.DefaultPlugin { - supportedWidgets = ['entity_autocomplete']; + supportedWidgets = ['entity_reference_autocomplete_tags']; getFormItemInputs(EditableElement) { return EditableElement.getFormItemWrapper().querySelectorAll('input'); diff --git a/js/edit_plus/plugins/field-plugin-base.js b/js/edit_plus/plugins/field-plugin-base.js new file mode 100644 index 0000000000000000000000000000000000000000..4f80696d983d14a4754448f556beb00a17cba24d --- /dev/null +++ b/js/edit_plus/plugins/field-plugin-base.js @@ -0,0 +1,301 @@ +/** + * Field plugin base. + * + * A base class for field plugins. + */ +export class FieldPluginBase { + + constructor() { + this.originalValue = null; + } + + init() {} + + /** + * Supported widgets. + * + * @type {[]} + * An array of widget ID's this plugin supports. + */ + supportedWidgets = []; + + /** + * Edit. + * + * @param EditableElement + * The element being edited. + */ + edit(EditableElement) { + Drupal.EditPlus.CurrentlyEditingElement = EditableElement; + this.replacePageElementWithFormItem(EditableElement); + this.focus(EditableElement); + this.revealAncillary(EditableElement); + } + + /** + * Focus. + * + * Focus on the field after it has been clicked. + * + * @param EditableElement + * The element being edited. + */ + focus(EditableElement) { + // Prepare for the eventual de-focusing the form item. It would be easy to + // add a formItem.onblur when the form item looses focus, but losing focus + // on the form item doesn't necessarily mean we are done with it. e.g. + // changing a text format in the sidebar. Let's keep track of what is + // currently focused so that we can check if one of its ancillary elements + // was clicked and remain focused. + EditableElement.getFormItemWrapper().classList.add('edit-plus-focused'); + document.addEventListener('mousedown', this.watchFocus, true); + + this.focusElement(EditableElement) + } + + /** + * Focus element. + * + * This should be overridden by field plugins to provide field specific + * instructions on how to give the element focus. + * + * @param EditableElement + * The element being edited. + * + * @param event + */ + focusElement(EditableElement, event) {} + + /** + * Watch focus. + * + * Once an element is being edited, monitor clicks to determine if the click + * should cause the form item to lose focus. Field plugins can provide selectors + * that do not make the form item loose focus when clicked. e.g. things in the + * sidebar like field formatters etc. + * + * @param e + * The mousedown event. + */ + watchFocus(e) { + // Check that the clicked element is something that should cause the form + // item to lose focus. + const plugin = Drupal.EditPlus.CurrentlyEditingElement.plugin; + const getCurrentlyFocusedSelectors = plugin.getFocusedSelectors(); + let remainFocused = false; + for (let i = 0; i < getCurrentlyFocusedSelectors.length; i++) { + const ignoredClick = e.target.closest(getCurrentlyFocusedSelectors[i]); + if (ignoredClick !== null) { + remainFocused = true; + break; + } + } + if (!remainFocused) { + plugin.blur(); + document.querySelectorAll('.edit-plus-focused').forEach(element => { + element.classList.remove('edit-plus-focused'); + }); + } + } + + /** + * Blur. + * + * Remove focus from the focused form item. + */ + async blur() { + const EditableElement = Drupal.EditPlus.CurrentlyEditingElement; + document.removeEventListener('mousedown', this.watchFocus, true); + if (Drupal.EditPlus.CurrentlyEditingElement.sizedPlaceholderHeightObserver) { + Drupal.EditPlus.CurrentlyEditingElement.sizedPlaceholderHeightObserver.disconnect(); + } + Drupal.EditPlus.CurrentlyEditingElement = null; + return this.blurElement(EditableElement); + } + + /** + * Blur element. + * + * This should be overridden by field plugins to provide field specific + * instructions on how to remove focus from the element. Since detecting if + * the field has changes occurs at this point, after editing, it's recommended + * for field plugins to call this.replaceFormItemWithPageElement(EditableElement) + * when appropriate. + * + * @param EditableElement + * The element being edited. + * + * @returns {Promise<void>} + * Getting the form inputs needs to be async since we may need to wait on + * a CKEditor to be attached before getting changes. + */ + blurElement(EditableElement) { + return Promise.resolve(); + } + + /** + * Reveal ancillary. + * + * Reveal things in the sidebar that pertain to this field e.g. formatter + * + * @param EditableElement + * The element being edited. + */ + revealAncillary(EditableElement) {} + + /** + * Hide ancillary. + * + * Reveal things in the sidebar that pertain to this field e.g. formatter + * + * @param EditableElement + * The current form item. + */ + hideAncillary(EditableElement) { + EditableElement.getFormItemWrapper().classList.add('edit-plus-hidden'); + } + + /** + * Replace page element with form item. + * + * Visually moves the hidden form item from the form in the sidebar over the + * editable element that was clicked in the main page area. + * + * @param EditableElement + * The element being edited. + */ + replacePageElementWithFormItem(EditableElement) { + const pageElement = EditableElement.getPageElementHandle(); + const formItem = EditableElement.getFormItemHandle(); + + // Add a placeholder element that the form item will hover over. + const sizedPlaceholderId = EditableElement.getSizedPlaceholderId() + const sizedPlaceholder = document.createElement('div'); + sizedPlaceholder.id = sizedPlaceholderId; + sizedPlaceholder.classList.add('sized-placeholder'); + pageElement.parentElement.insertBefore(sizedPlaceholder, pageElement); + + this.positionFormItemOverPageElement(); + document.addEventListener('scroll', this.positionFormItemOverPageElement); + window.addEventListener('resize', this.positionFormItemOverPageElement); + + // Ensure the sizedPlaceholder height stays in sync with the formItem height. + // As items are added or removed from the form item via ajax, the sizedPlaceholder + // show grow or shrink accordingly. + Drupal.EditPlus.CurrentlyEditingElement.sizedPlaceholderHeightObserver = new ResizeObserver(entries => { + const sizedPlaceholder = document.querySelector('#' + sizedPlaceholderId); + if (sizedPlaceholder) { + for (let entry of entries) { + sizedPlaceholder.style.height = entry.contentRect.height + 'px'; + } + } + }); + Drupal.EditPlus.CurrentlyEditingElement.sizedPlaceholderHeightObserver.observe(formItem); + + pageElement.classList.add('edit-plus-hidden'); + formItem.classList.add('edit-plus-editing'); + formItem.classList.remove('edit-plus-hidden'); + } + + /** + * Replace form item with page element. + * + * Moves a form item back to the form in the sidebar from the main page area. + * + * @param EditableElement + * The element being edited. + */ + replaceFormItemWithPageElement(EditableElement) { + document.removeEventListener('scroll', this.positionFormItemOverPageElement); + window.removeEventListener('resize', this.positionFormItemOverPageElement); + + const pageElement = EditableElement.getPageElementHandle(); + const formItem = EditableElement.getFormItemHandle(); + + formItem.removeAttribute('style'); + formItem.classList.remove('edit-plus-editing'); + const sizedPlaceholder = document.querySelector('#' + EditableElement.getSizedPlaceholderId()); + if (sizedPlaceholder) { + sizedPlaceholder.remove(); + } + + formItem.classList.add('edit-plus-hidden'); + pageElement.classList.remove('edit-plus-hidden'); + } + + /** + * Position form item over page element. + * + * The form item is position: fixed over the placeholder. The + * placeholder not only pushes content further down the page as if the + * editable element is in place there, but it also gives us the dimensions that + * the editable element should have. + */ + positionFormItemOverPageElement() { + const EditableElement = Drupal.EditPlus.CurrentlyEditingElement; + const formItem = EditableElement.getFormItemHandle(); + const popOutWidth = EditableElement.getFormItemWrapper().dataset.popOutWidth ?? 200; + const rectangle = document.querySelector('#' + EditableElement.getSizedPlaceholderId()).getBoundingClientRect(); + + formItem.style.position = 'fixed'; + + if (popOutWidth < rectangle.width) { + // The form item will fit inline. + formItem.style.left = rectangle.left + 'px'; + formItem.style.top = rectangle.top + 'px'; + formItem.style.width = rectangle.width + 'px'; + formItem.classList.remove('form-item-popout'); + } else { + // The form item won't fit in there, make a pop out. + formItem.style.top = rectangle.top + 'px'; + const width = (parseInt(popOutWidth) + 20); + // Center the popup over the sizedPlaceholder. + let left = (rectangle.left + (rectangle.width / 2)) - (popOutWidth / 2); + // Ensure we aren't hanging over the left edge. + const leftEdge = Drupal.displace.calculateOffset('left'); + left = left > leftEdge ? left : leftEdge; + // Ensure we aren't hanging over the right edge. + const rightEdge = document.documentElement.clientWidth - Drupal.displace.calculateOffset('right'); + left = (left + width) < rightEdge ? left : left - ((left + width) - rightEdge); + + formItem.style.left = left + 'px'; + formItem.style.width = width + 'px'; + formItem.classList.add('form-item-popout'); + } + } + + /** + * Get focused selectors. + * + * When an input like a text area or an inline editor has focus, we don't always + * want to hide the editor when the user clicks on something else on the page. + * e.g. clicking a Bold icon in the CKEditor or changing a text format in the + * sidebar. + * + * @returns {string[]} + * An array of selectors that will not cause the current form item to lose + * focus if clicked. + */ + getFocusedSelectors() { + return [ + '.edit-plus-focused', + '#toolbar-plus-right-sidebar', + ]; + } + + /** + * Applies. + * + * When the plugin manager is selecting plugins, ensure that this plugin applies. + * + * @param EditableElement + * The editable element. + * @returns {boolean} + * Whether this plugin should be used. + */ + applies(EditableElement) { + return true; + } + +} + diff --git a/js/edit_plus/fields/inline-editor.js b/js/edit_plus/plugins/inline-editor.js similarity index 63% rename from js/edit_plus/fields/inline-editor.js rename to js/edit_plus/plugins/inline-editor.js index 60c125af1c572de64718960468c7347740b4412a..e9cfd7421d74039c0ab6e496c03a82da59b0fea8 100644 --- a/js/edit_plus/fields/inline-editor.js +++ b/js/edit_plus/plugins/inline-editor.js @@ -1,4 +1,3 @@ -import * as util from '../utilities.js'; import * as entityForm from '../entity-form.js'; import * as fieldPluginBase from './field-plugin-base.js'; import * as editableElement from '../editable-element.js'; @@ -18,7 +17,8 @@ export default function ($ = jQuery, Drupal, once, dropZones) { * @type {[string]} */ supportedWidgets = [ - 'inline_textarea', + 'text_textarea', + 'text_textarea_with_summary', ]; /** @@ -32,15 +32,24 @@ export default function ($ = jQuery, Drupal, once, dropZones) { focusElement(EditableElement) { const formItem = EditableElement.getFormItem(); - const ckeditor5Id = formItem.querySelector('.edit-plus-inline-edit').dataset.ckeditor5Id; + const sourceElement = formItem.querySelector('.edit-plus-inline-edit'); + // Use whatever the original font color was while editing. + sourceElement.style.color = window.getComputedStyle(EditableElement.getPageElement()).color; + const ckeditor5Id = sourceElement.dataset.ckeditor5Id; if (ckeditor5Id) { const editor = Drupal.EditPlus.Ckeditor5Instances.get(ckeditor5Id); if (editor) { - editor.focus(); + // Normally you'd call editor.focus() here, but it's not playing + // nice with Safari. Let's manually fire the event. + setTimeout(() => { + editor.ui.focusTracker.isFocused = true; + editor.editing.view.document.fire('focus'); + }, 0); } else { // The editor hasn't been built yet because the form was just loaded. // Remember that this element needs focus and focus it after the editor - // has been built. + // has been attached. + // @see attachEditor. Drupal.EditPlus.FocusInlineEditor = ckeditor5Id; } } else { @@ -64,10 +73,11 @@ export default function ($ = jQuery, Drupal, once, dropZones) { if (data !== originalData) { pageElement.innerHTML = textArea.innerText = data; } - this.replaceEditableElementWithMarkup(EditableElement); + this.replaceFormItemWithPageElement(EditableElement); if (data !== originalData) { entityForm.updateTempstore(EditableElement, data.length === 0); } + return Promise.resolve(); } revealAncillary(EditableElement) { @@ -132,9 +142,10 @@ export default function ($ = jQuery, Drupal, once, dropZones) { /** * Focus Inline Editor * - * After replacePageMarkupWithEditableElement - * I think it's because the inline editor is attached with a promise so we can't immediately focus it in the instance that the form item was clicked, the form loaded, - * the editor attached, then we need to remember what was clicked so we can focus it once it has been instantiated. + * The scenario is that a user clicks on an editable element > the form is + * retrieved > the element is focused, but actually the CKEditor hasn't been + * asynchronously attached yet, so this is a flag to the attacher that when + * this CKEditor is attached it needs focus immediately. */ Drupal.EditPlus.FocusInlineEditor = null; @@ -144,7 +155,8 @@ export default function ($ = jQuery, Drupal, once, dropZones) { Drupal.behaviors.EditPlusInlineEditors = { attach: (context, settings) => { once('EditPlusInlineEdit', '[data-inline-editor-for]', context).forEach(formatSelector => { - // Attach the Inline Editor. + + // Attach the Inline Editor or reveal the text area. const textAreaId = '#' + formatSelector.dataset.inlineEditorFor; const markupElement = document.querySelector(textAreaId).closest('.edit-plus-form-item').querySelector('.edit-plus-inline-edit'); Drupal.behaviors.EditPlusInlineEditors.showInputElement(markupElement, formatSelector.value); @@ -159,8 +171,8 @@ export default function ($ = jQuery, Drupal, once, dropZones) { const inlineEditElement = formItem.querySelector('.edit-plus-inline-edit'); const ckeditor5Id = inlineEditElement.dataset.ckeditor5Id; - // Update the appropriate things before switching formats to prevent - // data loss. + // Update the textarea and inlineEditElement (markupElement) before + // switching formats to prevent data loss. if (ckeditor5Id) { const editor = Drupal.EditPlus.Ckeditor5Instances.get(ckeditor5Id); if (editor) { @@ -174,22 +186,25 @@ export default function ($ = jQuery, Drupal, once, dropZones) { if (ckeditor5Id) { let destroyPromise = Drupal.behaviors.EditPlusInlineEditors.detachEditor(e.currentTarget, inlineEditElement.dataset.ckeditor5Id); destroyPromise.then(() => { - Drupal.behaviors.EditPlusInlineEditors.showInputElement(markupElement, formatSelector.value); - Drupal.EditPlus.FieldPluginManager.getPlugin(EditableElement).focus(EditableElement); + Drupal.behaviors.EditPlusInlineEditors.attachEditorWithNewFormat(EditableElement, markupElement, formatSelector.value); }); } else { - Drupal.behaviors.EditPlusInlineEditors.showInputElement(markupElement, formatSelector.value); - Drupal.EditPlus.FieldPluginManager.getPlugin(EditableElement).focus(EditableElement); + Drupal.behaviors.EditPlusInlineEditors.attachEditorWithNewFormat(EditableElement, markupElement, formatSelector.value); } } }); }); }, + attachEditorWithNewFormat: (EditableElement, markupElement, format) => { + once.remove('EditPlusAttachEditor', markupElement); + Drupal.behaviors.EditPlusInlineEditors.showInputElement(markupElement, format); + EditableElement.plugin.focus(EditableElement); + }, /** * Show input element. * - * Either reveals the textarea or builds the Inline Editor depending on the + * Either reveals the textarea or builds the Inline Editor based on the * field formatter. * * @param element @@ -221,54 +236,65 @@ export default function ($ = jQuery, Drupal, once, dropZones) { * The field formatter ID. */ attachEditor: (element, format) => { - // Hide the textarea if it hasn't been already. - element.classList.remove('edit-plus-hidden'); - element.closest('.edit-plus-form-item').querySelector('textarea').classList.add('edit-plus-hidden'); - - // Process the field formatter config. - const { editorInline } = CKEditor5; - const { - toolbar, - plugins, - config: pluginConfig, - language, - } = format.editorSettings; - const extraPlugins = core.selectPlugins(plugins); - const config = { - extraPlugins, - toolbar, - language, - ...core.processConfig(pluginConfig), - }; - - // Add an Inline Editor - const id = core.setElementId(element); - const { InlineEditor } = editorInline; - - InlineEditor - .create(element, config) - .then((editor) => { - Drupal.EditPlus.Ckeditor5Instances.set(id, editor); - const formItem = editor.sourceElement.closest('.edit-plus-form-item'); - - - // Does this editor need focus right away? If the CKEditor was the - // first thing that was clicked that loaded the form, we should focus - // the editor as soon as it's created. - if (id === Drupal.EditPlus.FocusInlineEditor) { - Drupal.EditPlus.FocusInlineEditor = null; - editor.focus(); - - // Remember the editors original state. - const EditableElement = editableElement.EditableElement.createFromFormItem(formItem); - const plugin = Drupal.EditPlus.FieldPluginManager.getPlugin(EditableElement); - // @todo review this line as it changed from getData to getInputValue - plugin.originalData.set(EditableElement.info.elementId, plugin.getInputValue(formItem)); - } - }) - .catch(err => { - console.error(err.stack); - }); + once('EditPlusAttachEditor', element).forEach(element => { + // Hide the textarea if it hasn't been already. + element.classList.remove('edit-plus-hidden'); + element.closest('.edit-plus-form-item').querySelector('textarea').classList.add('edit-plus-hidden'); + + // Process the field formatter config. + const { editorInline } = CKEditor5; + let { + toolbar, + plugins, + config: pluginConfig, + language, + } = format.editorSettings; + + // @todo The source editing plugin is not supported in the InlineEditor. + // It is currently hidden with CSS, but let's do something that accounts for + // block separators so you don't get 2 of them together ||. + // plugins = plugins.filter(item => item !== 'sourceEditing.SourceEditing'); + + const extraPlugins = core.selectPlugins(plugins); + const config = { + extraPlugins, + toolbar, + language, + ...core.processConfig(pluginConfig), + }; + + // Add an Inline Editor + const id = core.setElementId(element); + const { InlineEditor } = editorInline; + + InlineEditor + .create(element, config) + .then((editor) => { + Drupal.EditPlus.Ckeditor5Instances.set(id, editor); + const formItem = editor.sourceElement.closest('.edit-plus-form-item'); + + // Does this editor need focus right away? If the CKEditor was the + // first thing that was clicked that loaded the form, we should focus + // the editor as soon as it's created. + if (id === Drupal.EditPlus.FocusInlineEditor) { + // Normally you'd call editor.focus() here, but it's not playing + // nice with safari. Let's manually fire the event. + editor.ui.focusTracker.isFocused = true; + editor.editing.view.document.fire('focus'); + Drupal.EditPlus.FocusInlineEditor = null; + + // Remember the editors original state so we can detect if it has + // changed and trigger a tempstore update. + const EditableElement = editableElement.EditableElement.createFromFormItem(formItem); + const plugin = EditableElement.plugin; + plugin.originalData.set(EditableElement.info.elementId, plugin.getInputValue(formItem)); + + } + }) + .catch(err => { + console.error(err.stack); + }); + }); }, /** diff --git a/js/edit_plus/fields/media.js b/js/edit_plus/plugins/media.js similarity index 78% rename from js/edit_plus/fields/media.js rename to js/edit_plus/plugins/media.js index a98b37005ce5e5524b6d3a61deaa6aad2a7c5d98..527db92d568fad4568343e15b4a4a0428c0c4ecd 100644 --- a/js/edit_plus/fields/media.js +++ b/js/edit_plus/plugins/media.js @@ -1,4 +1,3 @@ -import * as util from '../utilities.js'; import * as entityForm from '../entity-form.js'; import * as fieldPluginBase from './field-plugin-base.js'; @@ -10,24 +9,23 @@ export default function ($ = jQuery, Drupal, once, dropZones) { class MediaPlugin extends fieldPluginBase.FieldPluginBase { constructor() { super(); - this.isObserverActive = false; this.originalValue = null; - this.clonedElement = null; } supportedWidgets = [ - 'media_library', + 'media_library_widget', ]; focusElement(EditableElement) { - this.originalValue = EditableElement.getFormItemWrapper().querySelector('.edit-plus-field-value img').src; + this.originalValue = EditableElement.getFormItemWrapper().querySelector('.edit-plus-field-value img')?.src ?? null; } blurElement(EditableElement) { const formItemWrapper = EditableElement.getFormItemWrapper(); - const newValue = formItemWrapper.querySelector('.edit-plus-field-value img').src + const newValue = formItemWrapper.querySelector('.edit-plus-field-value img')?.src ?? null; - this.replaceEditableElementWithMarkup(EditableElement); - if (this.originalValue !== newValue) { + this.replaceFormItemWithPageElement(EditableElement); + if (newValue && this.originalValue !== newValue) { entityForm.updateTempstore(EditableElement, newValue.length === 0); } + return Promise.resolve(); } getFocusedSelectors() { diff --git a/js/edit_plus/plugins/textfield.js b/js/edit_plus/plugins/textfield.js new file mode 100644 index 0000000000000000000000000000000000000000..cd15321d5e0c39364dc429317c8d3894dfd9b097 --- /dev/null +++ b/js/edit_plus/plugins/textfield.js @@ -0,0 +1,58 @@ +import * as defaultPlugin from './default.js'; + +export default function ($ = jQuery, Drupal, once, dropZones) { + + /** + * Text field plugin. + */ + class TextfieldPlugin extends defaultPlugin.DefaultPlugin { + + supportedWidgets = ['string_textfield']; + + replacePageElementWithFormItem(EditableElement) { + const pageElement = EditableElement.getPageElementHandle(); + pageElement.setAttribute('contenteditable', true); + pageElement.classList.add('edit-plus-text-editing'); + } + + focusElement(EditableElement) { + this.getInputValue(EditableElement).then(values => { + this.originalValue = values; + }); + const input = EditableElement.getFormItemInputs()[0]; + + const pageElement = EditableElement.getPageElementHandle(); + pageElement.addEventListener('input', () => { + input.value = pageElement.textContent; + }); + pageElement.focus(); + } + + replaceFormItemWithPageElement(EditableElement) { + const pageElement = EditableElement.getPageElementHandle(); + if (pageElement) { + pageElement.removeAttribute('contenteditable'); + pageElement.classList.remove('edit-plus-text-editing'); + } + } + + getFocusedSelectors() { + let selectors = super.getFocusedSelectors(); + selectors.push('.edit-plus-text-editing'); + return selectors; + } + + applies(EditableElement) { + return EditableElement.getHandleType() !== 'wrapper'; + } + + } + + /** + * Register the entity reference label field plugin. + */ + window.addEventListener('EditPlusFieldManager.RegisterPlugins', e => { + e.detail.manager.registerPlugin(new TextfieldPlugin()); + }); + +} diff --git a/js/edit_plus/utilities.js b/js/edit_plus/utilities.js deleted file mode 100644 index 410c70192419cbb2126ee9f0a575988c4b171727..0000000000000000000000000000000000000000 --- a/js/edit_plus/utilities.js +++ /dev/null @@ -1,173 +0,0 @@ -import * as formatterPropertyMap from './formatter-property-map.js'; -import * as editableElement from './editable-element.js'; - -/** - * Parse markup ID. - * - * @param id - * The Entity Plus ID on the rendered field value. - * - * @returns {{elementId, langcode: *, fieldName: *, mainProperty: *, delta: *, entityTypeId: *, entityId: *, widget: *, viewMode: *, fieldType: *}} - * The elementInfo object. - */ -export const parseMarkupId = (id) => { - const parts = id.split('::'); - return { - entityTypeId: parts[0], - entityId: parts[1], - fieldName: parts[2], - delta: parts[3], - langcode: parts[4], - viewMode: parts[5], - fieldType: parts[6], - // widget: parts[7], - fieldFormatter: parts[7], - mainProperty: parts[8], - elementId: id, - }; -} - -/** - * Parse Form Item ID. - * - * @param id - * - * @returns {{fieldName: *, delta: *, property: *, entityTypeId: *, entityId: *}} - */ -export const parseFormItemId = (id) => { - const parts = id.split('::'); - return { - entityTypeId: parts[0], - entityId: parts[1], - fieldName: parts[2], - delta: parts[3], - property: parts[4], - }; -} - -/** - * Get Form Item Wrapper Id. - * - * @param elementInfo - * The elementInfo object returned from parseMarkupId. - * - * @returns {string} - * The form item ID. - */ -export const getFormItemWrapperId = (elementInfo) => { - return [ - elementInfo.entityTypeId, - elementInfo.entityId, - elementInfo.fieldName, - formatterPropertyMap.getProperty(elementInfo), - ].join('::'); -} - -/** - * Get Form Item Id. - * - * @param elementInfo - * The elementInfo object returned from parseMarkupId. - * - * @returns {string} - * The form item ID. - */ -export const getFormItemId = (elementInfo) => { - return [ - elementInfo.entityTypeId, - elementInfo.entityId, - elementInfo.fieldName, - elementInfo.delta, - formatterPropertyMap.getProperty(elementInfo), - ].join('::'); -} - -/** - * Get the elementInfo object - * - * @param formItem - * The form item. - * - * @returns {{elementId, langcode: *, fieldName: *, mainProperty: *, delta: *, entityTypeId: *, entityId: *, widget: *, viewMode: *, fieldType: *}} - * The elementInfo object. - */ -export const getElementInfo = (formItem) => { - const markupItem = getMarkupItem(formItem); - return parseMarkupId(markupItem.dataset.editPlusId); -} - -/** - * Get markup item. - * - * @param formItem - * The form item. - * - * @returns {Element|null} - * The markup item. - */ - -// @todo Use page element instead. -export const getMarkupItem = (formItem) => { - if (!formItem.classList.contains('edit-plus-form-item')) { - formItem = formItem.querySelector('.edit-plus-form-item'); - } - - if (!formItem) { - return null; - } - - return document.querySelector('[data-edit-plus-markup-item-id="' + formItem.dataset.editPlusFormItemId + '"]'); -} - -/** - * Get Form ID. - * - * @param elementInfo - * The elementInfo object returned from parseMarkupId. - * - * @returns {string} - * The Edit Plus form ID - */ -export const getFormId = (elementInfo) => { - return [ - elementInfo.entityTypeId, - elementInfo.entityId, - ].join('::'); -} - -export const getPageElementWrapperId = (elementInfo) => { - return [ - elementInfo.entityTypeId, - elementInfo.entityId, - elementInfo.fieldName, - elementInfo.mainProperty, - ].join('::'); -} - -/** - * Edit plus update markup - * - * This is an AJAX method called after updating the tempstore. - */ -jQuery.fn.EditPlusUpdateMarkup = (selector, content) => { - if (Drupal.EditPlus.onlyUpdateElement) { - // The user made changes to one form item then clicked another editable - // element > the next editable element was focused, the tempstore recorded the - // change and returned the updated page markup > let's now update only the - // first element that had its values recorded in the tempstore. If we were to - // update the whole page at this point the actively focused form element - // would be removed. - const contentElement = document.createElement('div'); - contentElement.innerHTML = content; - const elementToUpdateId = Drupal.EditPlus.onlyUpdateElement.getPageElementWrapper().dataset.editPlusFieldValueWrapper; - const elementToUpdateSelector = '[data-edit-plus-field-value-wrapper="' + elementToUpdateId + '"]'; - const updatedElement = contentElement.querySelector(elementToUpdateSelector); - const element = document.querySelector(elementToUpdateSelector); - element.replaceWith(updatedElement) - Drupal.EditPlus.onlyUpdateElement = null; - } else { - const entityWrapper = document.querySelector(selector); - entityWrapper.outerHTML = content; - } - Drupal.EditPlus.EnableEditMode(); -}; diff --git a/modules/edit_plus_lb/edit_plus_lb.services.yml b/modules/edit_plus_lb/edit_plus_lb.services.yml index f27fb5c7f37c82f9384ba10eefc4dfba53f4ca88..d682c796da81e8bf53c0841611f6cb73176fc79f 100644 --- a/modules/edit_plus_lb/edit_plus_lb.services.yml +++ b/modules/edit_plus_lb/edit_plus_lb.services.yml @@ -1,6 +1,7 @@ services: + _defaults: + autoconfigure: true + autowire: true + edit_plus_lb.section_component_build_render_array: class: Drupal\edit_plus_lb\EventSubscriber\SectionComponentBuildRenderArray - arguments: ['@current_user'] - tags: - - { name: event_subscriber } diff --git a/modules/edit_plus_lb/src/EditPlusLbTempstoreRepository.php b/modules/edit_plus_lb/src/EditPlusLbTempstoreRepository.php index f22e9d39cb4e19e4bcaa05e8130d65f4da9ffcdd..34e51012df4f9b9015857b0abd948a83781d2f04 100644 --- a/modules/edit_plus_lb/src/EditPlusLbTempstoreRepository.php +++ b/modules/edit_plus_lb/src/EditPlusLbTempstoreRepository.php @@ -4,14 +4,19 @@ namespace Drupal\edit_plus_lb; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Plugin\Context\EntityContext; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\TempStore\SharedTempStoreFactory; use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\layout_builder\SectionStorageInterface; use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage; use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Drupal\edit_plus\EditPlusTempstoreRepository as EditPlusTempstoreRepositoryBase; @@ -20,7 +25,9 @@ use Drupal\edit_plus\EditPlusTempstoreRepository as EditPlusTempstoreRepositoryB */ class EditPlusLbTempstoreRepository extends EditPlusTempstoreRepositoryBase { - use LayoutEntityHelperTrait; + use LayoutEntityHelperTrait { + getSectionStorageForEntity as lbGetSectionStorageForEntity; + } public function __construct( protected SharedTempStoreFactory $tempStoreFactory, @@ -93,14 +100,44 @@ class EditPlusLbTempstoreRepository extends EditPlusTempstoreRepositoryBase { * The section storage for the entity. */ private function getSectionStorageForEntity(EntityInterface $entity): ?SectionStorageInterface { + // This is a copy and tweak of LayoutEntityHelperTrait. It finds the + // section storage by context. $view_mode = 'full'; - $contexts['entity'] = EntityContext::fromEntity($entity); - $view_mode = LayoutBuilderEntityViewDisplay::collectRenderDisplay($entity, $view_mode)->getMode(); - $contexts['view_mode'] = new Context(new ContextDefinition('string'), $view_mode); - - $section_storage = $this->sectionStorageManager->load('overrides', $contexts); - if (empty($section_storage)) { - return NULL; + if ($entity instanceof LayoutEntityDisplayInterface) { + $contexts['display'] = EntityContext::fromEntity($entity); + $contexts['view_mode'] = new Context(new ContextDefinition('string'), $entity->getMode()); + } + else { + $contexts['entity'] = EntityContext::fromEntity($entity); + if ($entity instanceof FieldableEntityInterface) { + $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode); + if ($display instanceof LayoutEntityDisplayInterface) { + $contexts['display'] = EntityContext::fromEntity($display); + } + // Fall back to the actually used view mode (e.g. full > default). + $contexts['view_mode'] = new Context(new ContextDefinition('string'), $display->getMode()); + } + } + $section_storage = $this->sectionStorageManager->findByContext($contexts, new CacheableMetadata()); + // Create an override section storage if we have a default section storage. + if ($section_storage instanceof DefaultsSectionStorage) { + $view_mode = 'full'; + $contexts['entity'] = EntityContext::fromEntity($entity); + $view_mode = LayoutBuilderEntityViewDisplay::collectRenderDisplay($entity, $view_mode)->getMode(); + $contexts['view_mode'] = new Context(new ContextDefinition('string'), $view_mode); + + $override_section_storage = $this->sectionStorageManager->load('overrides', $contexts); + if (empty($override_section_storage)) { + return NULL; + } + // Copy the sections from the default to the override. + $sections = $section_storage->getSections(); + if (!empty($sections)) { + foreach ($sections as $section) { + $override_section_storage->appendSection($section); + } + } + $section_storage = $override_section_storage ; } return $section_storage; } diff --git a/modules/edit_plus_lb/src/Form/UpdateBlockForm.php b/modules/edit_plus_lb/src/Form/UpdateBlockForm.php index 1ec5e6580687aee1f69029361b4eb4286ef3b903..ef452e81becc7a5489f22cff80c80ff7aef0c182 100644 --- a/modules/edit_plus_lb/src/Form/UpdateBlockForm.php +++ b/modules/edit_plus_lb/src/Form/UpdateBlockForm.php @@ -53,7 +53,6 @@ class UpdateBlockForm extends UpdateBlockFormBase implements EntityFormInterface protected EventDispatcherInterface $dispatcher, protected RendererInterface $renderer, protected Request $request, - protected Ui $ui, $block_manager, $uuid, ) { @@ -74,7 +73,6 @@ class UpdateBlockForm extends UpdateBlockFormBase implements EntityFormInterface $container->get('event_dispatcher'), $container->get('renderer'), $container->get('request_stack')->getCurrentRequest(), - $container->get('edit_plus.ui'), $container->get('plugin.manager.block'), $container->get('uuid'), ); @@ -88,12 +86,14 @@ class UpdateBlockForm extends UpdateBlockFormBase implements EntityFormInterface $form_state->set('section_storage', $section_storage); $form['actions']['submit']['#attributes']['class'][] = 'edit-plus-update-button'; - $form['actions']['submit']['#ajax']['progress'] = ['type' => 'fullscreen']; $form['actions']['submit']['#attributes']['class'][] = 'edit-plus-hidden'; $form['settings']['block_form']['#process'][] = [$this, 'processBlockForm']; + $form['#after_build'][] = [$this, 'autoSubmitProperties']; $form['#after_build'][] = [$this, 'wrapFormAfterBuild']; + $this->attributeLabel($form, $form_state); + return $form; } @@ -147,26 +147,6 @@ class UpdateBlockForm extends UpdateBlockFormBase implements EntityFormInterface */ public function submitForm(array &$form, FormStateInterface $form_state) { parent::submitForm($form, $form_state); - // @todo Uhm, delete this? -// // Submit the plugin form. -// $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state); -// $this->getPluginForm($this->block)->submitConfigurationForm($form, $subform_state); -// -// // If this block is context-aware, set the context mapping. -// if ($this->block instanceof ContextAwarePluginInterface) { -// $this->block->setContextMapping($subform_state->getValue('context_mapping', [])); -// } -// -// // Get the submitted configuration. -// $configuration = $this->block->getConfiguration(); -// -// // Update the block in the current section storage. -// $current_section_storage = $form_state->getStorage()['current_section_storage']; -// $current_section_storage->getSection($this->delta)->getComponent($this->uuid)->setConfiguration($configuration); -// -// // Update the parent entity section storage. -// $this->messenger()->addWarning($this->t('You have unsaved changes.')); -// $form_state->setRebuild(TRUE); // Clear the page cache. $parent_entity = $this->getMainEntity($form, $form_state); @@ -179,11 +159,12 @@ class UpdateBlockForm extends UpdateBlockFormBase implements EntityFormInterface protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) { $response = new AjaxResponse(); - $entity = $form_state->getStorage()['section_storage']->getContextValue('entity'); - $this->buildBottomBar($entity, $response); $this->renderMessages($response); - $this->updateForm($response, $form, $form_state); $this->updatePage($response, $form, $form_state); + // Check if a field was just emptied. + if (!empty($form_state->getUserInput()['settings']['block_form']['empty_field'])) { + $this->updateForm($response, $form, $form_state); + } return $response; } @@ -213,12 +194,13 @@ class UpdateBlockForm extends UpdateBlockFormBase implements EntityFormInterface /** * {@inheritdoc} */ - public function entityContent(array &$form, FormStateInterface $form_state, string $view_mode) { + public function entityContent(array &$form, FormStateInterface $form_state, string $view_mode = NULL) { $component = $this->getComponent($form, $form_state); $contexts = ['layout_builder.entity' => EntityContext::fromEntity($this->getMainEntity($form, $form_state))]; return [ // Add a wrapper so Drupal.behaviors.EditPlusEnable can find the // entity wrapper which is context in this case. + // @todo Could you do an alternative once like in inline-editor.js attachEditor instead? '#type' => 'container', 'component' => $component->toRenderArray($contexts), ]; @@ -294,4 +276,53 @@ class UpdateBlockForm extends UpdateBlockFormBase implements EntityFormInterface return $block_content; } + protected function attributeLabel(array &$form, FormStateInterface $form_state) { + $entity = $this->getFormEntity($form, $form_state); + $form_item_id = sprintf('%s::%s::%s::%s::%s', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity), 'label', 0, 'block_property'); + $form['settings']['label']['#wrapper_attributes']['data-edit-plus-form-item-id'] = $form_item_id; + $form['settings']['label']['#wrapper_attributes']['class'][] = 'edit-plus-form-item'; + $form['settings']['label']['#wrapper_attributes']['class'][] = 'edit-plus-hidden'; + $form['settings']['label']['#attributes']['data-edit-plus-input'] = $form_item_id;; + $form['settings']['label_wrapper'] = [ + '#type' => 'container', + 'label' => $form['settings']['label'], + '#attributes' => [ + 'data-edit-plus-form-item-wrapper-id' => sprintf('%s::%s::%s::%s', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity), 'label', 'block_property'), + 'data-edit-plus-form-item-widget' => 'string_textfield', + 'class' => ['edit-plus-form-item-wrapper'], + ], + // This ensures the value is stored under $values['settings']['label'] + // even though we added a wrapper. + '#parents' => ['settings'], + ]; + unset($form['settings']['label']); + + } + + // + + /** + * Auto-submit properties. + * + * Attribute block properties so they can be auto-saved when changed in the sidebar. + * + * @param array $form + * The block form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The block form. + */ + public function autoSubmitProperties(array &$form, FormStateInterface $form_state) { + $block_properties = array_diff(Element::children($form['settings']), ['label', 'provider', 'block_form', 'context_mapping']); + foreach ($block_properties as $property) { + $form_item =& $form['settings'][$property]; + if ($form_item['#type'] !== 'item') { + $form_item['#attributes']['data-edit-plus-auto-submit'] = ''; + } + } + return $form; + } + } diff --git a/package-lock.json b/package-lock.json index 7a21f97c9676014d81918231d6da42b597c03af1..979e883b9cb3cfe65250cb7282fc92cec125ad0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,107 +5,107 @@ "packages": { "": { "devDependencies": { - "@ckeditor/ckeditor5-editor-inline": "40.2.0" + "@ckeditor/ckeditor5-editor-inline": "~41.3.1" } }, "node_modules/@ckeditor/ckeditor5-clipboard": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-40.2.0.tgz", - "integrity": "sha512-8/xPH9/i86ukcEiHdmTgNuPVJeYTrivbx5ZYqycPO4Eem7VM99gIbOe7pIYpuV+klr9ymVxIHbGyTJDJ3oUO8A==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-41.3.1.tgz", + "integrity": "sha512-6S7tq6FlnHYZmPACeqdf135Jx2bTKHVY8mHQ+CHC8ZZu0XVm62vVeeSLS2IcdtYmHjf4ced1G7suTUBHlfBCLw==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "@ckeditor/ckeditor5-widget": "40.2.0", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", + "@ckeditor/ckeditor5-widget": "41.3.1", "lodash-es": "4.17.21" } }, "node_modules/@ckeditor/ckeditor5-core": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-core/-/ckeditor5-core-40.2.0.tgz", - "integrity": "sha512-0fqIaN+ZhkXXA3mpBN+alycBzPMc8ruO8VrP0OnvCjowqZVS2HXC2AaXNBdxc75xGI3ScXIor7FsgFHxVJIYYQ==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-core/-/ckeditor5-core-41.3.1.tgz", + "integrity": "sha512-h+PgPtCpS2vjO3HbKMYtddRPW+B3AJx9qpixmHJnUZMiFCmRjUZjXATjpi3j+kSQISs4L2Yghq+lsAQxyGHb+A==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", "lodash-es": "4.17.21" } }, "node_modules/@ckeditor/ckeditor5-editor-inline": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-40.2.0.tgz", - "integrity": "sha512-Ox9lQiCSv0acyKaQLCcoebBjAMRE6L6iCBN8XVeQ3u91KZV6/LOhP+CJ314c8AuH+UHPeJt9MHP6eGU0trKHGQ==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-41.3.1.tgz", + "integrity": "sha512-bAhs57qbeGT907yFsUUxdujtrNlmOTJK4DrBCsxoKSoSo8fcG4D05g/I4ehQp3A1CFYsF2Wkx58TtI/fwWRVbQ==", "dev": true, "dependencies": { - "ckeditor5": "40.2.0", + "ckeditor5": "41.3.1", "lodash-es": "4.17.21" } }, "node_modules/@ckeditor/ckeditor5-engine": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-40.2.0.tgz", - "integrity": "sha512-sgboUX8Ps+LcEgywyT3BeK1nzLHjNVIiZU1qvRxR3ixzIw4w2xRNXCGfESWLW5Y5rv9+ypUCrX61oLnZU64PQQ==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-41.3.1.tgz", + "integrity": "sha512-Me4cnkCrknDH50db/jPczuhgzaxUhHbkh2gv8N8Ypken9ZnOPvMD9W1gCFFTLaxikpPmBQwk3u1BSjOKk3r6kw==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-utils": "41.3.1", "lodash-es": "4.17.21" } }, "node_modules/@ckeditor/ckeditor5-enter": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-40.2.0.tgz", - "integrity": "sha512-GjTRaKNX8QEDJ3YYKG3GfPZfGHrcigGBxbo+1WDT7NaOsR2DA/CIZfHlAPfgJDAMV17bhWsT3gy3+oQZsExtnQ==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-41.3.1.tgz", + "integrity": "sha512-iwhvJpfsutqcv/bf8QPMKhMolb7GtShaOT+UIDW3OXjMZaBKZOTyR8OceijwgBmZeillTaXQq9y2e9lbJd46xg==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1" } }, "node_modules/@ckeditor/ckeditor5-paragraph": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-40.2.0.tgz", - "integrity": "sha512-NotxWP1cKvbJSY1UwdTe/Oy1NnAj9Etsi4Z7XA908EvCsNSnFtzdMhYzLhFZJ18avrQFDa7PpSKSyN3M64CbSA==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-41.3.1.tgz", + "integrity": "sha512-weRPLyO/1Z8PpU9+lET4gYgJ8adDuCjYiREup81URSuS1DDQ8vb3D29xA+4Ov7lwg8BaNAMCpTBdp07GHHzv6w==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1" } }, "node_modules/@ckeditor/ckeditor5-select-all": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-40.2.0.tgz", - "integrity": "sha512-yaYCqhdMcoEH3BsilhweNdbOfuO/cexQ1r1/mYoBoW4CypIuAeq8J/3qLpvFaThmCRPzJBn1J7v2Yjs/0UnamA==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-41.3.1.tgz", + "integrity": "sha512-a/LAPO+O9fwHjQ/8s3UNtyrqQRieAnpnPw2IhLlGqOS7nxPKMR2vkb6WnG2LUdO+wYqkCzxUDpBlfVkjkQEI0w==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1" } }, "node_modules/@ckeditor/ckeditor5-typing": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-40.2.0.tgz", - "integrity": "sha512-2E7LkmC4RHdenMUwow0EZDKxlbX00c5UHysUVT51EBGrXiJcN++0cqxQaeJzQ262oTDpk94qE5IZdGXt3ntzrw==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-41.3.1.tgz", + "integrity": "sha512-4Oeafc3if6fTITOest1ILQ573fnkzE9/tn5eNm3zWnHVYR79mRCYxaha9yUlKVQiqaxZ48EVo2FjHiouXmn9+Q==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", "lodash-es": "4.17.21" } }, "node_modules/@ckeditor/ckeditor5-ui": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-40.2.0.tgz", - "integrity": "sha512-K8oC9zrJokZD5Nl4uQjJMo8Couds0eHmfNI/go6iU4A4OAdDzph+W50QnyMed4etKnMdhvUSbnuZnPtQjnsvFA==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-41.3.1.tgz", + "integrity": "sha512-xN7OAiRp7ALKYXUp6Qe/AjkjrhyLuoz9nxq7Jdsnsyb/XXfsXDloMcOuvNRoUgr4gIFHMOoZZxsIn8qegBvcYA==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", "color-convert": "2.0.1", "color-parse": "1.4.2", "lodash-es": "4.17.21", @@ -113,79 +113,78 @@ } }, "node_modules/@ckeditor/ckeditor5-undo": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-40.2.0.tgz", - "integrity": "sha512-k2VZS5x4SJtYk3zhdwHYg+D00DgD0iWR0H4qQgcWmQMFRipYvXJRixP3hSLZGJciQanPFeYcjZgxNQ+rU1s8ug==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-41.3.1.tgz", + "integrity": "sha512-PElWTnlIwuQ94mvdhuH7Mno99oocSnOWPMHi9UuWe6+zVgznQwn0f0diBZvX3l5y8hFgK6q/pQ/CCmbvvYnovA==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1" } }, "node_modules/@ckeditor/ckeditor5-upload": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-40.2.0.tgz", - "integrity": "sha512-AdJSKvWEQbSSyA/DfxbCHRhFN6S4ew4kuYETO57e6AS3aOuYGLBRdu9Mub7IAQcOyy1LL6ktr9u5WEOoWS2h0w==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-41.3.1.tgz", + "integrity": "sha512-ugTgGEgA9qsSl5+qptTmawdfYaONr6b3uTG4byZ76JMdf0qiniZjBF/TtGAVmBkCipcVWFoaZKteiz0fhQMHjA==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1" } }, "node_modules/@ckeditor/ckeditor5-utils": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-40.2.0.tgz", - "integrity": "sha512-f+kTJBwwk7Y/LXm8pEPxBTXVlJwQrH7Levzye9zxEDB0Jtj7+brGr87o666fPmL/ATQc5M+VPhbvnk2sOv7WKg==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-41.3.1.tgz", + "integrity": "sha512-jJu9ndn6Y7+ffBYdDCRXX7OnV9Ddgms2HSF1pmhjZN0uoL96XworuUOn8hx3Zs/KBPjJEwbtYWJMjG9aohrgaQ==", "dev": true, "dependencies": { "lodash-es": "4.17.21" } }, "node_modules/@ckeditor/ckeditor5-watchdog": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-40.2.0.tgz", - "integrity": "sha512-ets7o2dUR7l23G9o/RAbu+gJzUkc2Ul269E3TEhZnbQXFjshvEGK2kzuay7I+/waL3ADuYe4zuoBqsqdPoAhfg==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-41.3.1.tgz", + "integrity": "sha512-iDwdYxC8euSKxfRq4y5vVOX9GVUbEbC9z6glkXpxa1BogqYh39+fywjt+s4o3Ub3b8FJ/EUYuNc+/vK+CzEg4g==", "dev": true, "dependencies": { "lodash-es": "4.17.21" } }, "node_modules/@ckeditor/ckeditor5-widget": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-40.2.0.tgz", - "integrity": "sha512-okeUSwbnu6TUKvwBOl0YdED6Me0/vvs1ybfKZPNEJNwGl989iG0LQO4oYUye8BTCZvzCZ2cBTb1Cvnwr8KRcbg==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-41.3.1.tgz", + "integrity": "sha512-rdBxGS3bxWNhp+yxyBYkcbRV6/mdTDab+konDVhZ/ME1jVZ5cf8OBZcgHUqAxzuWt4XMEdzKINbo1OnSDwApUg==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-enter": "40.2.0", - "@ckeditor/ckeditor5-typing": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-enter": "41.3.1", + "@ckeditor/ckeditor5-typing": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", "lodash-es": "4.17.21" } }, "node_modules/ckeditor5": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/ckeditor5/-/ckeditor5-40.2.0.tgz", - "integrity": "sha512-JaFuY/6DX1wbA6yRB2xQVMr+9W1C3HvSX4AT10ccoKBKe9OctIatekDt2ztV+cMaVHLF1wocskS/Ql9XFRy2Eg==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/ckeditor5/-/ckeditor5-41.3.1.tgz", + "integrity": "sha512-pBK1YZV9Sy4R53XG70TEeLFOvTFC7tg8AmS6d6zizegtwkH8seblkcERkykcNuvmfzZ/2h9JbafJ4kisZOwiUQ==", "dev": true, "dependencies": { - "@ckeditor/ckeditor5-clipboard": "40.2.0", - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-enter": "40.2.0", - "@ckeditor/ckeditor5-paragraph": "40.2.0", - "@ckeditor/ckeditor5-select-all": "40.2.0", - "@ckeditor/ckeditor5-typing": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-undo": "40.2.0", - "@ckeditor/ckeditor5-upload": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "@ckeditor/ckeditor5-watchdog": "40.2.0", - "@ckeditor/ckeditor5-widget": "40.2.0" + "@ckeditor/ckeditor5-clipboard": "41.3.1", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-enter": "41.3.1", + "@ckeditor/ckeditor5-paragraph": "41.3.1", + "@ckeditor/ckeditor5-select-all": "41.3.1", + "@ckeditor/ckeditor5-typing": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-undo": "41.3.1", + "@ckeditor/ckeditor5-upload": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", + "@ckeditor/ckeditor5-watchdog": "41.3.1", + "@ckeditor/ckeditor5-widget": "41.3.1" } }, "node_modules/color-convert": { @@ -230,103 +229,103 @@ }, "dependencies": { "@ckeditor/ckeditor5-clipboard": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-40.2.0.tgz", - "integrity": "sha512-8/xPH9/i86ukcEiHdmTgNuPVJeYTrivbx5ZYqycPO4Eem7VM99gIbOe7pIYpuV+klr9ymVxIHbGyTJDJ3oUO8A==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-41.3.1.tgz", + "integrity": "sha512-6S7tq6FlnHYZmPACeqdf135Jx2bTKHVY8mHQ+CHC8ZZu0XVm62vVeeSLS2IcdtYmHjf4ced1G7suTUBHlfBCLw==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "@ckeditor/ckeditor5-widget": "40.2.0", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", + "@ckeditor/ckeditor5-widget": "41.3.1", "lodash-es": "4.17.21" } }, "@ckeditor/ckeditor5-core": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-core/-/ckeditor5-core-40.2.0.tgz", - "integrity": "sha512-0fqIaN+ZhkXXA3mpBN+alycBzPMc8ruO8VrP0OnvCjowqZVS2HXC2AaXNBdxc75xGI3ScXIor7FsgFHxVJIYYQ==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-core/-/ckeditor5-core-41.3.1.tgz", + "integrity": "sha512-h+PgPtCpS2vjO3HbKMYtddRPW+B3AJx9qpixmHJnUZMiFCmRjUZjXATjpi3j+kSQISs4L2Yghq+lsAQxyGHb+A==", "dev": true, "requires": { - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", "lodash-es": "4.17.21" } }, "@ckeditor/ckeditor5-editor-inline": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-40.2.0.tgz", - "integrity": "sha512-Ox9lQiCSv0acyKaQLCcoebBjAMRE6L6iCBN8XVeQ3u91KZV6/LOhP+CJ314c8AuH+UHPeJt9MHP6eGU0trKHGQ==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-41.3.1.tgz", + "integrity": "sha512-bAhs57qbeGT907yFsUUxdujtrNlmOTJK4DrBCsxoKSoSo8fcG4D05g/I4ehQp3A1CFYsF2Wkx58TtI/fwWRVbQ==", "dev": true, "requires": { - "ckeditor5": "40.2.0", + "ckeditor5": "41.3.1", "lodash-es": "4.17.21" } }, "@ckeditor/ckeditor5-engine": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-40.2.0.tgz", - "integrity": "sha512-sgboUX8Ps+LcEgywyT3BeK1nzLHjNVIiZU1qvRxR3ixzIw4w2xRNXCGfESWLW5Y5rv9+ypUCrX61oLnZU64PQQ==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-41.3.1.tgz", + "integrity": "sha512-Me4cnkCrknDH50db/jPczuhgzaxUhHbkh2gv8N8Ypken9ZnOPvMD9W1gCFFTLaxikpPmBQwk3u1BSjOKk3r6kw==", "dev": true, "requires": { - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-utils": "41.3.1", "lodash-es": "4.17.21" } }, "@ckeditor/ckeditor5-enter": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-40.2.0.tgz", - "integrity": "sha512-GjTRaKNX8QEDJ3YYKG3GfPZfGHrcigGBxbo+1WDT7NaOsR2DA/CIZfHlAPfgJDAMV17bhWsT3gy3+oQZsExtnQ==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-41.3.1.tgz", + "integrity": "sha512-iwhvJpfsutqcv/bf8QPMKhMolb7GtShaOT+UIDW3OXjMZaBKZOTyR8OceijwgBmZeillTaXQq9y2e9lbJd46xg==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1" } }, "@ckeditor/ckeditor5-paragraph": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-40.2.0.tgz", - "integrity": "sha512-NotxWP1cKvbJSY1UwdTe/Oy1NnAj9Etsi4Z7XA908EvCsNSnFtzdMhYzLhFZJ18avrQFDa7PpSKSyN3M64CbSA==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-41.3.1.tgz", + "integrity": "sha512-weRPLyO/1Z8PpU9+lET4gYgJ8adDuCjYiREup81URSuS1DDQ8vb3D29xA+4Ov7lwg8BaNAMCpTBdp07GHHzv6w==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1" } }, "@ckeditor/ckeditor5-select-all": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-40.2.0.tgz", - "integrity": "sha512-yaYCqhdMcoEH3BsilhweNdbOfuO/cexQ1r1/mYoBoW4CypIuAeq8J/3qLpvFaThmCRPzJBn1J7v2Yjs/0UnamA==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-41.3.1.tgz", + "integrity": "sha512-a/LAPO+O9fwHjQ/8s3UNtyrqQRieAnpnPw2IhLlGqOS7nxPKMR2vkb6WnG2LUdO+wYqkCzxUDpBlfVkjkQEI0w==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1" } }, "@ckeditor/ckeditor5-typing": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-40.2.0.tgz", - "integrity": "sha512-2E7LkmC4RHdenMUwow0EZDKxlbX00c5UHysUVT51EBGrXiJcN++0cqxQaeJzQ262oTDpk94qE5IZdGXt3ntzrw==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-41.3.1.tgz", + "integrity": "sha512-4Oeafc3if6fTITOest1ILQ573fnkzE9/tn5eNm3zWnHVYR79mRCYxaha9yUlKVQiqaxZ48EVo2FjHiouXmn9+Q==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", "lodash-es": "4.17.21" } }, "@ckeditor/ckeditor5-ui": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-40.2.0.tgz", - "integrity": "sha512-K8oC9zrJokZD5Nl4uQjJMo8Couds0eHmfNI/go6iU4A4OAdDzph+W50QnyMed4etKnMdhvUSbnuZnPtQjnsvFA==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-41.3.1.tgz", + "integrity": "sha512-xN7OAiRp7ALKYXUp6Qe/AjkjrhyLuoz9nxq7Jdsnsyb/XXfsXDloMcOuvNRoUgr4gIFHMOoZZxsIn8qegBvcYA==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", "color-convert": "2.0.1", "color-parse": "1.4.2", "lodash-es": "4.17.21", @@ -334,79 +333,78 @@ } }, "@ckeditor/ckeditor5-undo": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-40.2.0.tgz", - "integrity": "sha512-k2VZS5x4SJtYk3zhdwHYg+D00DgD0iWR0H4qQgcWmQMFRipYvXJRixP3hSLZGJciQanPFeYcjZgxNQ+rU1s8ug==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-41.3.1.tgz", + "integrity": "sha512-PElWTnlIwuQ94mvdhuH7Mno99oocSnOWPMHi9UuWe6+zVgznQwn0f0diBZvX3l5y8hFgK6q/pQ/CCmbvvYnovA==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1" } }, "@ckeditor/ckeditor5-upload": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-40.2.0.tgz", - "integrity": "sha512-AdJSKvWEQbSSyA/DfxbCHRhFN6S4ew4kuYETO57e6AS3aOuYGLBRdu9Mub7IAQcOyy1LL6ktr9u5WEOoWS2h0w==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-41.3.1.tgz", + "integrity": "sha512-ugTgGEgA9qsSl5+qptTmawdfYaONr6b3uTG4byZ76JMdf0qiniZjBF/TtGAVmBkCipcVWFoaZKteiz0fhQMHjA==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1" } }, "@ckeditor/ckeditor5-utils": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-40.2.0.tgz", - "integrity": "sha512-f+kTJBwwk7Y/LXm8pEPxBTXVlJwQrH7Levzye9zxEDB0Jtj7+brGr87o666fPmL/ATQc5M+VPhbvnk2sOv7WKg==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-41.3.1.tgz", + "integrity": "sha512-jJu9ndn6Y7+ffBYdDCRXX7OnV9Ddgms2HSF1pmhjZN0uoL96XworuUOn8hx3Zs/KBPjJEwbtYWJMjG9aohrgaQ==", "dev": true, "requires": { "lodash-es": "4.17.21" } }, "@ckeditor/ckeditor5-watchdog": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-40.2.0.tgz", - "integrity": "sha512-ets7o2dUR7l23G9o/RAbu+gJzUkc2Ul269E3TEhZnbQXFjshvEGK2kzuay7I+/waL3ADuYe4zuoBqsqdPoAhfg==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-41.3.1.tgz", + "integrity": "sha512-iDwdYxC8euSKxfRq4y5vVOX9GVUbEbC9z6glkXpxa1BogqYh39+fywjt+s4o3Ub3b8FJ/EUYuNc+/vK+CzEg4g==", "dev": true, "requires": { "lodash-es": "4.17.21" } }, "@ckeditor/ckeditor5-widget": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-40.2.0.tgz", - "integrity": "sha512-okeUSwbnu6TUKvwBOl0YdED6Me0/vvs1ybfKZPNEJNwGl989iG0LQO4oYUye8BTCZvzCZ2cBTb1Cvnwr8KRcbg==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-41.3.1.tgz", + "integrity": "sha512-rdBxGS3bxWNhp+yxyBYkcbRV6/mdTDab+konDVhZ/ME1jVZ5cf8OBZcgHUqAxzuWt4XMEdzKINbo1OnSDwApUg==", "dev": true, "requires": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-enter": "40.2.0", - "@ckeditor/ckeditor5-typing": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-enter": "41.3.1", + "@ckeditor/ckeditor5-typing": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", "lodash-es": "4.17.21" } }, "ckeditor5": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/ckeditor5/-/ckeditor5-40.2.0.tgz", - "integrity": "sha512-JaFuY/6DX1wbA6yRB2xQVMr+9W1C3HvSX4AT10ccoKBKe9OctIatekDt2ztV+cMaVHLF1wocskS/Ql9XFRy2Eg==", + "version": "41.3.1", + "resolved": "https://registry.npmjs.org/ckeditor5/-/ckeditor5-41.3.1.tgz", + "integrity": "sha512-pBK1YZV9Sy4R53XG70TEeLFOvTFC7tg8AmS6d6zizegtwkH8seblkcERkykcNuvmfzZ/2h9JbafJ4kisZOwiUQ==", "dev": true, "requires": { - "@ckeditor/ckeditor5-clipboard": "40.2.0", - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-enter": "40.2.0", - "@ckeditor/ckeditor5-paragraph": "40.2.0", - "@ckeditor/ckeditor5-select-all": "40.2.0", - "@ckeditor/ckeditor5-typing": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-undo": "40.2.0", - "@ckeditor/ckeditor5-upload": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "@ckeditor/ckeditor5-watchdog": "40.2.0", - "@ckeditor/ckeditor5-widget": "40.2.0" + "@ckeditor/ckeditor5-clipboard": "41.3.1", + "@ckeditor/ckeditor5-core": "41.3.1", + "@ckeditor/ckeditor5-engine": "41.3.1", + "@ckeditor/ckeditor5-enter": "41.3.1", + "@ckeditor/ckeditor5-paragraph": "41.3.1", + "@ckeditor/ckeditor5-select-all": "41.3.1", + "@ckeditor/ckeditor5-typing": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", + "@ckeditor/ckeditor5-undo": "41.3.1", + "@ckeditor/ckeditor5-upload": "41.3.1", + "@ckeditor/ckeditor5-utils": "41.3.1", + "@ckeditor/ckeditor5-watchdog": "41.3.1", + "@ckeditor/ckeditor5-widget": "41.3.1" } }, "color-convert": { diff --git a/package.json b/package.json index 81fb8111bc412cbc3738107b1d684d73de658308..621ee2fd03e6f8a9ed3e458c47314704d9c348ba 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,6 @@ "vendor-update": "drush edit_plus:update-ckeditor-version; npm install; drush edit_plus:move-library" }, "devDependencies": { - "@ckeditor\/ckeditor5-editor-inline": "40.2.0" + "@ckeditor\/ckeditor5-editor-inline": "~41.3.1" } } \ No newline at end of file diff --git a/src/Ajax/UpdateMarkup.php b/src/Ajax/UpdateMarkup.php index 9a8bf18ef7f05a0ecd0222752cadb2023f14b4cc..aaaf96261eab1c32e1e4fb5515215acb470c3ad5 100644 --- a/src/Ajax/UpdateMarkup.php +++ b/src/Ajax/UpdateMarkup.php @@ -14,10 +14,13 @@ class UpdateMarkup implements CommandInterface { * A CSS selector. * @param array $content * The content that will be updated. + * @param string|null $updatedElementId + * The rendered element to be updated. */ public function __construct( protected string $selector, protected array $content, + protected ?string $updatedElementId = NULL, ) {} public function render() { @@ -25,7 +28,7 @@ class UpdateMarkup implements CommandInterface { 'command' => 'invoke', 'selector' => $this->selector, 'method' => 'EditPlusUpdateMarkup', - 'args' => [$this->selector, $this->getRenderedContent()], + 'args' => [$this->selector, $this->getRenderedContent(), $this->updatedElementId], ]; } diff --git a/src/Controller/MultipleEntityFormController.php b/src/Controller/MultipleEntityFormController.php index ab882a96531bdd0e9fb53c762c1b1aeb3995b9a3..4ccc2ba7cc44e2ca111ecc3d095950c3f76c9428 100644 --- a/src/Controller/MultipleEntityFormController.php +++ b/src/Controller/MultipleEntityFormController.php @@ -2,21 +2,21 @@ namespace Drupal\edit_plus\Controller; -use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\edit_plus\Ui; use Drupal\Core\Form\FormState; use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\InvokeCommand; use Drupal\Core\Ajax\AppendCommand; use Drupal\edit_plus\EditPlusFormTrait; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Controller\ControllerBase; -use Drupal\edit_plus\Ui; -use Drupal\field_sample_value\SampleValueEntityGeneratorInterface; -use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Drupal\edit_plus\EditPlusTempstoreRepository; +use Psr\EventDispatcher\EventDispatcherInterface; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\field_sample_value\SampleValueEntityGeneratorInterface; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; /** @@ -28,7 +28,6 @@ final class MultipleEntityFormController extends ControllerBase { use StringTranslationTrait; public function __construct( - protected Ui $ui, protected Request $request, protected EventDispatcherInterface $dispatcher, protected ArgumentResolverInterface $argumentResolver, @@ -39,7 +38,6 @@ final class MultipleEntityFormController extends ControllerBase { public static function create(ContainerInterface $container) { return new static ( - $container->get('edit_plus.ui'), $container->get('request_stack')->getCurrentRequest(), $container->get('event_dispatcher'), $container->get('http_kernel.controller.argument_resolver'), @@ -83,10 +81,11 @@ final class MultipleEntityFormController extends ControllerBase { } $response = new AjaxResponse(); - $response->addCommand(new AppendCommand('#toolbar-plus-sidebar', $form)); - $response->addCommand(new InvokeCommand('#toolbar-plus-sidebar', 'show')); + $response->addCommand(new AppendCommand('#toolbar-plus-right-sidebar', $form)); return $response; } + public function entityContent(array &$form, FormStateInterface $form_state, string $view_mode = NULL) {} + } diff --git a/src/Drush/Commands/UpdateInlineEditor.php b/src/Drush/Commands/UpdateInlineEditor.php index 73f6d1deaf78c9da9b0f98725673a0de588d6d64..419a39bac37221139fd6f94a03ea9843865db15f 100644 --- a/src/Drush/Commands/UpdateInlineEditor.php +++ b/src/Drush/Commands/UpdateInlineEditor.php @@ -17,8 +17,8 @@ final class UpdateInlineEditor extends DrushCommands { #[CLI\Command(name: 'edit_plus:update-ckeditor-version', aliases: ['e+_update'])] #[CLI\Usage(name: 'e+_update', description: 'Extracts the ckeditor version that core is currently using and updates Edit+\'s package.json.')] public function updateVersion() { - $cores_package_lock = Json::decode(file_get_contents(DRUPAL_ROOT . '/core/package-lock.json')); - $ckeditor5_version = $cores_package_lock['packages']['node_modules/ckeditor5']['version']; + $cores_packages = Json::decode(file_get_contents(DRUPAL_ROOT . '/core/package.json')); + $ckeditor5_version = $cores_packages['devDependencies']['ckeditor5']; $edit_plus_path = \Drupal::service('extension.list.module')->getPath('edit_plus'); // @todo inject. $package_json_path = DRUPAL_ROOT . "/$edit_plus_path/package.json"; $edit_plus_packages = Json::decode(file_get_contents($package_json_path)); diff --git a/src/EditPlusFormTrait.php b/src/EditPlusFormTrait.php index 23927710ab9360a51ca002ee1a848ab76a7a2e53..661a3b62be3a2c29d36a6000724e4bc03e6dfa55 100644 --- a/src/EditPlusFormTrait.php +++ b/src/EditPlusFormTrait.php @@ -12,18 +12,10 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\edit_plus\Ajax\UpdateMarkup; use Drupal\Core\Form\FormStateInterface; use Drupal\edit_plus\Event\AddEmptyField; -use Drupal\Core\Render\RendererInterface; use Drupal\edit_plus\Event\FieldAttributes; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\EntityDisplayRepositoryInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Drupal\field_sample_value\SampleValueEntityGeneratorInterface; trait EditPlusFormTrait { - public $editPlusTempStore; - public function prepareFormForInlineEditing(&$form, FormStateInterface $form_state) { $entity = $this->getFormEntity($form, $form_state); $entity = $this->editPlusTempstoreRepository->get($entity); @@ -40,6 +32,23 @@ trait EditPlusFormTrait { 'id' => 'empty-field', ], ]; + + // Pass the view mode along to subsequent page requests. + $form['view_mode'] = [ + '#type' => 'hidden', + '#attributes' => ['class' => ['edit-plus-view-mode']], + '#value' => $this->getViewMode($form, $form_state, $entity), + ]; + + // Let entity-form.js updateTempstore flag to only update the markup of the + // changed editable element instead of the entire entity. This allows users + // to continue editing other fields. + $form['only_update_element'] = [ + '#type' => 'hidden', + '#attributes' => ['class' => ['edit-plus-only-update-element']], + '#value' => NULL, + ]; + foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { if (!empty($form[$field_name])) { $this->addFieldToPageButton($form, $entity, $field_name); @@ -61,6 +70,7 @@ trait EditPlusFormTrait { $entity = $this->getFormEntity($form, $form_state); $entity = $this->editPlusTempstoreRepository->get($entity); $view_mode = $this->getViewMode($form, $form_state, $entity); + // Add attributes to associate the form item with the rendered markup. foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { if (!empty($form[$field_name])) { $event = new FieldAttributes($form, $view_mode, $field_name, $entity, $form_state); @@ -113,7 +123,7 @@ trait EditPlusFormTrait { * Generate field values before adding an empty field to the page. */ public function populateEmptyField(&$form, FormStateInterface $form_state) { - $entity = $this->editPlusTempStore->get($this->getFormEntity($form, $form_state)); + $entity = $this->editPlusTempstoreRepository->get($this->getFormEntity($form, $form_state)); $input = $form_state->getUserInput(); [$_, $field_name] = explode('::', $input['_triggering_element_name']); $field = $entity->get($field_name); @@ -141,10 +151,8 @@ trait EditPlusFormTrait { */ public function addEmptyField(&$form, FormStateInterface $form_state) { $response = new AjaxResponse(); - $this->messenger()->addWarning($this->t('You have unsaved changes.')); $this->updatePage($response, $form, $form_state); $this->updateForm($response, $form, $form_state); - $this->buildBottomBar($this->getMainEntity($form, $form_state), $response); $this->renderMessages($response); return $response; @@ -163,11 +171,42 @@ trait EditPlusFormTrait { public function updatePage(AjaxResponse $response, array &$form, FormStateInterface $form_state) { $entity = $this->getFormEntity($form, $form_state); $view_mode = $this->getViewMode($form, $form_state, $entity); + $view_mode = _toolbar_plus_get_view_mode($entity, $view_mode); $content = $this->entityContent($form, $form_state, $view_mode); - $selector = sprintf('[data-edit-plus-entity-wrapper="%s::%s::%s::%s"]', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity), $view_mode, $entity->bundle()); - $response->addCommand(new UpdateMarkup($selector, $content)); + $input = $this->getUserInput($form_state); + if (!empty($content['component']['content']['#view_mode']) && $content['component']['content']['#view_mode'] !== $view_mode) { + // It's possible that the view mode was changed in this update. $view_mode + // needs to remain as it was for replacing the old content on the page, but + // let's update the view mode for passing it on down the line. + if (!empty($form['settings']['block_form']['view_mode']['#value'])) { + // @todo This only covers inline blocks I think since it looks for the new view mode in component > content > #view_mode. + // Ensure that we don't need to cover other situations like entity forms. + $form['settings']['block_form']['view_mode']['#value'] = $content['component']['content']['#view_mode']; + } + + $input['view_mode'] = $content['component']['content']['#view_mode']; + $form_state->setUserInput($input); + } + + $selector = sprintf('[data-toolbar-plus-entity-wrapper="%s::%s::%s::%s"]', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity), $view_mode, $entity->bundle()); + $response->addCommand(new UpdateMarkup($selector, $content, $input['only_update_element'])); } + /** + * Entity Content + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param string|NULL $view_mode + * The view mode. + * + * @return array + * A render array of the entity content to return as the updated page. + */ + abstract public function entityContent(array &$form, FormStateInterface $form_state, string $view_mode = NULL); + /** * Update the entity form. * @@ -213,7 +252,8 @@ trait EditPlusFormTrait { } public static function getEditPlusFormId(EntityInterface $entity) { - return "edit-plus-form_{$entity->getEntityTypeId()}-{$entity->id()}"; + $id = edit_plus_entity_identifier($entity); + return "edit-plus-form_{$entity->getEntityTypeId()}-{$id}"; } /** @@ -237,28 +277,14 @@ trait EditPlusFormTrait { '#tree' => FALSE, '#attributes' => [ 'id' => self::getEditPlusFormId($entity), - 'class' => ['edit-plus-form'], + 'class' => ['edit-plus-form', 'toolbar-plus-sidebar', 'right-sidebar'], 'data-edit-plus-form-id' => sprintf('%s::%s', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity)), + 'data-offset-right' => '', ], 'form' => $form, ]; } - /** - * Build bottom bar. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to build the bottom bar for. - * @param \Drupal\Core\Ajax\AjaxResponse $response - * The ajax response. - * - * @throws \Drupal\Core\Entity\EntityMalformedException - */ - public function buildBottomBar(EntityInterface $entity, AjaxResponse $response) { - $page_bottom['bottom_bar']['edit_plus_bottom_bar'] = $this->ui->buildBottomBar([$entity->getEntityTypeId() . '.' . $entity->id() => $entity], $entity->toUrl()->toString()); - $response->addCommand(new InsertCommand('#toolbar-plus-bottom-bar', $page_bottom)); - } - public function renderMessages(AjaxResponse $response) { $status_messages = ['#type' => 'status_messages']; $messages = $this->renderer->renderRoot($status_messages); @@ -280,34 +306,32 @@ trait EditPlusFormTrait { * The view mode of the entity being edited. */ public function getViewMode(array &$form, FormStateInterface $form_state, EntityInterface $entity) { - $input = $this->getUserInput($form_state); - // View mode comes from the initial JS that loads the form or from the previously // loaded form for subsequent form loads. e.g. Adding an empty field to the page. $view_mode = $this->request->get('viewMode'); - if (!empty($view_mode)) { - // Sanitize view mode. - $valid_view_modes = $this->entityDisplayRepository->getViewModes($entity->getEntityTypeId()); - $valid_view_modes['default'] = TRUE; - if (empty($valid_view_modes[$view_mode])) { - throw new \InvalidArgumentException((string) t('Edit +: Invalid view mode for @label', ['@label' => $entity->label()])); - } - } else { - $view_mode = $input['view_mode'] ?? NULL; - } if (empty($view_mode)) { - $view_mode = 'full'; + // When the form is submitted we are going to need to update the rendered page + // so we need the view mode. Make the form aware of this value so we can set + // it with JS before submitting. + $input = $form_state->getUserInput(); + if (!empty($input['settings']['block_form']['view_mode'])) { + // Block content entity form. + $view_mode = $input['settings']['block_form']['view_mode']; + } + elseif (!empty($input['view_mode'])) { + // Entity form. + $view_mode = $input['view_mode']; + } } - - // Pass the view mode along to subsequent page requests. - if (empty($form['view_mode']['#value'])) { - $form['view_mode'] = [ - '#type' => 'hidden', - '#value' => $view_mode, - ]; + // Sanitize view mode. + $valid_view_modes = $this->entityDisplayRepository->getViewModes($entity->getEntityTypeId()); + $valid_view_modes['default'] = TRUE; + if (empty($valid_view_modes[$view_mode])) { + throw new \InvalidArgumentException((string) t('Edit +: Invalid view mode for @label', ['@label' => $entity->label()])); } return $view_mode; } } + diff --git a/src/EditPlusMessagesTrait.php b/src/EditPlusMessagesTrait.php index 68fac0072d1b78f46a578417fafbb1ae9d1a0a89..b6bb1584f0d567d7dc844254ffc59d4cfb545428 100644 --- a/src/EditPlusMessagesTrait.php +++ b/src/EditPlusMessagesTrait.php @@ -8,6 +8,8 @@ trait EditPlusMessagesTrait { * Clear unsaved changes message. */ public function clearUnsavedChangesMessage() { + // @todo Remove this and update things that call it? + // now we are removing all of these messages in lb_plus_preprocess_status_messages $messenger = \Drupal::messenger(); $all_messages = $messenger->all(); if (!empty($all_messages['warning'])) { diff --git a/src/Event/FieldProperties.php b/src/Event/FieldProperties.php new file mode 100644 index 0000000000000000000000000000000000000000..d8fd4bc0fda883d84766afdc7e82fa4ff9916467 --- /dev/null +++ b/src/Event/FieldProperties.php @@ -0,0 +1,69 @@ +<?php + +namespace Drupal\edit_plus\Event; + +use Drupal\Component\EventDispatcher\Event; +use Drupal\Core\Entity\EntityInterface; + +class FieldProperties extends Event { + + /** + * An array of field properties. + */ + private array $properties; + + /** + * @var \Drupal\Core\Entity\EntityInterface + */ + private EntityInterface $entity; + + /** + * The field name. + */ + private string $fieldName; + + public function __construct(EntityInterface $entity, string $field_name, array $properties) { + $this->properties = $properties; + $this->entity = $entity; + $this->fieldName = $field_name; + } + + /** + * Get properties. + * + * @return + * An array of properties on this field. + */ + public function getProperties() { + return $this->properties; + } + + /** + * @param array $properties + * An array of field properties. + */ + public function setProperties(array $properties) { + $this->properties = $properties; + } + + /** + * Get entity. + * + * @return \Drupal\Core\Entity\EntityInterface + * The entity that this field is on. + */ + public function getEntity(): EntityInterface { + return $this->entity; + } + + /** + * Get field name. + * + * @return string + * The field name. + */ + public function getFieldName(): string { + return $this->fieldName; + } + +} diff --git a/src/EventSubscriber/AttributesTrait.php b/src/EventSubscriber/AttributesTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..6eff7dcefd4b718124c30f10fdaab4c5ed16f42e --- /dev/null +++ b/src/EventSubscriber/AttributesTrait.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\edit_plus\EventSubscriber; + +use Drupal\edit_plus\Event\FieldAttributes; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; + +trait AttributesTrait { + + protected static function getWidget(FieldAttributes $event): ?string { + $field_name = $event->getFieldName(); + $form_display = $event->getFormState()->get('form_display'); + if (empty($form_display)) { + // Must not be an entity form, load the form display. + $entity = $event->getEntity(); + $form_display = static::getEntityDisplayRepository()->getFormDisplay($entity->getEntityTypeId(), $entity->bundle()); + } + + return $form_display->getComponent($field_name)['type']; + } + + /** + * Get entity display repository. + * + * @return \Drupal\Core\Entity\EntityDisplayRepositoryInterface + * The entity display repository service. + */ + protected static function getEntityDisplayRepository(): EntityDisplayRepositoryInterface { + return \Drupal::service('entity_display.repository'); + } + +} diff --git a/src/EventSubscriber/CloneFieldAttribute.php b/src/EventSubscriber/CloneFieldAttribute.php deleted file mode 100644 index 1445f9581155fb38899b478b43229ea95f65d5be..0000000000000000000000000000000000000000 --- a/src/EventSubscriber/CloneFieldAttribute.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\edit_plus\EventSubscriber; - -use Drupal\edit_plus\Event\FieldAttributes; -use Drupal\Core\Field\FieldDefinitionInterface; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - -/** - * Clone field attribute. - */ -class CloneFieldAttribute implements EventSubscriberInterface { - - const DO_NOT_CLONE = [ - 'entity_autocomplete', - 'inline_textarea', - ]; - - public function addFormItemAttributesAlter(FieldAttributes $event) { - $field_name = $event->getFieldName(); - $form = $event->getForm(); - $form_item = &$form[$field_name]; - - if (empty($form_item['#attributes']['data-edit-plus-clone'])) { - $field_definition = $event->getEntity()->getFieldDefinition($field_name); - $clone = $this->getClonedSetting($field_definition) ?? $this->getCloneDefault($form_item, $field_definition); - $form_item['#attributes']['data-edit-plus-clone'] = $clone; - $event->setForm($form); - } - - } - - private function getClonedSetting(FieldDefinitionInterface $field_definition) { - if (method_exists($field_definition, 'getThirdPartySettings')) { - $third_party_settings = $field_definition->getThirdPartySettings('edit_plus'); - if (!empty($third_party_settings['clone'])) { - return $third_party_settings['clone']; - } - } - return NULL; - } - - private function getCloneDefault(array $form_item, FieldDefinitionInterface $field_definition) { - $type = edit_plus_find_widget_type($form_item, $field_definition); - if (!empty($type) && in_array($type, self::DO_NOT_CLONE)) { - return 'false'; - } - return 'true'; - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents(): array { - return [ - FieldAttributes::ALTER => ['addFormItemAttributesAlter'], - ]; - } - -} diff --git a/src/EventSubscriber/DefaultFieldAttributes.php b/src/EventSubscriber/DefaultFieldAttributes.php index 1be1c9e41b0e4cb15bb18074973c1029a13dd880..6fbe0b9ff5b10548de81af8a8203c3769e6713ab 100644 --- a/src/EventSubscriber/DefaultFieldAttributes.php +++ b/src/EventSubscriber/DefaultFieldAttributes.php @@ -9,6 +9,9 @@ use Drupal\Core\Template\Attribute; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\edit_plus\Event\FieldAttributes; +use Drupal\edit_plus\Event\FieldProperties; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -16,6 +19,12 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; */ class DefaultFieldAttributes implements EventSubscriberInterface { + use AttributesTrait; + + public function __construct( + protected EventDispatcherInterface $eventDispatcher, + ) {} + /** * Add form item attributes. * @@ -31,9 +40,17 @@ class DefaultFieldAttributes implements EventSubscriberInterface { $form_item = &$form[$field_name]; $entity = $event->getEntity(); $form_item['#attributes']['data-edit-plus-form-item-wrapper-id'] = sprintf('%s::%s::%s::%s', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity), $field_name, 'value'); - $form_item['#attributes']['data-edit-plus-form-item-widget'] = edit_plus_find_widget_type($form_item, $entity->getFieldDefinition($field_name)); + $form_item['#attributes']['data-edit-plus-form-item-widget'] = self::getWidget($event); $form_item['#attributes']['class'][] = 'edit-plus-form-item-wrapper'; - $form_item['#attributes']['class'][] = 'edit-plus-hidden'; + + $display = EntityViewDisplay::collectRenderDisplay($entity, $event->getViewMode())->toArray(); + if (empty($display['hidden'][$field_name])) { + // Hide form items of elements that will be edited inline. + $form_item['#attributes']['class'][] = 'edit-plus-hidden'; + } else { + // If the field is hidden in the view mode, it can't be edited inline. + $form_item['#attributes']['data-edit-plus-auto-submit'] = ''; + } $event->setForm($form); } @@ -44,7 +61,11 @@ class DefaultFieldAttributes implements EventSubscriberInterface { $entity = $event->getEntity(); $field_name = $event->getFieldName(); $form_item = &$form[$field_name]; + $properties = $entity->getFieldDefinition($field_name)->getFieldStorageDefinition()->getPropertyNames(); + // Allow modules to specify custom properties. + $properties = $this->eventDispatcher->dispatch(new FieldProperties($entity, $field_name, $properties), FieldProperties::class)->getProperties(); + if (!empty($form_item['widget']['#theme']) && $form_item['widget']['#theme'] === 'field_multiple_value_form') { foreach (Element::children($form_item['widget']) as $delta) { foreach ($properties as $property) { diff --git a/src/EventSubscriber/EntityTemplate.php b/src/EventSubscriber/EntityTemplate.php deleted file mode 100644 index ddd37a95e82b6e7e10b2935dcda768898d892c76..0000000000000000000000000000000000000000 --- a/src/EventSubscriber/EntityTemplate.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php declare(strict_types = 1); - -namespace Drupal\edit_plus\EventSubscriber; - -use Drupal\Core\Render\Markup; -use Drupal\twig_events\Event\TwigRenderTemplateEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - -final class EntityTemplate implements EventSubscriberInterface { - - public function onTwigRenderTemplate(TwigRenderTemplateEvent $event): void { - $variables = $event->getVariables(); - $entity_info = $this->getEntityInfo($variables); - if (!empty($entity_info)) { - // Give the rendered entity an AJAX wrapper so it can be updated as - // changes are made. - $output = $event->getOutput(); - $wrapped_output = sprintf('<div class="edit-plus-entity-wrapper" data-edit-plus-entity-wrapper="%s::%s::%s::%s">%s</div>', $entity_info['entity_type'], $entity_info['entity_id'], $entity_info['view_mode'], $entity_info['bundle'], $output->__toString()); - $event->setOutput(Markup::create($wrapped_output)); - } - } - - - private function getEntityInfo(array $variables) { - if (!empty($variables['elements']['#edit_plus_entity'])) { - // Regular entities. - return $variables['elements']['#edit_plus_entity']; - } elseif (!empty($variables['elements']['content']['#edit_plus_entity'])) { - // Inline blocks. - return $variables['elements']['content']['#edit_plus_entity']; - } - } - - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents(): array { - return [ - TwigRenderTemplateEvent::class => ['onTwigRenderTemplate'], - ]; - } - -} diff --git a/src/EventSubscriber/HandleFieldAttribute.php b/src/EventSubscriber/HandleFieldAttribute.php index 08fee879e046234244b44b92dcfb411f86145d1e..9ae00e34de9cbbc41263ac1fa54a74b913be45c5 100644 --- a/src/EventSubscriber/HandleFieldAttribute.php +++ b/src/EventSubscriber/HandleFieldAttribute.php @@ -10,6 +10,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Handle field attribute. + * + * The "handle" is what DOM element will be used when swapping the form item + * into the markup of the page for editing. Options: + * - form_item: */ class HandleFieldAttribute implements EventSubscriberInterface { diff --git a/src/EventSubscriber/InlineEditorFieldAttributes.php b/src/EventSubscriber/InlineEditorFieldAttributes.php index 63c52bb64dcf0758b5c6e6b198a04ff0240e68e4..643f73aaea96f260f83708a6a57d0a8c80229b6d 100644 --- a/src/EventSubscriber/InlineEditorFieldAttributes.php +++ b/src/EventSubscriber/InlineEditorFieldAttributes.php @@ -13,8 +13,11 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; */ class InlineEditorFieldAttributes implements EventSubscriberInterface { + use AttributesTrait; + const INLINE_EDITOR_WIDGET_MAPPING = [ - 'text_format' + 'text_textarea', + 'text_textarea_with_summary', ]; /** @@ -30,13 +33,12 @@ class InlineEditorFieldAttributes implements EventSubscriberInterface { $field_name = $event->getFieldName(); $form = $event->getForm(); $form_item = &$form[$field_name]; - if (!empty($form_item['widget'])) { + $widget = self::getWidget($event); + + if (in_array($widget, static::INLINE_EDITOR_WIDGET_MAPPING)) { foreach (Element::children($form_item['widget']) as $delta) { if (!empty($form_item['widget'][$delta]['#type'])) { - $type =& $form_item['widget'][$delta]['#type']; - if (in_array($type, static::INLINE_EDITOR_WIDGET_MAPPING)) { - $type = 'inline_textarea'; - } + $form_item['widget'][$delta]['#type'] = 'inline_textarea'; } } } diff --git a/src/EventSubscriber/MediaFieldAttributes.php b/src/EventSubscriber/MediaFieldAttributes.php index 9d684b7b4be934f930d6e98471b0e37088e74da9..1b34675aa92d7e772ca37db763a2ac89884a779c 100644 --- a/src/EventSubscriber/MediaFieldAttributes.php +++ b/src/EventSubscriber/MediaFieldAttributes.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace Drupal\edit_plus\EventSubscriber; +use Drupal\Core\Render\Element; use Drupal\edit_plus\Event\FieldAttributes; +use Drupal\media_library\MediaLibraryState; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -12,6 +14,8 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; */ class MediaFieldAttributes implements EventSubscriberInterface { + use AttributesTrait; + /** * Add form item attributes. * @@ -31,12 +35,40 @@ class MediaFieldAttributes implements EventSubscriberInterface { $form_item['#attributes']['class'][] = 'edit-plus-hidden'; $form_item['#attributes']['class'][] = 'edit-plus-form-item-wrapper'; $form_item['#attributes']['data-edit-plus-handle'] = 'wrapper'; - $form_item['#attributes']['data-edit-plus-clone'] = 'true'; $form_item['#attributes']['data-edit-plus-form-item-wrapper-id'] = sprintf('%s::%s::%s::%s', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity), $field_name, 'value'); + $form_item['#attributes']['data-edit-plus-form-item-widget'] = self::getWidget($event); $form_item['widget']['#type'] = 'container'; $form_item['widget']['#attributes']['class'][] = 'edit-plus-form-item'; $form_item['widget']['#attributes']['data-edit-plus-form-item-id'] = sprintf('%s::%s::%s::%s::%s', $entity->getEntityTypeId(), edit_plus_entity_identifier($entity), $field_name, 0, 'value'); - $form_item['widget']['#attributes']['data-edit-plus-form-item-widget'] = edit_plus_find_widget_type($form_item, $entity->getFieldDefinition($field_name)); + + // There could be multiple identical field widgets on the same page. e.g. + // Say we have a custom block with a field_hero_media field on it. If we + // have two of the same block types being edited that means two forms on + // the page with the same field. The AJAX wrapper in MediaLibraryWidget is + // $field_name . '-media-library-wrapper' . $id_suffix; so we'd have duplicate + // ID's. Let's add a little more specificity when we are clicking the add and + // remove buttons on the block form. + // @see https://www.drupal.org/project/drupal/issues/3345064#comment-15774869 + $wrapper_id = $form_item['widget']['#attributes']['id'] = $form_item['widget']['#attributes']['id'] . '-' . $entity->uuid(); + foreach (Element::children($form_item['widget']['selection']) as $key) { + $form_item['widget']['selection'][$key]['remove_button']['#ajax']['wrapper'] + = $form_item['widget']['media_library_update_widget']['#ajax']['wrapper'] + = $wrapper_id; + } + // Add more specificity when we are clicking the Insert selected button in + // the modal. + // @see ViewsFormMediaLibraryWidgetAlter + $state = $form_item['widget']['open_button']['#media_library_state']; + $parameters = $state->getOpenerParameters(); + if (empty($parameters['field_widget_id'])) { + throw new \InvalidArgumentException('field_widget_id parameter is missing.'); + } + $parameters['field_widget_id_and_block_uuid'] = $wrapper_id; + $state->add(['media_library_opener_parameters' => $parameters]); + $state->set('hash', $state->getHash()); + $form_item['widget']['media_library_update_widget']['#attributes']['data-media-library-widget-update'] + = $form_item['widget']['media_library_selection']['#attributes']['data-media-library-widget-value'] + = $parameters['field_widget_id_and_block_uuid']; $event->setForm($form); $event->stopPropagation(); @@ -59,15 +91,7 @@ class MediaFieldAttributes implements EventSubscriberInterface { * Whether the field is a Media field. */ public static function isMediaReferenceField(FieldAttributes $event): bool { - $entity = $event->getEntity(); - $field_name = $event->getFieldName(); - $field_definition = $entity->getFieldDefinition($field_name); - $target_type = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); - $form = $event->getForm(); - return $target_type === 'media' && - !empty($form[$field_name]['widget']['selection'][0]['#theme']) && - $form[$field_name]['widget']['selection'][0]['#theme'] === 'media_library_item__widget' - ; + return self::getWidget($event) === 'media_library_widget'; } /** diff --git a/src/EventSubscriber/PopOutWidthFieldAttribute.php b/src/EventSubscriber/PopOutWidthFieldAttribute.php new file mode 100644 index 0000000000000000000000000000000000000000..cccc6df55e3837386f2d6fcaafd5650eadf7abdc --- /dev/null +++ b/src/EventSubscriber/PopOutWidthFieldAttribute.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\edit_plus\EventSubscriber; + +use Drupal\edit_plus\Event\FieldAttributes; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Pop out width field attribute. + */ +class PopOutWidthFieldAttribute implements EventSubscriberInterface { + + public function addFormItemAttributesAlter(FieldAttributes $event) { + $form = $event->getForm(); + $field_name = $event->getFieldName(); + $form_item = &$form[$field_name]; + if (empty($form_item['#attributes']['data-pop-out-width'])) { + $form_item['#attributes']['data-pop-out-width'] = '200'; + } + + $event->setForm($form); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + FieldAttributes::ALTER => ['addFormItemAttributesAlter'], + ]; + } + +} diff --git a/src/Form/FieldConfigFormAlter.php b/src/Form/FieldConfigFormAlter.php index 189d02698c94a4ea64a733da5b27663d1dd1c8b4..109430b6b84966397f9a8435fe293691c4676b8c 100644 --- a/src/Form/FieldConfigFormAlter.php +++ b/src/Form/FieldConfigFormAlter.php @@ -56,21 +56,6 @@ class FieldConfigFormAlter { ], ], ]; - $form['edit_plus']['clone'] = [ - '#type' => 'radios', - '#title' => t('Clone Form Item'), - '#default_value' => $field->getThirdPartySetting('edit_plus', 'clone') ?? 'true', - '#options' => [ - 'true' => $this->t('Use a clone of the form item'), - 'false' => $this->t('Use the original form Item'), - ], - '#description' => $this->t('Select whether the inline editing for this field uses the original form item or a clone of the original form item. The cloning is based off of the handle selected above. If a clone is used, JS forwards any mouse or input events from the clone to the hidden original form element. Cloning is the prefered method since Drupal AJAX requires the form property to be set, meaning the element remains in the form, in order for many AJAX commands to work. Use the original form item for autocomplete fields as well as really simple fields.'), - '#states' => [ - 'invisible' => [ - ':input[name="edit_plus[disable]"]' => ['checked' => TRUE], - ], - ], - ]; array_unshift($form['actions']['submit']['#submit'], [$this, 'submit']); } diff --git a/src/Form/InlineEntityFormAlter.php b/src/Form/InlineEntityFormAlter.php index 013037f4e4fc7f9362a79df6600cca5ee383130e..fa21ca308be8eacb4703cfd00812f03b51bbfa3b 100644 --- a/src/Form/InlineEntityFormAlter.php +++ b/src/Form/InlineEntityFormAlter.php @@ -5,6 +5,7 @@ namespace Drupal\edit_plus\Form; use Drupal\edit_plus\Ui; use Drupal\Core\Cache\Cache; use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\InvokeCommand; use Drupal\edit_plus\EditPlusFormTrait; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; @@ -43,7 +44,6 @@ class InlineEntityFormAlter implements EntityFormInterface { protected ModuleHandlerInterface $moduleHandler, protected RendererInterface $renderer, protected RequestStack $requestStack, - protected Ui $ui, ) { $this->request = $this->requestStack->getCurrentRequest(); } @@ -61,7 +61,6 @@ class InlineEntityFormAlter implements EntityFormInterface { $form['actions']['submit']['#submit'][] = [$this, 'update']; $form['actions']['submit']['#value'] = $this->t('Update'); - $form['actions']['submit']['#ajax']['progress'] = ['type' => 'fullscreen']; $form['actions']['submit']['#attributes']['class'][] = 'edit-plus-update-button'; $this->prepareFormForInlineEditing($form, $form_state); @@ -76,7 +75,6 @@ class InlineEntityFormAlter implements EntityFormInterface { public function update(array &$form, FormStateInterface $form_state) { $entity = $form_state->getFormObject()->getEntity(); $this->editPlusTempstoreRepository->set($entity); - $this->messenger()->addWarning($this->t('You have unsaved changes.')); Cache::invalidateTags([getCacheTag($entity)]); $form_state->setTemporaryValue('updatePage', TRUE); @@ -103,9 +101,9 @@ class InlineEntityFormAlter implements EntityFormInterface { $this->updateForm($response, $form, $form_state); } - $this->buildBottomBar($entity, $response); $this->renderMessages($response); + $response->addCommand(new InvokeCommand(NULL, 'editPlusIsDoneUpdating')); return $response; } @@ -169,7 +167,7 @@ class InlineEntityFormAlter implements EntityFormInterface { /** * {@inheritdoc} */ - public function entityContent(array &$form, FormStateInterface $form_state, string $view_mode) { + public function entityContent(array &$form, FormStateInterface $form_state, string $view_mode = NULL) { $entity = $this->editPlusTempstoreRepository->get($form_state->getFormObject()->getEntity()); return $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $view_mode); } diff --git a/src/Form/ViewsFormMediaLibraryWidgetAlter.php b/src/Form/ViewsFormMediaLibraryWidgetAlter.php new file mode 100644 index 0000000000000000000000000000000000000000..b530814d79b70f46f10969406e2ea6f8cdfa77bc --- /dev/null +++ b/src/Form/ViewsFormMediaLibraryWidgetAlter.php @@ -0,0 +1,75 @@ +<?php + +namespace Drupal\edit_plus\Form; + +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\InvokeCommand; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Ajax\CloseDialogCommand; +use Drupal\media_library\MediaLibraryState; +use Symfony\Component\HttpFoundation\Request; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\media_library\Plugin\views\field\MediaLibrarySelectForm; + +/** + */ +class ViewsFormMediaLibraryWidgetAlter { + + use StringTranslationTrait; + + /** + * Implements hook_form_FORM_ID_alter() for the 'views_form_media_library_widget' form ID. + * + * @param $form + * Forms. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public function formAlter(&$form, FormStateInterface $form_state) { + $form['actions']['submit']['#ajax']['callback'][0] = static::class; + } + + /** + * Update widget ajax callback override. + * + * There could be multiple identical field widgets on the same page. e.g. + * Say we have a custom block with a field_hero_media field on it. If we + * have two of the same block types being edited that means two forms on + * the page with the same field. The AJAX wrapper in MediaLibraryWidget is + * $field_name . '-media-library-wrapper' . $id_suffix; so we'd have duplicate + * ID's. Let's add a little more specificity when we are clicking the Insert + * selected button in the modal. + * @see https://www.drupal.org/project/drupal/issues/3345064#comment-15774869 + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * A modified version of the request from MediaLibrarySelectForm::updateWidget + * and MediaLibraryFieldWidgetOpener->getSelectionResponse that has block + * specific media widget ID selectors. + */ + public static function updateWidget(array &$form, FormStateInterface $form_state, Request $request) { + $response = MediaLibrarySelectForm::updateWidget($form, $form_state, $request); + $state = MediaLibraryState::fromRequest($request); + + $parameters = $state->getOpenerParameters(); + if (empty($parameters['field_widget_id_and_block_uuid'])) { + return $response; + } + + $widget_id = $parameters['field_widget_id_and_block_uuid']; + $commands = $response->getCommands(); + $response_with_more_specific_selectors = new AjaxResponse(); + $response_with_more_specific_selectors + ->addCommand(new InvokeCommand("[data-media-library-widget-value='$widget_id']", 'val', $commands[0]['args'])) + ->addCommand(new InvokeCommand("[data-media-library-widget-update='$widget_id']", 'trigger', ['mousedown'])) + ->addCommand(new CloseDialogCommand()); + return $response_with_more_specific_selectors; + } + +} diff --git a/src/ParamConverter/EntityConverter.php b/src/ParamConverter/EntityConverter.php index a4226faeb13479f40431d1038b4f91537cbca49b..d7c0525e3a53fb65acd0f1d63e77aba184e50dab 100644 --- a/src/ParamConverter/EntityConverter.php +++ b/src/ParamConverter/EntityConverter.php @@ -34,17 +34,41 @@ class EntityConverter extends EntityConverterBase { */ public function convert($value, $definition, $name, array $defaults) { $entity = parent::convert($value, $definition, $name, $defaults); + if (!\Drupal::currentUser()->hasPermission('access inline editing')) { + return $entity; + } // Use the Edit + tempstore version of an entity if it exist. $tempstore_entity = $this->editPlusTempstoreRepository->get($entity); - if ($tempstore_entity && $entity !== $tempstore_entity) { - $entity = $tempstore_entity; - // Flag that we have swapped this entity with a tempstore version. - edit_plus_active_tempstore_entities($entity); - $this->messenger->addWarning($this->t('You have unsaved changes.')); + if ($tempstore_entity) { + $tempstore_entity_array = $tempstore_entity->toArray(); + $entity_array = $entity->toArray(); + $difference = $this->recursiveArrayDiffAssoc($tempstore_entity_array, $entity_array); + if (!empty($difference)) { + $entity = $tempstore_entity; + // Flag that we have swapped this entity with a tempstore version. + edit_plus_active_tempstore_entities($entity); + } } return $entity; } + private function recursiveArrayDiffAssoc($array1, $array2) { + $difference = []; + + foreach ($array1 as $key => $value) { + if (is_array($value) && isset($array2[$key]) && is_array($array2[$key])) { + $diff = $this->recursiveArrayDiffAssoc($value, $array2[$key]); + if (!empty($diff)) { + $difference[$key] = $diff; + } + } elseif (!array_key_exists($key, $array2) || $value !== $array2[$key]) { + $difference[$key] = $value; + } + } + + return $difference; + } + } diff --git a/src/Plugin/Tool/EditPlus.php b/src/Plugin/Tool/EditPlus.php index 5ab61864a10db43a75a0199f95c6dc4cfc80c7bd..b87e4ece62945c7362a6460141ae65577eda02d9 100644 --- a/src/Plugin/Tool/EditPlus.php +++ b/src/Plugin/Tool/EditPlus.php @@ -18,7 +18,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; #[Tool( id: 'edit_plus', label: new TranslatableMarkup('Edit'), - + weight: 20, )] final class EditPlus extends ToolPluginBase { @@ -29,7 +29,10 @@ final class EditPlus extends ToolPluginBase { $path = $this->extensionList->getPath('edit_plus'); return [ 'mouse_icon' => "/$path/assets/text-mouse.svg", - 'toolbar_icon' => "/$path/assets/text-toolbar.svg"]; + 'toolbar_button_icons' => [ + 'edit_plus' => "/$path/assets/pencil.svg", + ], + ]; } } diff --git a/src/Plugin/Tool/edit-plus.js b/src/Plugin/Tool/edit-plus.js new file mode 100644 index 0000000000000000000000000000000000000000..4616a3305382c25c80f60a8771d8a3675f3d98f6 --- /dev/null +++ b/src/Plugin/Tool/edit-plus.js @@ -0,0 +1,19 @@ +import * as toolPluginBase from '../../../../toolbar_plus/js/toolbar_plus/tool-plugin-base.js'; + +(($, Drupal, once) => { + + /** + * Edit+'s Toolbar+ plugin + */ + class EditPlusPlugin extends toolPluginBase.ToolPluginBase { + id = 'edit_plus'; + enable = () => { + Drupal.EditPlus.EnableEditMode(); + }; + disable = () => { + return Drupal.EditPlus.DisableEditMode(); + }; + } + Drupal.ToolbarPlus.PluginManager.registerPlugin(new EditPlusPlugin()); + +})(jQuery, Drupal, once); diff --git a/src/Ui.php b/src/Ui.php deleted file mode 100644 index 31e39e7d0dac105c8324fa5eff8c8a1d1fff8497..0000000000000000000000000000000000000000 --- a/src/Ui.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -namespace Drupal\edit_plus; - -use Drupal\Core\Url; - -class Ui { - - function buildBottomBar($actively_used_tempstore_entities, $destination) { - return [ - 'toolbar_plus' => [ - '#type' => 'container', - '#attributes' => [ - 'id' => 'toolbar-plus-bottom-bar', - ], - 'edit_plus_bottom_bar' => [ - '#type' => 'container', - '#attributes' => [ - 'id' => 'edit-plus-bottom-bar', - ], - 'save' => [ - '#type' => 'link', - '#title' => t('Save'), - '#url' => Url::fromRoute('edit_plus.tempstore.save', [ - 'entities' => implode('::', array_keys($actively_used_tempstore_entities)), - ], [ - 'query' => ['destination' => $destination] - ]), - '#attributes' => [ - 'class' => ['button'], - ], - ], - 'discard_changes' => [ - '#type' => 'link', - '#title' => t('Discard changes'), - '#url' => Url::fromRoute('edit_plus.tempstore.delete', [ - 'entities' => implode('::', array_keys($actively_used_tempstore_entities)), - ], [ - 'query' => ['destination' => $destination] - ]), - '#attributes' => [ - 'class' => ['button'], - ], - ], - ], - ], - ]; - } - -} diff --git a/templates/block-property.html.twig b/templates/block-property.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..1af55ac945805e32a24bdcf0eea7241e9eee4bbf --- /dev/null +++ b/templates/block-property.html.twig @@ -0,0 +1,7 @@ +{% macro wrapper(edit_plus, type, formatter, name, property) %} + <div data-edit-plus-field-value-wrapper="{{ edit_plus.type }}::{{ edit_plus.id }}::{{ name }}::block_property"> + <div class="edit-plus-field-value" data-edit-plus-id="{{ edit_plus.type }}::{{ edit_plus.id }}::{{ name }}::0::{{ edit_plus.language }}::{{ edit_plus.view_mode }}::{{ type }}::{{ formatter }}::block_property" data-edit-plus-page-element-id="{{ edit_plus.type }}::{{ edit_plus.id }}::{{ name }}::0::block_property"> + {{ property }} + </div> + </div> +{% endmacro %} diff --git a/yarn.lock b/yarn.lock index 1c79606a4cbda3bbe3784921b6bdab77ef8fb67f..389dbf21fac58fc0c2ece5ca732a085f6abe62fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,155 +2,154 @@ # yarn lockfile v1 -"@ckeditor/ckeditor5-clipboard@40.2.0": - "integrity" "sha512-8/xPH9/i86ukcEiHdmTgNuPVJeYTrivbx5ZYqycPO4Eem7VM99gIbOe7pIYpuV+klr9ymVxIHbGyTJDJ3oUO8A==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-40.2.0.tgz" - "version" "40.2.0" - dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-engine" "40.2.0" - "@ckeditor/ckeditor5-ui" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" - "@ckeditor/ckeditor5-widget" "40.2.0" +"@ckeditor/ckeditor5-clipboard@41.3.1": + "integrity" "sha512-6S7tq6FlnHYZmPACeqdf135Jx2bTKHVY8mHQ+CHC8ZZu0XVm62vVeeSLS2IcdtYmHjf4ced1G7suTUBHlfBCLw==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-41.3.1.tgz" + "version" "41.3.1" + dependencies: + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-engine" "41.3.1" + "@ckeditor/ckeditor5-ui" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" + "@ckeditor/ckeditor5-widget" "41.3.1" "lodash-es" "4.17.21" -"@ckeditor/ckeditor5-core@40.2.0": - "integrity" "sha512-0fqIaN+ZhkXXA3mpBN+alycBzPMc8ruO8VrP0OnvCjowqZVS2HXC2AaXNBdxc75xGI3ScXIor7FsgFHxVJIYYQ==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-core/-/ckeditor5-core-40.2.0.tgz" - "version" "40.2.0" +"@ckeditor/ckeditor5-core@41.3.1": + "integrity" "sha512-h+PgPtCpS2vjO3HbKMYtddRPW+B3AJx9qpixmHJnUZMiFCmRjUZjXATjpi3j+kSQISs4L2Yghq+lsAQxyGHb+A==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-core/-/ckeditor5-core-41.3.1.tgz" + "version" "41.3.1" dependencies: - "@ckeditor/ckeditor5-engine" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" + "@ckeditor/ckeditor5-engine" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" "lodash-es" "4.17.21" -"@ckeditor/ckeditor5-editor-inline@40.2.0": - "integrity" "sha512-Ox9lQiCSv0acyKaQLCcoebBjAMRE6L6iCBN8XVeQ3u91KZV6/LOhP+CJ314c8AuH+UHPeJt9MHP6eGU0trKHGQ==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-40.2.0.tgz" - "version" "40.2.0" +"@ckeditor/ckeditor5-editor-inline@~41.3.1": + "integrity" "sha512-bAhs57qbeGT907yFsUUxdujtrNlmOTJK4DrBCsxoKSoSo8fcG4D05g/I4ehQp3A1CFYsF2Wkx58TtI/fwWRVbQ==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-41.3.1.tgz" + "version" "41.3.1" dependencies: - "ckeditor5" "40.2.0" + "ckeditor5" "41.3.1" "lodash-es" "4.17.21" -"@ckeditor/ckeditor5-engine@40.2.0": - "integrity" "sha512-sgboUX8Ps+LcEgywyT3BeK1nzLHjNVIiZU1qvRxR3ixzIw4w2xRNXCGfESWLW5Y5rv9+ypUCrX61oLnZU64PQQ==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-40.2.0.tgz" - "version" "40.2.0" +"@ckeditor/ckeditor5-engine@41.3.1": + "integrity" "sha512-Me4cnkCrknDH50db/jPczuhgzaxUhHbkh2gv8N8Ypken9ZnOPvMD9W1gCFFTLaxikpPmBQwk3u1BSjOKk3r6kw==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-41.3.1.tgz" + "version" "41.3.1" dependencies: - "@ckeditor/ckeditor5-utils" "40.2.0" + "@ckeditor/ckeditor5-utils" "41.3.1" "lodash-es" "4.17.21" -"@ckeditor/ckeditor5-enter@40.2.0": - "integrity" "sha512-GjTRaKNX8QEDJ3YYKG3GfPZfGHrcigGBxbo+1WDT7NaOsR2DA/CIZfHlAPfgJDAMV17bhWsT3gy3+oQZsExtnQ==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-40.2.0.tgz" - "version" "40.2.0" - dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-engine" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" - -"@ckeditor/ckeditor5-paragraph@40.2.0": - "integrity" "sha512-NotxWP1cKvbJSY1UwdTe/Oy1NnAj9Etsi4Z7XA908EvCsNSnFtzdMhYzLhFZJ18avrQFDa7PpSKSyN3M64CbSA==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-40.2.0.tgz" - "version" "40.2.0" - dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-ui" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" - -"@ckeditor/ckeditor5-select-all@40.2.0": - "integrity" "sha512-yaYCqhdMcoEH3BsilhweNdbOfuO/cexQ1r1/mYoBoW4CypIuAeq8J/3qLpvFaThmCRPzJBn1J7v2Yjs/0UnamA==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-40.2.0.tgz" - "version" "40.2.0" - dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-ui" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" - -"@ckeditor/ckeditor5-typing@40.2.0": - "integrity" "sha512-2E7LkmC4RHdenMUwow0EZDKxlbX00c5UHysUVT51EBGrXiJcN++0cqxQaeJzQ262oTDpk94qE5IZdGXt3ntzrw==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-40.2.0.tgz" - "version" "40.2.0" - dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-engine" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" +"@ckeditor/ckeditor5-enter@41.3.1": + "integrity" "sha512-iwhvJpfsutqcv/bf8QPMKhMolb7GtShaOT+UIDW3OXjMZaBKZOTyR8OceijwgBmZeillTaXQq9y2e9lbJd46xg==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-41.3.1.tgz" + "version" "41.3.1" + dependencies: + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-engine" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" + +"@ckeditor/ckeditor5-paragraph@41.3.1": + "integrity" "sha512-weRPLyO/1Z8PpU9+lET4gYgJ8adDuCjYiREup81URSuS1DDQ8vb3D29xA+4Ov7lwg8BaNAMCpTBdp07GHHzv6w==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-41.3.1.tgz" + "version" "41.3.1" + dependencies: + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-ui" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" + +"@ckeditor/ckeditor5-select-all@41.3.1": + "integrity" "sha512-a/LAPO+O9fwHjQ/8s3UNtyrqQRieAnpnPw2IhLlGqOS7nxPKMR2vkb6WnG2LUdO+wYqkCzxUDpBlfVkjkQEI0w==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-41.3.1.tgz" + "version" "41.3.1" + dependencies: + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-ui" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" + +"@ckeditor/ckeditor5-typing@41.3.1": + "integrity" "sha512-4Oeafc3if6fTITOest1ILQ573fnkzE9/tn5eNm3zWnHVYR79mRCYxaha9yUlKVQiqaxZ48EVo2FjHiouXmn9+Q==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-41.3.1.tgz" + "version" "41.3.1" + dependencies: + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-engine" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" "lodash-es" "4.17.21" -"@ckeditor/ckeditor5-ui@40.2.0": - "integrity" "sha512-K8oC9zrJokZD5Nl4uQjJMo8Couds0eHmfNI/go6iU4A4OAdDzph+W50QnyMed4etKnMdhvUSbnuZnPtQjnsvFA==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-40.2.0.tgz" - "version" "40.2.0" +"@ckeditor/ckeditor5-ui@41.3.1": + "integrity" "sha512-xN7OAiRp7ALKYXUp6Qe/AjkjrhyLuoz9nxq7Jdsnsyb/XXfsXDloMcOuvNRoUgr4gIFHMOoZZxsIn8qegBvcYA==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-41.3.1.tgz" + "version" "41.3.1" dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" "color-convert" "2.0.1" "color-parse" "1.4.2" "lodash-es" "4.17.21" "vanilla-colorful" "0.7.2" -"@ckeditor/ckeditor5-undo@40.2.0": - "integrity" "sha512-k2VZS5x4SJtYk3zhdwHYg+D00DgD0iWR0H4qQgcWmQMFRipYvXJRixP3hSLZGJciQanPFeYcjZgxNQ+rU1s8ug==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-40.2.0.tgz" - "version" "40.2.0" +"@ckeditor/ckeditor5-undo@41.3.1": + "integrity" "sha512-PElWTnlIwuQ94mvdhuH7Mno99oocSnOWPMHi9UuWe6+zVgznQwn0f0diBZvX3l5y8hFgK6q/pQ/CCmbvvYnovA==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-41.3.1.tgz" + "version" "41.3.1" dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-engine" "40.2.0" - "@ckeditor/ckeditor5-ui" "40.2.0" + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-engine" "41.3.1" + "@ckeditor/ckeditor5-ui" "41.3.1" -"@ckeditor/ckeditor5-upload@40.2.0": - "integrity" "sha512-AdJSKvWEQbSSyA/DfxbCHRhFN6S4ew4kuYETO57e6AS3aOuYGLBRdu9Mub7IAQcOyy1LL6ktr9u5WEOoWS2h0w==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-40.2.0.tgz" - "version" "40.2.0" +"@ckeditor/ckeditor5-upload@41.3.1": + "integrity" "sha512-ugTgGEgA9qsSl5+qptTmawdfYaONr6b3uTG4byZ76JMdf0qiniZjBF/TtGAVmBkCipcVWFoaZKteiz0fhQMHjA==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-41.3.1.tgz" + "version" "41.3.1" dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-ui" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" -"@ckeditor/ckeditor5-utils@40.2.0": - "integrity" "sha512-f+kTJBwwk7Y/LXm8pEPxBTXVlJwQrH7Levzye9zxEDB0Jtj7+brGr87o666fPmL/ATQc5M+VPhbvnk2sOv7WKg==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-40.2.0.tgz" - "version" "40.2.0" +"@ckeditor/ckeditor5-utils@41.3.1": + "integrity" "sha512-jJu9ndn6Y7+ffBYdDCRXX7OnV9Ddgms2HSF1pmhjZN0uoL96XworuUOn8hx3Zs/KBPjJEwbtYWJMjG9aohrgaQ==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-41.3.1.tgz" + "version" "41.3.1" dependencies: "lodash-es" "4.17.21" -"@ckeditor/ckeditor5-watchdog@40.2.0": - "integrity" "sha512-ets7o2dUR7l23G9o/RAbu+gJzUkc2Ul269E3TEhZnbQXFjshvEGK2kzuay7I+/waL3ADuYe4zuoBqsqdPoAhfg==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-40.2.0.tgz" - "version" "40.2.0" +"@ckeditor/ckeditor5-watchdog@41.3.1": + "integrity" "sha512-iDwdYxC8euSKxfRq4y5vVOX9GVUbEbC9z6glkXpxa1BogqYh39+fywjt+s4o3Ub3b8FJ/EUYuNc+/vK+CzEg4g==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-41.3.1.tgz" + "version" "41.3.1" dependencies: "lodash-es" "4.17.21" -"@ckeditor/ckeditor5-widget@40.2.0": - "integrity" "sha512-okeUSwbnu6TUKvwBOl0YdED6Me0/vvs1ybfKZPNEJNwGl989iG0LQO4oYUye8BTCZvzCZ2cBTb1Cvnwr8KRcbg==" - "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-40.2.0.tgz" - "version" "40.2.0" - dependencies: - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-engine" "40.2.0" - "@ckeditor/ckeditor5-enter" "40.2.0" - "@ckeditor/ckeditor5-typing" "40.2.0" - "@ckeditor/ckeditor5-ui" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" +"@ckeditor/ckeditor5-widget@41.3.1": + "integrity" "sha512-rdBxGS3bxWNhp+yxyBYkcbRV6/mdTDab+konDVhZ/ME1jVZ5cf8OBZcgHUqAxzuWt4XMEdzKINbo1OnSDwApUg==" + "resolved" "https://registry.npmjs.org/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-41.3.1.tgz" + "version" "41.3.1" + dependencies: + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-engine" "41.3.1" + "@ckeditor/ckeditor5-enter" "41.3.1" + "@ckeditor/ckeditor5-typing" "41.3.1" + "@ckeditor/ckeditor5-ui" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" "lodash-es" "4.17.21" -"ckeditor5@40.2.0": - "integrity" "sha512-JaFuY/6DX1wbA6yRB2xQVMr+9W1C3HvSX4AT10ccoKBKe9OctIatekDt2ztV+cMaVHLF1wocskS/Ql9XFRy2Eg==" - "resolved" "https://registry.npmjs.org/ckeditor5/-/ckeditor5-40.2.0.tgz" - "version" "40.2.0" - dependencies: - "@ckeditor/ckeditor5-clipboard" "40.2.0" - "@ckeditor/ckeditor5-core" "40.2.0" - "@ckeditor/ckeditor5-engine" "40.2.0" - "@ckeditor/ckeditor5-enter" "40.2.0" - "@ckeditor/ckeditor5-paragraph" "40.2.0" - "@ckeditor/ckeditor5-select-all" "40.2.0" - "@ckeditor/ckeditor5-typing" "40.2.0" - "@ckeditor/ckeditor5-ui" "40.2.0" - "@ckeditor/ckeditor5-undo" "40.2.0" - "@ckeditor/ckeditor5-upload" "40.2.0" - "@ckeditor/ckeditor5-utils" "40.2.0" - "@ckeditor/ckeditor5-watchdog" "40.2.0" - "@ckeditor/ckeditor5-widget" "40.2.0" +"ckeditor5@41.3.1": + "integrity" "sha512-pBK1YZV9Sy4R53XG70TEeLFOvTFC7tg8AmS6d6zizegtwkH8seblkcERkykcNuvmfzZ/2h9JbafJ4kisZOwiUQ==" + "resolved" "https://registry.npmjs.org/ckeditor5/-/ckeditor5-41.3.1.tgz" + "version" "41.3.1" + dependencies: + "@ckeditor/ckeditor5-clipboard" "41.3.1" + "@ckeditor/ckeditor5-core" "41.3.1" + "@ckeditor/ckeditor5-engine" "41.3.1" + "@ckeditor/ckeditor5-enter" "41.3.1" + "@ckeditor/ckeditor5-paragraph" "41.3.1" + "@ckeditor/ckeditor5-select-all" "41.3.1" + "@ckeditor/ckeditor5-typing" "41.3.1" + "@ckeditor/ckeditor5-ui" "41.3.1" + "@ckeditor/ckeditor5-undo" "41.3.1" + "@ckeditor/ckeditor5-upload" "41.3.1" + "@ckeditor/ckeditor5-utils" "41.3.1" + "@ckeditor/ckeditor5-watchdog" "41.3.1" + "@ckeditor/ckeditor5-widget" "41.3.1" "color-convert@2.0.1": "integrity" "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="