diff --git a/build/css/components/drag-hint.css b/build/css/components/drag-hint.css index 65400868988160b2de3775921b66aba37a0e000f..1e98353fafbe014122a0211bfe227a23249a1768 100644 --- a/build/css/components/drag-hint.css +++ b/build/css/components/drag-hint.css @@ -1,3 +1,15 @@ .lp-hint { + z-index: 1000; +} +.lp-hint[data-orientation="vertical"] { + top: 0; + border-right: 5px solid blue; +} +.lp-hint[data-orientation="horizontal"] { + left: 0; + width: 100%; border-top: 5px solid blue; } +.gu-mirror { + opacity: 1 !important; +} diff --git a/build/css/components/form.css b/build/css/components/form.css index 9538670d8225415a97dba5ef462c3b68ed8b379e..879f250549fdee8a253c89b9adba32c3d1c9a892 100644 --- a/build/css/components/form.css +++ b/build/css/components/form.css @@ -52,7 +52,7 @@ mercury-dialog .form-item__description { .layout-paragraphs-component-form details .layout-paragraphs-component-form details:hover { border-color: var(--me-color-border-focus); } -.layout-paragraphs-component-form input:not([type="checkbox"]):not([type="radio"]), +.layout-paragraphs-component-form input:not([type="checkbox"]):not([type="radio"]):not(.media-library-item__remove), .layout-paragraphs-component-form textarea, .layout-paragraphs-component-form select { height: auto; diff --git a/build/css/components/frontend-builder.css b/build/css/components/frontend-builder.css index 1f0800c003d33412f52e3f2f93860e1d33c85701..5c6a444c3cb0ccdaef8624c7140860620e90310f 100644 --- a/build/css/components/frontend-builder.css +++ b/build/css/components/frontend-builder.css @@ -29,19 +29,23 @@ * Component hover styles. Uses a class instead of hover state for slight * pause, to avoid jumpiness. */ -.js-lpb-component.focused { +.lp-builder:not(.is-navigating) .js-lpb-component.focused { outline: 1px solid blue; + z-index: 1000; } -.js-lpb-component.focused .js-lpb-region { +.lp-builder:not(.is-navigating) .js-lpb-component.focused .js-lpb-region { outline: 1px dotted rgba(0, 0, 255, 0.5); } -.js-lpb-component.focused > .js-lpb-ui { +.lp-builder:not(.is-navigating) .js-lpb-component.focused > .js-lpb-ui { opacity: 1; } /** * Mercury Editor controls modifications. */ +.is-mercury-edit-mode .lp-builder { + z-index: 100; + } .is-mercury-edit-mode .lpb-controls { padding: 0 5px 0 0; border-radius: 4px; @@ -72,6 +76,25 @@ font-size: .7em; letter-spacing: 2px; } +.is-mercury-edit-mode .lpb-tooltiptext { + left: var(--me-lpb-tooltip-text-left, -12px); + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); + width: 1px; + height: 1px; + word-wrap: normal; + } +.is-mercury-edit-mode .lp-builder:not(.is-dragging) .lpb-tooltip--focus:focus + .lpb-tooltiptext, + .is-mercury-edit-mode .lp-builder:not(.is-dragging) .lpb-tooltip--hover:hover + .lpb-tooltiptext, + .is-mercury-edit-mode .lpb-tooltiptext--visible { + overflow: visible; + clip: auto; + width: auto; + height: auto; + } +.is-mercury-edit-mode .lpb-tooltiptext::after { + left: var(--me-lpb-tooltip-text-arrow-left, 20px); + } @keyframes controlsSlideOpen { 0% { diff --git a/build/css/menu.css b/build/css/menu.css index 100a7aa14782a57b0d854f5829280d78e0ce30fb..1b2cd21dcd2bbef90404ce6e9c59948d11d4351d 100644 --- a/build/css/menu.css +++ b/build/css/menu.css @@ -88,3 +88,6 @@ .lpb-component-list__item.type-card a::before { background: url('../../images/menu-icons/icon-card.png'); } +.lpb-component-list__item.type-tabs a::before { + background: url('../../images/menu-icons/icon-tabs.png'); + } diff --git a/build/js/component-form.js b/build/js/component-form.js index cc0b128c1835e333b64f72f284672bf5ce8efb6d..3abce73eb09dfe97e7c758ec178f03adf2392476 100644 --- a/build/js/component-form.js +++ b/build/js/component-form.js @@ -13,7 +13,7 @@ layoutSelect.focus(); } } - const form = once('me-component-form', '.layout-paragraphs-component-form')[0]; + const form = once('me-component-form', 'mercury-dialog .layout-paragraphs-component-form')[0]; if (form) { form.closest('mercury-dialog').addEventListener('open', (e) => { const dialog = e.target.shadowRoot.querySelector('dialog'); diff --git a/build/js/component-form.min.js b/build/js/component-form.min.js index cb8f753928a3b283a78c48ea58db1326d43d7071..652d4840e3330c9fc90af71101bab771e51eda9b 100644 --- a/build/js/component-form.min.js +++ b/build/js/component-form.min.js @@ -1 +1 @@ -!function(){"use strict";((t,e)=>{t.behaviors.mercuryEditorComponentForm={attach:function(t,o){if(t.classList.contains("layout-paragraphs-component-form")){const e=t.querySelector('.layout-select input[type="radio"]:checked');e&&e.focus()}const r=e("me-component-form",".layout-paragraphs-component-form")[0];r&&r.closest("mercury-dialog").addEventListener("open",(t=>{const e=t.target.shadowRoot.querySelector("dialog"),o=e.offsetWidth+"px",r=e.offsetHeight+"px";t.target.style.setProperty("--me-dialog-width-default",o),t.target.style.setProperty("--me-dialog-height-default",r)}))}}})(Drupal,once)}(); +!function(){"use strict";((t,e)=>{t.behaviors.mercuryEditorComponentForm={attach:function(t,o){if(t.classList.contains("layout-paragraphs-component-form")){const e=t.querySelector('.layout-select input[type="radio"]:checked');e&&e.focus()}const r=e("me-component-form","mercury-dialog .layout-paragraphs-component-form")[0];r&&r.closest("mercury-dialog").addEventListener("open",(t=>{const e=t.target.shadowRoot.querySelector("dialog"),o=e.offsetWidth+"px",r=e.offsetHeight+"px";t.target.style.setProperty("--me-dialog-width-default",o),t.target.style.setProperty("--me-dialog-height-default",r)}))}}})(Drupal,once)}(); diff --git a/build/js/edit-screen.js b/build/js/edit-screen.js index bb012f8355d7a1d12e1f878cb6749c85fe68a515..3214d6f3a3235de1d0739fdc4070914223c2de3e 100644 --- a/build/js/edit-screen.js +++ b/build/js/edit-screen.js @@ -116,7 +116,6 @@ const sidebarToggle = document.querySelector('#me-sidebar-toggle-btn'); sidebarToggle.addEventListener('click', (e) => { e.currentTarget; - console.warn('sidebarToggle', sidebarState); if (sidebarState === 'open') { // When closing the sidebar, set the width to 10px. document.documentElement.style.setProperty('--me-dialog-dock-width', '10px'); @@ -145,6 +144,7 @@ sidebarToggle.classList.remove('me-button--sidebar-expand'); sidebarToggle.classList.add('me-button--sidebar-collapse'); sidebarToggle.innerHTML = `<span>${Drupal.t('Hide sidebar')}</span>`; + sidebarToggle.setAttribute('title', Drupal.t('Hide sidebar')); localStorage.removeItem('mercury-dialog-dock-collapsed'); } else { @@ -152,6 +152,7 @@ sidebarToggle.classList.remove('me-button--sidebar-collapse'); sidebarToggle.classList.add('me-button--sidebar-expand'); sidebarToggle.innerHTML = `<span>${Drupal.t('Show sidebar')}</span>`; + sidebarToggle.setAttribute('title', Drupal.t('Show sidebar')); localStorage.setItem('mercury-dialog-dock-collapsed', 'true'); } @@ -220,6 +221,11 @@ document.addEventListener('mouseup', iFramePointerEventsToggle); } } + // Set the iframe URL once other js files have loaded. + if (once('me-preview-iframe', '#me-preview', context).length) { + const iframe = document.querySelector('#me-preview'); + iframe.src = iframe.getAttribute('data-src'); + } } }; })(Drupal, drupalSettings, jQuery, once); diff --git a/build/js/edit-screen.min.js b/build/js/edit-screen.min.js index abd6f63de9ebfacb9ad02b81e38ea3e7670c1cca..d3e83410875a42890496ce3e5b12c2c545e88954 100644 --- a/build/js/edit-screen.min.js +++ b/build/js/edit-screen.min.js @@ -1 +1 @@ -!function(){"use strict";((e,t,o,r)=>{let n,i="open";function a(e,t){const o=document.querySelector("#me-preview");e?o.style.width=e:o.style.removeProperty("width"),t?o.style.height=t:o.style.removeProperty("height")}function d(e){const t=document.querySelector('[data-drupal-selector="edit-submit"]:not([disabled])');if(t){const e=t.closest("form").querySelectorAll("input, textarea, select")||[],o=Array.from(e).filter((e=>!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)&&!e.validity.valid));o.length?(o[0].focus(),o[0].reportValidity()):t.dispatchEvent(new Event("mousedown"))}}function l(e){const t=(document.querySelector(".me-edit-screen-redirect-url")??{}).value;return window.location.href=t,!1}function c(e){const t=document.querySelector("#me-preview");t&&(t.style.pointerEvents="mouseup"==e.type?"auto":"none")}o(window).on("dialog:afterclose",((e,t,o)=>{"MERCURY-DIALOG"==(o[0]||{}).tagName&&document.getElementById("me-preview").contentWindow.postMessage({type:"onCloseMercuryDialog"})})),e.behaviors.mercuryEditorEditScreen={attach:function(o,s){const m=r("me-first-error",".js-form-item.error",o)[0];if(m&&(m.focus(),m.scrollIntoView({behavior:"smooth"})),r("me-toolbar","#me-toolbar",o).length&&function(){function o(){const e=document.querySelector(".me-mobile-presets");if(e){const t=e.options[e.selectedIndex??0].value.split("x");a(t[0]+"px",Math.min(t[1],window.innerHeight-document.getElementById("me-toolbar").offsetHeight-20)+"px")}else a("390px",Math.min("844",window.innerHeight-document.getElementById("me-toolbar").offsetHeight-20)+"px")}const r=document.querySelector(".me-mobile-presets");r&&r.addEventListener("change",o),document.querySelector("#me-mobile-toggle-btn").addEventListener("click",(e=>(r&&(r.style.display="block"),o(),window.addEventListener("resize",o),e.preventDefault(),e.stopPropagation(),!1))),document.querySelector("#me-desktop-toggle-btn").addEventListener("click",(e=>(r&&(r.style.display="none"),window.removeEventListener("resize",o),a("100%","100%"),e.preventDefault(),e.stopPropagation(),!1))),document.querySelector('[data-drupal-selector="edit-submit"]:not([disabled])')?document.querySelector("#me-save-btn").addEventListener("click",d):document.querySelector("#me-save-btn").remove(),document.querySelector("#me-done-btn").addEventListener("click",l),t.mercuryEditor&&t.mercuryEditor.width&&localStorage.setItem("mercury-dialog-dock-default-width",t.mercuryEditor.width);let c="true"===localStorage.getItem("mercury-dialog-dock-collapsed");i=c?"closed":"open";const s=document.querySelector("#me-sidebar-toggle-btn");s.addEventListener("click",(e=>(e.currentTarget,console.warn("sidebarToggle",i),"open"===i?(document.documentElement.style.setProperty("--me-dialog-dock-width","10px"),localStorage.setItem("mercury-dialog-dock-collapsed","true")):(n=localStorage.getItem("mercury-dialog-dock-default-width"),n&&document.documentElement.style.setProperty("--me-dialog-dock-width",`${n}px`)),e.preventDefault(),e.stopPropagation(),!1))),document.addEventListener("mercury:dockResize",(t=>{let o=t.detail.width;o>10?(i="open",s.classList.remove("me-button--sidebar-expand"),s.classList.add("me-button--sidebar-collapse"),s.innerHTML=`<span>${e.t("Hide sidebar")}</span>`,localStorage.removeItem("mercury-dialog-dock-collapsed")):(i="closed",s.classList.remove("me-button--sidebar-collapse"),s.classList.add("me-button--sidebar-expand"),s.innerHTML=`<span>${e.t("Show sidebar")}</span>`,localStorage.setItem("mercury-dialog-dock-collapsed","true")),localStorage.setItem("mercury-dialog-dock-width",o)}))}(),r("me-edit-tray","#me-edit-screen",o).length){const r=o.querySelector("#me-edit-screen");if(r){e.mercuryDialog(r).show(),void 0!==e.Ajax&&void 0===e.Ajax.prototype.beforeSerializeMercuryEditor&&(e.Ajax.prototype.beforeSerializeMercuryEditor=e.Ajax.prototype.beforeSerialize,e.Ajax.prototype.beforeSerialize=function(e,o){this.beforeSerializeMercuryEditor.apply(this,arguments);const r=t.ajaxPreviewPageState||{};o.data["ajax_preview_page_state[theme]"]=r.theme,o.data["ajax_preview_page_state[theme_token]"]=r.theme_token,o.data["ajax_preview_page_state[libraries]"]=r.libraries}),document.addEventListener("mousedown",c),document.addEventListener("mouseup",c)}}}}})(Drupal,drupalSettings,jQuery,once)}(); +!function(){"use strict";((e,t,o,r)=>{let i,n="open";function a(e,t){const o=document.querySelector("#me-preview");e?o.style.width=e:o.style.removeProperty("width"),t?o.style.height=t:o.style.removeProperty("height")}function d(e){const t=document.querySelector('[data-drupal-selector="edit-submit"]:not([disabled])');if(t){const e=t.closest("form").querySelectorAll("input, textarea, select")||[],o=Array.from(e).filter((e=>!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)&&!e.validity.valid));o.length?(o[0].focus(),o[0].reportValidity()):t.dispatchEvent(new Event("mousedown"))}}function c(e){const t=(document.querySelector(".me-edit-screen-redirect-url")??{}).value;return window.location.href=t,!1}function l(e){const t=document.querySelector("#me-preview");t&&(t.style.pointerEvents="mouseup"==e.type?"auto":"none")}o(window).on("dialog:afterclose",((e,t,o)=>{"MERCURY-DIALOG"==(o[0]||{}).tagName&&document.getElementById("me-preview").contentWindow.postMessage({type:"onCloseMercuryDialog"})})),e.behaviors.mercuryEditorEditScreen={attach:function(o,s){const m=r("me-first-error",".js-form-item.error",o)[0];if(m&&(m.focus(),m.scrollIntoView({behavior:"smooth"})),r("me-toolbar","#me-toolbar",o).length&&function(){function o(){const e=document.querySelector(".me-mobile-presets");if(e){const t=e.options[e.selectedIndex??0].value.split("x");a(t[0]+"px",Math.min(t[1],window.innerHeight-document.getElementById("me-toolbar").offsetHeight-20)+"px")}else a("390px",Math.min("844",window.innerHeight-document.getElementById("me-toolbar").offsetHeight-20)+"px")}const r=document.querySelector(".me-mobile-presets");r&&r.addEventListener("change",o),document.querySelector("#me-mobile-toggle-btn").addEventListener("click",(e=>(r&&(r.style.display="block"),o(),window.addEventListener("resize",o),e.preventDefault(),e.stopPropagation(),!1))),document.querySelector("#me-desktop-toggle-btn").addEventListener("click",(e=>(r&&(r.style.display="none"),window.removeEventListener("resize",o),a("100%","100%"),e.preventDefault(),e.stopPropagation(),!1))),document.querySelector('[data-drupal-selector="edit-submit"]:not([disabled])')?document.querySelector("#me-save-btn").addEventListener("click",d):document.querySelector("#me-save-btn").remove(),document.querySelector("#me-done-btn").addEventListener("click",c),t.mercuryEditor&&t.mercuryEditor.width&&localStorage.setItem("mercury-dialog-dock-default-width",t.mercuryEditor.width);let l="true"===localStorage.getItem("mercury-dialog-dock-collapsed");n=l?"closed":"open";const s=document.querySelector("#me-sidebar-toggle-btn");s.addEventListener("click",(e=>(e.currentTarget,"open"===n?(document.documentElement.style.setProperty("--me-dialog-dock-width","10px"),localStorage.setItem("mercury-dialog-dock-collapsed","true")):(i=localStorage.getItem("mercury-dialog-dock-default-width"),i&&document.documentElement.style.setProperty("--me-dialog-dock-width",`${i}px`)),e.preventDefault(),e.stopPropagation(),!1))),document.addEventListener("mercury:dockResize",(t=>{let o=t.detail.width;o>10?(n="open",s.classList.remove("me-button--sidebar-expand"),s.classList.add("me-button--sidebar-collapse"),s.innerHTML=`<span>${e.t("Hide sidebar")}</span>`,s.setAttribute("title",e.t("Hide sidebar")),localStorage.removeItem("mercury-dialog-dock-collapsed")):(n="closed",s.classList.remove("me-button--sidebar-collapse"),s.classList.add("me-button--sidebar-expand"),s.innerHTML=`<span>${e.t("Show sidebar")}</span>`,s.setAttribute("title",e.t("Show sidebar")),localStorage.setItem("mercury-dialog-dock-collapsed","true")),localStorage.setItem("mercury-dialog-dock-width",o)}))}(),r("me-edit-tray","#me-edit-screen",o).length){const r=o.querySelector("#me-edit-screen");if(r){e.mercuryDialog(r).show(),void 0!==e.Ajax&&void 0===e.Ajax.prototype.beforeSerializeMercuryEditor&&(e.Ajax.prototype.beforeSerializeMercuryEditor=e.Ajax.prototype.beforeSerialize,e.Ajax.prototype.beforeSerialize=function(e,o){this.beforeSerializeMercuryEditor.apply(this,arguments);const r=t.ajaxPreviewPageState||{};o.data["ajax_preview_page_state[theme]"]=r.theme,o.data["ajax_preview_page_state[theme_token]"]=r.theme_token,o.data["ajax_preview_page_state[libraries]"]=r.libraries}),document.addEventListener("mousedown",l),document.addEventListener("mouseup",l)}}if(r("me-preview-iframe","#me-preview",o).length){const e=document.querySelector("#me-preview");e.src=e.getAttribute("data-src")}}}})(Drupal,drupalSettings,jQuery,once)}(); diff --git a/build/js/horizontal-tabs.js b/build/js/horizontal-tabs.js index 9d3c6205c108f366104e20a54a22ec52dc21b248..3addb9d0d03d6f6d634ee42f083328ecd19ca50d 100644 --- a/build/js/horizontal-tabs.js +++ b/build/js/horizontal-tabs.js @@ -24,6 +24,17 @@ const selected = tabs.querySelector('input[type="radio"]:checked') || {}; toggleTabs(selected.value); }); + if (document.querySelector('.me-tab-group .error')) { + const tabGroup = document.querySelector('.me-tab-group .error').closest('.me-tab-group'); + // Get the class that starts with "me-tab-group--" and get the part after "--". + const selectedValue = tabGroup.className.match(/me-tab-group--([^ ]+)/)[1]; + // Get the tab radio button with the same value as the tab group. + const selected = document.querySelector(`.me-tabs input[type="radio"][value="${selectedValue}"]`); + if (selected) { + selected.checked = true; + toggleTabs(selectedValue); + } + } } }; diff --git a/build/js/horizontal-tabs.min.js b/build/js/horizontal-tabs.min.js index 643f04951a95883199bc457d73660e94e36678e2..3298324fbee15fa3a60c0d885f2b50c3738203c4 100644 --- a/build/js/horizontal-tabs.min.js +++ b/build/js/horizontal-tabs.min.js @@ -1 +1 @@ -!function(){"use strict";((e,t,a)=>{function i(e){(document.querySelectorAll(".me-tab-group")||[]).forEach((e=>{e.setAttribute("aria-hidden",!0),e.classList.add("hidden-tab")})),(document.querySelectorAll(`.me-tab-group--${e}`)||[]).forEach((e=>{e.removeAttribute("aria-hidden"),e.classList.remove("hidden-tab")}))}t.behaviors.mercuryEditorTabs={attach:function(e,t){a("me-tabs",'.me-tabs input[type="radio"]').forEach((e=>{e.addEventListener("change",(t=>{i(e.value)}))})),document.querySelectorAll(".me-tabs").forEach((e=>{i((e.querySelector('input[type="radio"]:checked')||{}).value)}))}},e(window).on("dialog:aftercreate",((e,t,a)=>{const i=a.attr("id");if(i&&0===i.indexOf("lpb-dialog-")){const e=a.find(".horizontal-tab-radios"),t=a.closest(".ui-dialog").find(".ui-dialog-titlebar");e.length&&t.length&&t.addClass("has-tabs").append(e)}}))})(jQuery,Drupal,once)}(); +!function(){"use strict";((e,t,a)=>{function r(e){(document.querySelectorAll(".me-tab-group")||[]).forEach((e=>{e.setAttribute("aria-hidden",!0),e.classList.add("hidden-tab")})),(document.querySelectorAll(`.me-tab-group--${e}`)||[]).forEach((e=>{e.removeAttribute("aria-hidden"),e.classList.remove("hidden-tab")}))}t.behaviors.mercuryEditorTabs={attach:function(e,t){if(a("me-tabs",'.me-tabs input[type="radio"]').forEach((e=>{e.addEventListener("change",(t=>{r(e.value)}))})),document.querySelectorAll(".me-tabs").forEach((e=>{r((e.querySelector('input[type="radio"]:checked')||{}).value)})),document.querySelector(".me-tab-group .error")){const e=document.querySelector(".me-tab-group .error").closest(".me-tab-group").className.match(/me-tab-group--([^ ]+)/)[1],t=document.querySelector(`.me-tabs input[type="radio"][value="${e}"]`);t&&(t.checked=!0,r(e))}}},e(window).on("dialog:aftercreate",((e,t,a)=>{const r=a.attr("id");if(r&&0===r.indexOf("lpb-dialog-")){const e=a.find(".horizontal-tab-radios"),t=a.closest(".ui-dialog").find(".ui-dialog-titlebar");e.length&&t.length&&t.addClass("has-tabs").append(e)}}))})(jQuery,Drupal,once)}(); diff --git a/build/js/me-dragula.js b/build/js/me-dragula.js deleted file mode 100644 index 4e3ace4ac48e5bf8b0917673aba500878ac66185..0000000000000000000000000000000000000000 --- a/build/js/me-dragula.js +++ /dev/null @@ -1,205 +0,0 @@ -(function () { - 'use strict'; - - (($, Drupal, once) => { - - /** - * Ensures that all layout paragraphs controls are fully within viewport. - * - * @param {jQuery} $builder - * The Layout Paragraphs container jQuery object. - */ - function repositionControls($builder) { - $builder.find('.lpb-controls').each((i, controls) => { - controls.setAttribute('style', controls.getAttribute('data-style')); - const bounding = controls.getBoundingClientRect(); - // Left viewport edge. - const l = 0; - // Right viewport edge. - const r = (window.innerWidth || document.documentElement.clientWidth); - // Overlapping left. - if (bounding.left < l) { - controls.setAttribute('data-style', controls.getAttribute('style')); - $(controls).offset({left: 0}); - } - // Overlapping right. - if (bounding.right > r) { - $(controls).css({right: (bounding.right - r) + 'px'}); - } - }); - } - - /** - * Simplifies drag and drop visual cues to prevent jumpiness. - * - * The default behavior of the dragula library can create excessive - * jumpiness in some cases. This function simplifies the UI and drag and drop - * experience in several key ways, including: - * - * - Detaches all layout paragraphs UI elements when dragging starts. - * - Provides a simple "hint" element to show where an item will be dropped. - * - Leaves a "ghost" copy of the grabbed element in place at the source. - * - Reattaches all UI elements when dragging ends. - * - * @see https://github.com/bevacqua/dragula#drakeon-events. - * - * @param {Object} drake - * The dragula object. - */ - function simplifyDragHints($builder, settings) { - - const drake = $builder.data('drake'); - let ghost, grabbed; - const hint = $('<div class="lp-hint hidden"></div>')[0]; - - // Hide UI elements when dragging starts. - drake.on('drag', (el) => { - el.parentNode.insertBefore(hint, el); - $builder.find('.js-lpb-ui').addClass('hidden'); - }); - // Provide a simple hint element to indicate where an item will be dropped. - drake.on('shadow', (item, container) => { - if (item.classList.contains('lp-hint')) { - return; - } - // Remove comments and text nodes from container. - [...container.childNodes].filter((e) => e.classList === undefined).forEach((e) => e.remove()); - container.replaceChild(hint, item); - - // Ensure the hint does not get at the end of the region after the add button. - if (hint.nextSibling === null && hint.previousSibling !== null && hint.previousSibling.classList.contains('lpb-btn--add')) { - container.insertBefore(hint, hint.previousSibling); - } - - const nextIsGhost = hint.nextSibling !== null ? hint.nextSibling.classList.contains('lp-ghost') : false; - const prevIsGhost = hint.previousSibling !== null ? hint.previousSibling.classList.contains('lp-ghost') : false; - const ghostAdjacent = nextIsGhost || prevIsGhost; - if (ghostAdjacent) { - hint.classList.add('hidden'); - ghost.classList.remove('gu-transit'); - } - else { - hint.classList.remove('hidden'); - ghost.classList.add('gu-transit'); - } - }); - // Leave a copy of the grabbed item in place at the original source. - drake.on('cloned', (mirror, item) => { - ghost = item.cloneNode(true); - ghost.classList.add('lp-ghost'); - item.parentNode.insertBefore(ghost, item); - grabbed = item; - item.remove(); - }); - // Show UI elements and remove ghost and hint elements when dragging stops. - drake.on('dragend', (el) => { - hint.replaceWith(grabbed); - ghost.remove(); - $builder.find('.js-lpb-ui').removeClass('hidden'); - repositionControls($builder); - $builder.trigger('lpb-component:drop', [el.getAttribute('data-uuid')]); - }); - } - - /** - * Waits for a condition to be met, then calls the provided callback. - * - * @param {Function} cond - * The condition to wait for. - * @param {Function} cb - * The callback to call when cond evaluates true. - */ - function waitFor(cond, cb) { - const i = setInterval(() => { - if (cond() === true) { - clearInterval(i); - cb(); - } - }, 100); - } - - function removeMovedFormActions() { - const outdatedFormActions = document.getElementsByClassName('lpb-form__actions repositioned'); - if (outdatedFormActions.length) { - outdatedFormActions[0].remove(); - } - } - - function moveFormActions(context) { - const formActions = context.querySelector('.lpb-form__actions'); - if (formActions) { - removeMovedFormActions(); - formActions.classList.add('repositioned'); - document.body.appendChild(formActions); - } - } - - function showControls(e) { - const el = e.target.closest('.lpb-controls, .js-lpb-component'); - el.classList.add('focused'); - el.classList.remove('transitioning'); - el.classList.remove('blurred'); - } - - function hideControls(e) { - const el = e.target.closest('.lpb-controls, .js-lpb-component'); - el.classList.add('transitioning'); - setTimeout(() => { - if (el.classList.contains('transitioning')) { - el.classList.remove('focused'); - el.classList.remove('transitioning'); - el.classList.add('blurred'); - } - }, 250); - } - - Drupal.behaviors.mercuryEditorDragula = { - attach: function attach(context, settings) { - // Append form-actions to body for better styling control. - if (once('me-builder-events', 'html', context).length) { - $(window).on('lpb-builder:open.lpb lpb-builder:save.lpb', (e) => { - moveFormActions(context); - }); - $(window).on('lpb-builder:close.lpb', (e) => { - removeMovedFormActions(); - }); - } - var events = ['lpb-component:insert.lpb', 'lpb-component:update.lpb', 'lpb-component:move.lpb', 'lpb-component:drop.lpb'].join(' '); - $(once('me-builder-form', '[data-lpb-id]', context)).on(events, function (e) { - const cancelButton = document.querySelector('lpb-form__actions repositioned .lpb-btn--cancel'); - if (cancelButton) { - cancelButton.value = Drupal.t('Cancel'); - } - }); - once('me-dragula', '.lp-builder.has-components').forEach((builder) => { - const $builder = $(builder); - waitFor( - () => $builder.data('drake') !== undefined, - () => { - repositionControls($builder); - moveFormActions($builder[0]); - $(window).resize(() => repositionControls($builder)); - // Simplifies drag and drop functionality. - try { - simplifyDragHints($builder, settings); - } - catch (e) { - console.warn(e); - } - }); - }); - once('reveal-on-hover', '.js-lpb-component').forEach((component) => { - component.addEventListener('mouseenter', showControls); - component.addEventListener('mouseleave', hideControls); - }); - once('reveal-on-hover', '.lpb-controls').forEach((el) => { - el.addEventListener('mouseenter', showControls); - el.addEventListener("focusin", showControls); - el.addEventListener('mouseleave', hideControls); - el.addEventListener("focusout", hideControls); - }); - } - }; - })(jQuery, Drupal, once); - -})(); diff --git a/build/js/me-dragula.min.js b/build/js/me-dragula.min.js deleted file mode 100644 index ffe05c7b8da9c2f24f5d2a0d716901a499c8a56d..0000000000000000000000000000000000000000 --- a/build/js/me-dragula.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";((e,t,n)=>{function s(t){t.find(".lpb-controls").each(((t,n)=>{n.setAttribute("style",n.getAttribute("data-style"));const s=n.getBoundingClientRect(),o=window.innerWidth||document.documentElement.clientWidth;s.left<0&&(n.setAttribute("data-style",n.getAttribute("style")),e(n).offset({left:0})),s.right>o&&e(n).css({right:s.right-o+"px"})}))}function o(){const e=document.getElementsByClassName("lpb-form__actions repositioned");e.length&&e[0].remove()}function i(e){const t=e.querySelector(".lpb-form__actions");t&&(o(),t.classList.add("repositioned"),document.body.appendChild(t))}function l(e){const t=e.target.closest(".lpb-controls, .js-lpb-component");t.classList.add("focused"),t.classList.remove("transitioning"),t.classList.remove("blurred")}function r(e){const t=e.target.closest(".lpb-controls, .js-lpb-component");t.classList.add("transitioning"),setTimeout((()=>{t.classList.contains("transitioning")&&(t.classList.remove("focused"),t.classList.remove("transitioning"),t.classList.add("blurred"))}),250)}t.behaviors.mercuryEditorDragula={attach:function(a,d){n("me-builder-events","html",a).length&&(e(window).on("lpb-builder:open.lpb lpb-builder:save.lpb",(e=>{i(a)})),e(window).on("lpb-builder:close.lpb",(e=>{o()})));var c=["lpb-component:insert.lpb","lpb-component:update.lpb","lpb-component:move.lpb","lpb-component:drop.lpb"].join(" ");e(n("me-builder-form","[data-lpb-id]",a)).on(c,(function(e){const n=document.querySelector("lpb-form__actions repositioned .lpb-btn--cancel");n&&(n.value=t.t("Cancel"))})),n("me-dragula",".lp-builder.has-components").forEach((t=>{const n=e(t);!function(e,t){const n=setInterval((()=>{!0===e()&&(clearInterval(n),t())}),100)}((()=>void 0!==n.data("drake")),(()=>{s(n),i(n[0]),e(window).resize((()=>s(n)));try{!function(t,n){const o=t.data("drake");let i,l;const r=e('<div class="lp-hint hidden"></div>')[0];o.on("drag",(e=>{e.parentNode.insertBefore(r,e),t.find(".js-lpb-ui").addClass("hidden")})),o.on("shadow",((e,t)=>{if(e.classList.contains("lp-hint"))return;[...t.childNodes].filter((e=>void 0===e.classList)).forEach((e=>e.remove())),t.replaceChild(r,e),null===r.nextSibling&&null!==r.previousSibling&&r.previousSibling.classList.contains("lpb-btn--add")&&t.insertBefore(r,r.previousSibling);const n=null!==r.nextSibling&&r.nextSibling.classList.contains("lp-ghost"),s=null!==r.previousSibling&&r.previousSibling.classList.contains("lp-ghost");n||s?(r.classList.add("hidden"),i.classList.remove("gu-transit")):(r.classList.remove("hidden"),i.classList.add("gu-transit"))})),o.on("cloned",((e,t)=>{i=t.cloneNode(!0),i.classList.add("lp-ghost"),t.parentNode.insertBefore(i,t),l=t,t.remove()})),o.on("dragend",(e=>{r.replaceWith(l),i.remove(),t.find(".js-lpb-ui").removeClass("hidden"),s(t),t.trigger("lpb-component:drop",[e.getAttribute("data-uuid")])}))}(n)}catch(e){console.warn(e)}}))})),n("reveal-on-hover",".js-lpb-component").forEach((e=>{e.addEventListener("mouseenter",l),e.addEventListener("mouseleave",r)})),n("reveal-on-hover",".lpb-controls").forEach((e=>{e.addEventListener("mouseenter",l),e.addEventListener("focusin",l),e.addEventListener("mouseleave",r),e.addEventListener("focusout",r)}))}}})(jQuery,Drupal,once)}(); diff --git a/build/js/post-messages-listener.js b/build/js/post-messages-listener.js index a90b3eaf364b60df0528db4871b6e468c0cce736..1dd65489b1dfcfa5045c3b3423634d1f24cd2f7d 100644 --- a/build/js/post-messages-listener.js +++ b/build/js/post-messages-listener.js @@ -11,14 +11,6 @@ * @param {Object} settings The ajax settings. */ drupalAjax: function (settings) { - console.log('drupalAjax', settings); - Drupal.ajax(settings).execute(); - }, - /** - * Ajax click handler for Layout Paragraphs UI elements in iframe. - * @param {Object} settings The ajax settings. - */ - lpbUiClick: function (settings) { Drupal.ajax(settings).execute(); }, /** diff --git a/build/js/post-messages-listener.min.js b/build/js/post-messages-listener.min.js index f889bbe456bc1f05d6b2464030afd9c9393798f3..1f1cf5662471d8da872ff111d76e962526cfca39 100644 --- a/build/js/post-messages-listener.min.js +++ b/build/js/post-messages-listener.min.js @@ -1 +1 @@ -!function(){"use strict";((e,t,n)=>{const a={drupalAjax:function(t){console.log("drupalAjax",t),e.ajax(t).execute()},lpbUiClick:function(t){e.ajax(t).execute()},syncChanges:function(e){const{ref:t,value:n}=e;document.querySelector(`[data-sync-changes="${t}"]`).innerHTML=n},ajaxCommands:function(t){const{commands:n,status:a}=t,o=e.ajax({url:""}),s=new e.AjaxCommands;Object.keys(n||{}).reduce((function(e,t){return e.then((function(){var e=n[t].command;if(e&&s[e])return s[e](o,n[t],a)})).catch(console.error)}),Promise.resolve())},ajaxPreviewPageState:function(e){t.ajaxPreviewPageState=e},onCloseMercuryDialog:function(){document.querySelectorAll(".is-me-focused").forEach((e=>{e.focus(),e.classList.remove("is-me-focused")}))}};e.behaviors.mercuryEditorPostMessagesListener={attach:function(e,t){n("me-msg-listener","html").length&&window.addEventListener("message",(e=>{a[e.data.type]&&a[e.data.type](e.data.settings)}))}}})(Drupal,drupalSettings,once)}(); +!function(){"use strict";((e,t,n)=>{const a={drupalAjax:function(t){e.ajax(t).execute()},syncChanges:function(e){const{ref:t,value:n}=e;document.querySelector(`[data-sync-changes="${t}"]`).innerHTML=n},ajaxCommands:function(t){const{commands:n,status:a}=t,s=e.ajax({url:""}),o=new e.AjaxCommands;Object.keys(n||{}).reduce((function(e,t){return e.then((function(){var e=n[t].command;if(e&&o[e])return o[e](s,n[t],a)})).catch(console.error)}),Promise.resolve())},ajaxPreviewPageState:function(e){t.ajaxPreviewPageState=e},onCloseMercuryDialog:function(){document.querySelectorAll(".is-me-focused").forEach((e=>{e.focus(),e.classList.remove("is-me-focused")}))}};e.behaviors.mercuryEditorPostMessagesListener={attach:function(e,t){n("me-msg-listener","html").length&&window.addEventListener("message",(e=>{a[e.data.type]&&a[e.data.type](e.data.settings)}))}}})(Drupal,drupalSettings,once)}(); diff --git a/build/js/preview-screen.js b/build/js/preview-screen.js index 917b799c9bc49d9be642f24bdd5e17b1c6da2e53..0029166f5befd28ab2303025e1dfa4bc8248c3ea 100644 --- a/build/js/preview-screen.js +++ b/build/js/preview-screen.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - ((Drupal, drupalSettings, once) => { + ((Drupal, drupalSettings, $, once) => { /** * Prevent a click. @@ -32,7 +32,7 @@ e.currentTarget.classList.add('is-me-focused'); // Then, send the click event. const message = { - type: 'lpbUiClick', + type: 'drupalAjax', settings: { dialogType: e.currentTarget.getAttribute('data-dialog-type'), dialog: JSON.parse(e.currentTarget.getAttribute('data-dialog-options')), @@ -46,18 +46,202 @@ return false; } + function scaleMirror(e) { + const mirror = document.querySelector('.gu-mirror'); + if (!mirror) { + return; + } + const scaleAxis = mirror.offsetWidth > mirror.offsetHeight ? 'offsetHeight' : 'offsetWidth'; + const scale = Math.min(300 / mirror[scaleAxis], 1); + const boundingRect = mirror.getBoundingClientRect(); + const diffX = e.clientX - boundingRect.x; + const diffY = e.clientY - boundingRect.y; + mirror.style.setProperty('transform-origin', `${diffX}px ${diffY}px`); + mirror.style.setProperty('transform', `scale(${scale})`); + window.removeEventListener('mousemove', scaleMirror); + } + + /** + * Simplifies drag and drop visual cues to prevent jumpiness. + * + * The default behavior of the dragula library can create excessive + * jumpiness in some cases. This function simplifies the UI and drag and drop + * experience in several key ways, including: + * + * - Detaches all layout paragraphs UI elements when dragging starts. + * - Provides a simple "hint" element to show where an item will be dropped. + * - Leaves a "ghost" copy of the grabbed element in place at the source. + * - Reattaches all UI elements when dragging ends. + * + * @see https://github.com/bevacqua/dragula#drakeon-events. + * + * @param {Object} drake + * The dragula object. + */ + function simplifyDragHints($builder, drake) { + + let ghost, grabbed; + const hint = $('<div class="lp-hint hidden"></div>')[0]; + // Scales the mirror element to make it easier to drag. + drake.on('cloned', (clone, original, type) => { + window.addEventListener('mousemove', scaleMirror); + }); + // Hide UI elements when dragging starts. + drake.on('drag', (el) => { + if (el.parentNode) { + el.parentNode.insertBefore(hint, el); + } + $builder.find('.js-lpb-ui').addClass('hidden'); + }); + // Provide a simple hint element to indicate where an item will be dropped. + drake.on('shadow', (shadow, container, src) => { + + if (shadow.classList.contains('lp-hint')) { + return; + } + + hint.style = { + width: '', + height: '', + marginLeft: '', + marginTop: '', + }; + + const sibling = shadow.nextElementSibling || shadow.previousElementSibling; + const orientation = sibling && shadow.getBoundingClientRect().top === sibling.getBoundingClientRect().top + ? 'vertical' + : 'horizontal'; + + if (orientation == 'horizontal') { + const offset = parseInt(window.getComputedStyle(shadow.parentNode).getPropertyValue('padding-left')); + hint.style.marginLeft = '-' + offset + 'px'; + } + + if (orientation == 'vertical') { + const offset = parseInt(window.getComputedStyle(shadow.parentNode).getPropertyValue('padding-top')); + hint.style.marginTop = '-' + offset + 'px'; + } + + if (orientation === 'vertical') { + hint.style.height = shadow.parentNode.clientHeight + 'px'; + } + if (orientation === 'horizontal') { + hint.style.width = shadow.parentNode.clientWidth + 'px'; + } + + hint.setAttribute('data-orientation', orientation); + + // Remove comments and text nodes from container. + [...container.childNodes].filter((e) => e.classList === undefined).forEach((e) => e.remove()); + container.replaceChild(hint, shadow); + + // Ensure the hint does not get at the end of the region after the add button. + if (hint.nextSibling === null && hint.previousSibling !== null && hint.previousSibling.classList.contains('lpb-btn--add')) { + container.insertBefore(hint, hint.previousSibling); + } + + const nextIsGhost = hint.nextSibling !== null ? hint.nextSibling.classList.contains('lp-ghost') : false; + const prevIsGhost = hint.previousSibling !== null ? hint.previousSibling.classList.contains('lp-ghost') : false; + const ghostAdjacent = nextIsGhost || prevIsGhost; + if (ghostAdjacent) { + hint.classList.add('hidden'); + ghost.classList.remove('gu-transit'); + } + else { + hint.classList.remove('hidden'); + ghost.classList.add('gu-transit'); + } + }); + // Leave a copy of the grabbed item in place at the original source. + drake.on('cloned', (mirror, item) => { + ghost = item.cloneNode(true); + ghost.classList.add('lp-ghost'); + if (item.parentNode) { + item.parentNode.insertBefore(ghost, item); + } + grabbed = item; + item.remove(); + }); + // Show UI elements and remove ghost and hint elements when dragging stops. + drake.on('dragend', (el) => { + hint.replaceWith(grabbed); + ghost.remove(); + $builder.find('.js-lpb-ui').removeClass('hidden'); + }); + } + + /** + * Calls simplifyDragHints() when the builder is initialized. + */ + $(document).on('lpb-builder:init', (e) => { + const builder = e.target; + const drake = $(builder).data('drake'); + if (drake) { + simplifyDragHints($(builder), drake); + } + }); + + function padElements(layout) { + [...layout.querySelectorAll('[data-region]'), layout].forEach((el) => { + const computed = getComputedStyle(el); + if (!el.hasAttribute('data-me-padding')) { + el.setAttribute('data-me-padding', `${computed.paddingTop} ${computed.paddingRight} ${computed.paddingBottom} ${computed.paddingLeft}`); + } + el.style.paddingTop = Math.max(10, parseInt(computed.paddingTop)) + 'px'; + el.style.paddingRight = Math.max(10, parseInt(computed.paddingRight)) + 'px'; + el.style.paddingBottom = Math.max(10, parseInt(computed.paddingBottom)) + 'px'; + el.style.paddingLeft = Math.max(10, parseInt(computed.paddingLeft)) + 'px'; + }); + } + + function unpadElements(layout) { + [...layout.querySelectorAll('[data-region]'), layout].forEach((el) => { + if (el.hasAttribute('data-me-padding')) { + getComputedStyle(el); + el.style.padding = el.getAttribute('data-me-padding'); + } + }); + } + + function showControls(e) { + const el = e.target.closest('.lpb-controls, .js-lpb-component'); + el.classList.add('focused'); + el.classList.remove('transitioning'); + el.classList.remove('blurred'); + } + + function hideControls(e) { + const el = e.target.closest('.lpb-controls, .js-lpb-component'); + el.classList.add('transitioning'); + setTimeout(() => { + if (el.classList.contains('transitioning')) { + el.classList.remove('focused'); + el.classList.remove('transitioning'); + el.classList.add('blurred'); + } + }, 250); + } + /** * Attaches the behavior to the edit screen. */ Drupal.behaviors.mercuryEditorPreviewScreen = { attach: function(context, _settings) { + const duplicateContainers = [...document.querySelectorAll('[data-me-edit-screen-key]')] + .map((container) => container.getAttribute('data-me-edit-screen-key')) + // check for duplicates in array + .filter((value, index, self) => self.indexOf(value) !== index); + if (duplicateContainers.length > 0) { + console.error('Multiple HTML elements found using the same data attribute, "data-me-edit-screen-key", which should be unique. Make sure attributes are not passed to child elements in twig templates.', duplicateContainers); + } // Send the initial ajaxPageState to the parent window. window.parent.postMessage({ type: 'ajaxPreviewPageState', settings: drupalSettings.ajaxPageState }); // Attaches click handlers to links that use window.postMessage(). - once('me-msg-broadcaster', '.use-postmessage').forEach((el) => { + once('me-msg-broadcaster', '.js-lpb-ui.use-ajax, .js-lpb-ui .use-ajax').forEach((el) => { + $(el).off(); el.addEventListener('mousedown', preventDefault); el.addEventListener('mouseup', preventDefault); el.addEventListener('click', lpbClickHander); @@ -73,18 +257,52 @@ return false; }); } + else { + // The dragula library prevents links from automatically focusing + // on mousedown, which can cause issues with keyboard navigation. + link.addEventListener('mousedown', (e) => e.target.focus()); + } }); once('me-prevent-focus', 'a, button, input, textarea, select, details', context).forEach((focussable) => { if ( focussable.closest('.lpb-controls') === null && + focussable.closest('.mercury-editor-ui') === null && !focussable.classList.contains('use-postmessage') ) { focussable.setAttribute('tabindex', '-1'); } }); + once('me-layout-hover', '.lpb-layout').forEach((layout) => { + layout.addEventListener('mouseenter', (e) => { + e.target.setAttribute('data-mouseover', 'true'); + setTimeout(() => { + if (e.target.getAttribute('data-mouseover')) { + padElements(e.target); + } + }, 100); + }); + layout.addEventListener('mouseleave', (e) => { + e.target.removeAttribute('data-mouseover'); + setTimeout(() => { + if (!e.target.getAttribute('data-mouseover')) { + unpadElements(e.target); + } + }, 100); + }); + }); } + once('reveal-on-hover', '.js-lpb-component').forEach((component) => { + component.addEventListener('mouseenter', showControls); + component.addEventListener('mouseleave', hideControls); + }); + once('reveal-on-hover', '.lpb-controls').forEach((el) => { + el.addEventListener('mouseenter', showControls); + el.addEventListener("focusin", showControls); + el.addEventListener('mouseleave', hideControls); + el.addEventListener("focusout", hideControls); + }); } }; - })(Drupal, drupalSettings, once); + })(Drupal, drupalSettings, jQuery, once); })(); diff --git a/build/js/preview-screen.min.js b/build/js/preview-screen.min.js index 29199f19cba2917828d67185888f2e2e39f4ba44..c7391af321c02adeff15dfb5273a0d43f2a4da55 100644 --- a/build/js/preview-screen.min.js +++ b/build/js/preview-screen.min.js @@ -1 +1 @@ -!function(){"use strict";((e,t,a)=>{function r(e){return e.stopPropagation(),e.preventDefault(),!1}function s(e){window.parent.postMessage({type:"ajaxPreviewPageState",settings:t.ajaxPageState}),document.querySelectorAll(".is-me-focused").forEach((e=>{e.classList.remove("is-me-focused")})),e.currentTarget.classList.add("is-me-focused");const a={type:"lpbUiClick",settings:{dialogType:e.currentTarget.getAttribute("data-dialog-type"),dialog:JSON.parse(e.currentTarget.getAttribute("data-dialog-options")),dialogRenderer:JSON.parse(e.currentTarget.getAttribute("data-dialog-renderer")),url:e.currentTarget.getAttribute("href")}};return window.parent.postMessage(a),e.stopPropagation(),e.preventDefault(),!1}e.behaviors.mercuryEditorPreviewScreen={attach:function(e,n){window.parent.postMessage({type:"ajaxPreviewPageState",settings:t.ajaxPageState}),a("me-msg-broadcaster",".use-postmessage").forEach((e=>{e.addEventListener("mousedown",r),e.addEventListener("mouseup",r),e.addEventListener("click",s)})),window.parent!==window&&(a("me-stop-iframed-links","a",e).forEach((e=>{null===e.closest(".lpb-controls")&&(e.setAttribute("target","_parent"),e.addEventListener("click",(e=>(e.stopPropagation(),e.preventDefault(),!1))))})),a("me-prevent-focus","a, button, input, textarea, select, details",e).forEach((e=>{null!==e.closest(".lpb-controls")||e.classList.contains("use-postmessage")||e.setAttribute("tabindex","-1")})))}}})(Drupal,drupalSettings,once)}(); +!function(){"use strict";((e,t,n,a)=>{function s(e){return e.stopPropagation(),e.preventDefault(),!1}function i(e){window.parent.postMessage({type:"ajaxPreviewPageState",settings:t.ajaxPageState}),document.querySelectorAll(".is-me-focused").forEach((e=>{e.classList.remove("is-me-focused")})),e.currentTarget.classList.add("is-me-focused");const n={type:"drupalAjax",settings:{dialogType:e.currentTarget.getAttribute("data-dialog-type"),dialog:JSON.parse(e.currentTarget.getAttribute("data-dialog-options")),dialogRenderer:JSON.parse(e.currentTarget.getAttribute("data-dialog-renderer")),url:e.currentTarget.getAttribute("href")}};return window.parent.postMessage(n),e.stopPropagation(),e.preventDefault(),!1}function o(e){const t=document.querySelector(".gu-mirror");if(!t)return;const n=t.offsetWidth>t.offsetHeight?"offsetHeight":"offsetWidth",a=Math.min(300/t[n],1),s=t.getBoundingClientRect(),i=e.clientX-s.x,r=e.clientY-s.y;t.style.setProperty("transform-origin",`${i}px ${r}px`),t.style.setProperty("transform",`scale(${a})`),window.removeEventListener("mousemove",o)}function r(e){const t=e.target.closest(".lpb-controls, .js-lpb-component");t.classList.add("focused"),t.classList.remove("transitioning"),t.classList.remove("blurred")}function d(e){const t=e.target.closest(".lpb-controls, .js-lpb-component");t.classList.add("transitioning"),setTimeout((()=>{t.classList.contains("transitioning")&&(t.classList.remove("focused"),t.classList.remove("transitioning"),t.classList.add("blurred"))}),250)}n(document).on("lpb-builder:init",(e=>{const t=e.target,a=n(t).data("drake");a&&function(e,t){let a,s;const i=n('<div class="lp-hint hidden"></div>')[0];t.on("cloned",((e,t,n)=>{window.addEventListener("mousemove",o)})),t.on("drag",(t=>{t.parentNode&&t.parentNode.insertBefore(i,t),e.find(".js-lpb-ui").addClass("hidden")})),t.on("shadow",((e,t,n)=>{if(e.classList.contains("lp-hint"))return;i.style={width:"",height:"",marginLeft:"",marginTop:""};const s=e.nextElementSibling||e.previousElementSibling,o=s&&e.getBoundingClientRect().top===s.getBoundingClientRect().top?"vertical":"horizontal";if("horizontal"==o){const t=parseInt(window.getComputedStyle(e.parentNode).getPropertyValue("padding-left"));i.style.marginLeft="-"+t+"px"}if("vertical"==o){const t=parseInt(window.getComputedStyle(e.parentNode).getPropertyValue("padding-top"));i.style.marginTop="-"+t+"px"}"vertical"===o&&(i.style.height=e.parentNode.clientHeight+"px"),"horizontal"===o&&(i.style.width=e.parentNode.clientWidth+"px"),i.setAttribute("data-orientation",o),[...t.childNodes].filter((e=>void 0===e.classList)).forEach((e=>e.remove())),t.replaceChild(i,e),null===i.nextSibling&&null!==i.previousSibling&&i.previousSibling.classList.contains("lpb-btn--add")&&t.insertBefore(i,i.previousSibling);const r=null!==i.nextSibling&&i.nextSibling.classList.contains("lp-ghost"),d=null!==i.previousSibling&&i.previousSibling.classList.contains("lp-ghost");r||d?(i.classList.add("hidden"),a.classList.remove("gu-transit")):(i.classList.remove("hidden"),a.classList.add("gu-transit"))})),t.on("cloned",((e,t)=>{a=t.cloneNode(!0),a.classList.add("lp-ghost"),t.parentNode&&t.parentNode.insertBefore(a,t),s=t,t.remove()})),t.on("dragend",(t=>{i.replaceWith(s),a.remove(),e.find(".js-lpb-ui").removeClass("hidden")}))}(n(t),a)})),e.behaviors.mercuryEditorPreviewScreen={attach:function(e,o){const l=[...document.querySelectorAll("[data-me-edit-screen-key]")].map((e=>e.getAttribute("data-me-edit-screen-key"))).filter(((e,t,n)=>n.indexOf(e)!==t));l.length>0&&console.error('Multiple HTML elements found using the same data attribute, "data-me-edit-screen-key", which should be unique. Make sure attributes are not passed to child elements in twig templates.',l),window.parent.postMessage({type:"ajaxPreviewPageState",settings:t.ajaxPageState}),a("me-msg-broadcaster",".js-lpb-ui.use-ajax, .js-lpb-ui .use-ajax").forEach((e=>{n(e).off(),e.addEventListener("mousedown",s),e.addEventListener("mouseup",s),e.addEventListener("click",i)})),window.parent!==window&&(a("me-stop-iframed-links","a",e).forEach((e=>{null===e.closest(".lpb-controls")?(e.setAttribute("target","_parent"),e.addEventListener("click",(e=>(e.stopPropagation(),e.preventDefault(),!1)))):e.addEventListener("mousedown",(e=>e.target.focus()))})),a("me-prevent-focus","a, button, input, textarea, select, details",e).forEach((e=>{null!==e.closest(".lpb-controls")||null!==e.closest(".mercury-editor-ui")||e.classList.contains("use-postmessage")||e.setAttribute("tabindex","-1")})),a("me-layout-hover",".lpb-layout").forEach((e=>{e.addEventListener("mouseenter",(e=>{e.target.setAttribute("data-mouseover","true"),setTimeout((()=>{e.target.getAttribute("data-mouseover")&&function(e){[...e.querySelectorAll("[data-region]"),e].forEach((e=>{const t=getComputedStyle(e);e.hasAttribute("data-me-padding")||e.setAttribute("data-me-padding",`${t.paddingTop} ${t.paddingRight} ${t.paddingBottom} ${t.paddingLeft}`),e.style.paddingTop=Math.max(10,parseInt(t.paddingTop))+"px",e.style.paddingRight=Math.max(10,parseInt(t.paddingRight))+"px",e.style.paddingBottom=Math.max(10,parseInt(t.paddingBottom))+"px",e.style.paddingLeft=Math.max(10,parseInt(t.paddingLeft))+"px"}))}(e.target)}),100)})),e.addEventListener("mouseleave",(e=>{e.target.removeAttribute("data-mouseover"),setTimeout((()=>{e.target.getAttribute("data-mouseover")||function(e){[...e.querySelectorAll("[data-region]"),e].forEach((e=>{e.hasAttribute("data-me-padding")&&(getComputedStyle(e),e.style.padding=e.getAttribute("data-me-padding"))}))}(e.target)}),100)}))}))),a("reveal-on-hover",".js-lpb-component").forEach((e=>{e.addEventListener("mouseenter",r),e.addEventListener("mouseleave",d)})),a("reveal-on-hover",".lpb-controls").forEach((e=>{e.addEventListener("mouseenter",r),e.addEventListener("focusin",r),e.addEventListener("mouseleave",d),e.addEventListener("focusout",d)}))}}})(Drupal,drupalSettings,jQuery,once)}(); diff --git a/composer.json b/composer.json index 4a63a25cff4e1e8b744db29c32f4487cf4dfd175..8c985e5411f65039a67854828ac4c63d45475865 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ } ], "require": { - "drupal/layout_paragraphs": "^2", + "drupal/layout_paragraphs": "^2.1", "drupal/style_options": "^1", "php": ">=8.1" } diff --git a/images/menu-icons/icon-tabs.png b/images/menu-icons/icon-tabs.png new file mode 100644 index 0000000000000000000000000000000000000000..3ff407ab501ad6b629a04c94409343a3c00bb782 Binary files /dev/null and b/images/menu-icons/icon-tabs.png differ diff --git a/mercury_editor.install b/mercury_editor.install index 443be513a60db5c522a91cea21d66834c776f84e..ae19c603ef509c3278d9859a83fa843813109592 100644 --- a/mercury_editor.install +++ b/mercury_editor.install @@ -15,3 +15,13 @@ function mercury_editor_update_9001() { } $mercury_settings->clear('content_types')->save(); } + +/** + * Set dialog_tray_width in mercury_editor.settings config if it does not exist. + */ +function mercury_editor_update_9002() { + $mercury_settings = Drupal::configFactory()->getEditable('mercury_editor.settings'); + if (!$mercury_settings->get('dialog_tray_width')) { + $mercury_settings->set('dialog_tray_width', '400')->save(); + } +} diff --git a/mercury_editor.libraries.yml b/mercury_editor.libraries.yml index 2feccc514e44170789ded45dfa67294bb35fc758..5d5ad8d934f559132082184879ef730bb40b4dcf 100755 --- a/mercury_editor.libraries.yml +++ b/mercury_editor.libraries.yml @@ -14,8 +14,6 @@ mercury_editor: build/css/components/table.css: {} theme: build/css/theme/theme.css: {} - js: - build/js/me-dragula.js: {} dependencies: - mercury_editor/base - mercury_editor/unpublished_hint diff --git a/mercury_editor.module b/mercury_editor.module index 362b6ab9e4c02b7fe95e687be4cdb9d788e36da0..9e54d91a8afd12d1cd8d09513503aec10f64e0cf 100755 --- a/mercury_editor.module +++ b/mercury_editor.module @@ -1,16 +1,18 @@ <?php +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Url; use Drupal\Core\Render\Element; use Drupal\Core\Entity\EntityInterface; use Drupal\Component\Serialization\Yaml; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Form\FormStateInterface; +use Drupal\mercury_editor\Entity\MercuryEditorBlockContentForm; +use Drupal\mercury_editor\Entity\MercuryEditorNodeForm; +use Drupal\mercury_editor\Entity\MercuryEditorTermForm; use Drupal\mercury_editor\EntityTypeInfo; -use Drupal\Core\Entity\EntityFormInterface; +use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Entity\ContentEntityInterface; -use Laminas\Diactoros\Response\RedirectResponse; -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; /** * @file @@ -35,9 +37,6 @@ function mercury_editor_library_info_alter(&$libraries, $extension) { if ($extension == 'image_radios' && isset($libraries['image_radios'])) { $libraries['image_radios']['dependencies'][] = 'mercury_editor/image_radios'; } - if ($extension == 'claro' && isset($libraries['media_library.theme'])) { - $libraries['media_library.theme']['dependencies'][] = 'mercury_editor/claro.media_library.theme'; - } if ($extension == 'gin' && isset($libraries['media_library'])) { // When using the gin_toolbar module, we need to add the gin_base and @@ -57,11 +56,8 @@ function mercury_editor_library_info_alter(&$libraries, $extension) { * Replaces Drupal's ajax Dialog commands with MercuryDialog commands. */ function mercury_editor_ajax_render_alter(array &$data): void { - $current_route_name = \Drupal::routeMatch()->getRouteName(); - if ( - isset($current_route_name) - && str_starts_with($current_route_name, 'mercury_editor') - ) { + $route_name = \Drupal::routeMatch()->getRouteName() ?? ''; + if (str_contains($route_name, 'mercury_editor') || \Drupal::request()->query->has('me_id')) { foreach ($data as &$command) { if ($command['command'] == 'openDialog') { $command['command'] = 'openMercuryDialog'; @@ -70,7 +66,6 @@ function mercury_editor_ajax_render_alter(array &$data): void { $command['command'] = 'closeMercuryDialog'; } } - Drupal::service('mercury_editor.ajax_adapter')->ajaxRenderAlter($data); } } @@ -81,17 +76,12 @@ function mercury_editor_ajax_render_alter(array &$data): void { * @see contextual_preprocess() */ function mercury_editor_preprocess(array &$variables, $hook, $info) { - - // Set a css class if the entity is being edited with mercury. - if (isset($variables['elements']['#is_mercury_edit_mode'])) { - $route_match = \Drupal::routeMatch(); - $route_name = $route_match->getRouteName(); - if (\Drupal::service('mercury_editor.context')->isPreview() || $route_name == 'mercury_editor.editor') { - $variables['page'] = TRUE; - } - $variables['attributes']['class'][] = 'is-mercury-edit-mode'; + if (empty($variables['title_suffix']['contextual_links'])) { + return; + } + if (!\Drupal::service('mercury_editor.context')->isPreview()) { + return; } - // Determine the primary theme function argument. if (!empty($info['variables'])) { $keys = array_keys($info['variables']); @@ -107,9 +97,7 @@ function mercury_editor_preprocess(array &$variables, $hook, $info) { if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) { // Disable contextual links on the preview route. $variables['title_suffix']['#cache']['contexts'][] = 'route.name.is_mercury_editor_preview'; - if (\Drupal::service('mercury_editor.context')->isPreview()) { - unset($variables['title_suffix']['contextual_links']); - } + unset($variables['title_suffix']['contextual_links']); } } @@ -122,13 +110,17 @@ function mercury_editor_preprocess_layout_paragraphs_builder_controls(&$variable /** @var \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout */ $layout = $variables['layout_paragraphs_layout']; - $is_mercury_editor_context = $layout->getSetting('mercury_editor_context') ?? FALSE; $component = $layout->getComponentByUuid($variables['uuid']); $paragraph = $component->getEntity(); $paragraph_type = $paragraph->bundle(); - if ($is_mercury_editor_context) { + if (Drupal::service('mercury_editor.context')->isPreview()) { + foreach ($variables['controls'] as &$control) { + if (isset($control['#url']) && substr($control['#url']->toString(), 0, 1) !== '#') { + _mercury_editor_add_me_id($control['#url']); + } + } $variables['controls']['label']['#suffix'] = '<span class="reveal-on-hover">'; $variables['controls']['delete_link']['#suffix'] = '</span>'; @@ -137,20 +129,23 @@ function mercury_editor_preprocess_layout_paragraphs_builder_controls(&$variable $component = $layout->getComponentByUuid($uuid); $type = $component->getEntity()->getParagraphType(); + // Alters the edit link. $edit_dialog_options = json_decode($variables['controls']['edit_link']['#attributes']['data-dialog-options']); $edit_dialog_options->height = 'max-content'; $edit_dialog_options->resizable = TRUE; $edit_dialog_options = Drupal::service('mercury_editor.dialog')->dialogSettings(['layout' => $layout, 'dialog' => $paragraph_type . '_form']); $variables['controls']['edit_link']['#attributes']['data-dialog-options'] = json_encode($edit_dialog_options); $variables['controls']['edit_link']['#attributes']['title'] = t('Edit :type', [':type' => $type->label()]); - _mercury_editor_replace_ajax_class($variables['controls']['edit_link']['#attributes']['class']); _mercury_editor_replace_layout_paragraphs_routes($variables['controls']['edit_link']['#url']); + // Alters the delete link. $delete_dialog_options = Drupal::service('mercury_editor.dialog')->dialogSettings(['layout' => $layout, 'dialog' => 'delete_form']); $variables['controls']['delete_link']['#attributes']['data-dialog-options'] = json_encode($delete_dialog_options); $variables['controls']['delete_link']['#attributes']['title'] = t('Delete :type', [':type' => $type->label()]); - _mercury_editor_replace_ajax_class($variables['controls']['delete_link']['#attributes']['class']); _mercury_editor_replace_layout_paragraphs_routes($variables['controls']['delete_link']['#url']); + + // Alters the duplicate link. + _mercury_editor_replace_layout_paragraphs_routes($variables['controls']['duplicate_link']['#url']); } } @@ -161,31 +156,21 @@ function mercury_editor_preprocess_layout_paragraphs_builder_controls(&$variable * of directly invoking an Ajax action. */ function mercury_editor_preprocess_layout_paragraphs_insert_component_btn(&$variables) { - /** @var \Drupal\Core\Url $variables['url'] */ - $parameters = $variables['url']->getRouteParameters(); - $layout_id = $parameters['layout_paragraphs_layout']; - $layout = \Drupal::service('layout_paragraphs.tempstore_repository')->getWithStorageKey($layout_id); - $is_mercury_editor_context = $layout->getSetting('mercury_editor_context') ?? FALSE; - if (!$is_mercury_editor_context) { + if (!Drupal::service('mercury_editor.context')->isPreview()) { return; } - _mercury_editor_replace_ajax_class($variables['attributes']['class']); + _mercury_editor_add_me_id($variables['url']); _mercury_editor_replace_layout_paragraphs_routes($variables['url']); $old_options = json_decode($variables['attributes']['data-dialog-options'], TRUE); $dialog_options = ['target' => $old_options['target']] + Drupal::service('mercury_editor.dialog')->dialogSettings(['dialog' => 'component_menu']); $variables['attributes']['data-dialog-options'] = json_encode($dialog_options); } -/** - * Helper function to replace use-ajax classes with use-postmessage. - * - * @param array $classes - * An array of classes. - */ -function _mercury_editor_replace_ajax_class(&$classes) { - if (($key = array_search('use-ajax', $classes)) !== FALSE) { - unset($classes[$key]); - $classes[] = 'use-postmessage'; +function _mercury_editor_add_me_id(Url &$url) { + if ($entity = \Drupal::service('mercury_editor.context')->getEntity()) { + $query = $url->getOption('query'); + $query['me_id'] = $entity->uuid(); + $url->setOption('query', $query); } } @@ -233,6 +218,13 @@ function mercury_editor_theme_suggestions_layout_paragraphs_builder_component_me if ($route_name === 'mercury_editor.builder.choose_component') { $suggestions[] = 'layout_paragraphs_builder_component_menu__mercury_editor'; } + foreach ($variables['types'] as $type => &$links) { + foreach ($links as $key => &$link) { + if (isset($link['url_object'])) { + _mercury_editor_add_me_id($link['url_object']); + } + } + } } /** @@ -259,25 +251,45 @@ function mercury_editor_entity_build_defaults_alter(array &$build, EntityInterfa } } +/** + * Implements hook_block_build_alter(). + * + * Add cache context for mercury editor preview screen. + */ +function mercury_editor_block_build_alter(array &$build, BlockPluginInterface $block) { + $me_entity_types = \Drupal::config('mercury_editor.settings')->get('bundles'); + if (empty($me_entity_types['block_content'])) { + return; + } + $build['#cache']['contexts'][] = 'route.name.is_mercury_editor_preview'; +} + /** * Implements hook_build_alter(). */ function mercury_editor_entity_display_build_alter(&$build, $context) { - $route_name = \Drupal::routeMatch()->getRouteName(); - if (!\Drupal::service('mercury_editor.context')->isPreview() && $route_name !== 'mercury_editor.editor') { + if (empty($context['entity'])) { + return; + } + if (!\Drupal::service('mercury_editor.context')->isPreview()) { + return; + } + $mercury_editor_entity = \Drupal::service('mercury_editor.context')->getEntity(); + if (empty($mercury_editor_entity)) { return; } - /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */ - $entity = $context['entity']; - if (!$entity) { + if ($context['entity']->uuid() != $mercury_editor_entity->uuid()) { return; } - $build['#is_mercury_edit_mode'] = TRUE; + + // Adds a data attribute to the entity wrapper for Mercury Editor. + $build['#attributes']['data-me-edit-screen-key'] = $mercury_editor_entity->uuid(); + $build['#attributes']['class'][] = 'is-mercury-edit-mode'; // Turns on Layout Paragraphs builder for Mercury Editor LP fields. - if (isset($entity->lp_storage_keys)) { - foreach ($entity->lp_storage_keys as $field_name => $lp_key) { + if (isset($mercury_editor_entity->lp_storage_keys)) { + foreach ($mercury_editor_entity->lp_storage_keys as $field_name => $lp_key) { if (isset($build[$field_name])) { $layout = \Drupal::service('layout_paragraphs.tempstore_repository')->getWithStorageKey($lp_key); $build[$field_name] = [ @@ -288,6 +300,7 @@ function mercury_editor_entity_display_build_alter(&$build, $context) { } } } + } /** @@ -356,11 +369,16 @@ function mercury_editor_preprocess_html(&$variables) { return strpos($value, 'adminimal-admin-toolbar') !== 0; }); } - // Check if navigation is enabled. - if (\Drupal::moduleHandler()->moduleExists('navigation')) { - // Remove navigation from the page. - unset($variables['page_top']['navigation']); - } + } +} + +/** + * Implements hook_preprocess_node(). + */ +function mercury_editor_preprocess_node(&$variables) { + $mercury_editor_entity = \Drupal::service('mercury_editor.context')->getEntity(); + if ($mercury_editor_entity && $variables['node']->uuid() == $mercury_editor_entity->uuid()) { + $variables['page'] = TRUE; } } @@ -407,6 +425,16 @@ function mercury_editor_preprocess_page__mercury_editor(&$variables) { } } +/** + * Implements hook_preprocess_taxonomy_term(). + */ +function mercury_editor_preprocess_taxonomy_term(&$variables) { + $mercury_editor_entity = \Drupal::service('mercury_editor.context')->getEntity(); + if ($mercury_editor_entity && $variables['term']->uuid() == $mercury_editor_entity->uuid()) { + $variables['page'] = TRUE; + } +} + /** * Implements hook_theme_registry_alter(). * @@ -546,42 +574,63 @@ function mercury_editor_preprocess_layout_paragraphs_builder_component_menu(&$va } $groups = Drupal::config('mercury_editor.menu.settings')->get('groups'); - $groups_array = !empty($groups) - ? Yaml::decode($groups) - : [ - 'default' => [ - 'label' => 'Default', - 'components' => [], - ] + $types = $variables['types']['content'] + $variables['types']['layout']; + $variables['count'] = count($types); + + if (!empty($groups)) { + $groups_array = Yaml::decode($groups); + $variables['groups'] = []; + foreach ($groups_array as $name => &$group) { + $variables['groups'][$name] = [ + 'items' => array_filter(array_map(function ($component) use ($types) { + return $types[$component] ?? FALSE; + }, array_combine($group['components'], $group['components']))), + 'label' => $group['label'], ]; - $types = $variables['types']['content']; - $variables['groups'] = []; - foreach ($groups_array as $name => &$group) { - $variables['groups'][$name] = [ - 'items' => array_filter(array_map(function ($component) use ($types) { - return $types[$component] ?? FALSE; - }, array_combine($group['components'], $group['components']))), - 'label' => $group['label'], + } + $default_group = key(array_filter($groups_array, function($group) { + return !empty($group['default']); + })) ?? 'default'; + + $variables['groups'] = array_filter($variables['groups'], function($group, $id) use ($default_group) { + return count($group['items']) || $id == $default_group; + }, ARRAY_FILTER_USE_BOTH); + + $orphaned_types = array_filter(array_keys($types), function($type) use ($variables) { + foreach ($variables['groups'] as $group) { + if (!empty($group['items'][$type])) { + return FALSE; + } + } + return TRUE; + }); + foreach ($orphaned_types as $type) { + $variables['groups'][$default_group]['items'][$type] = $types[$type]; + } + } + else { + $variables['groups'] = [ + 'layout' => [ + 'items' => $variables['types']['layout'], + 'label' => t('Layout'), + ], + 'content' => [ + 'items' => $variables['types']['content'], + 'label' => t('Content'), + ], ]; } - $default_group = key(array_filter($groups_array, function($group) { - return !empty($group['default']); - })) ?? 'default'; - $variables['groups'] = array_filter($variables['groups'], function($group, $id) use ($default_group) { - return count($group['items']) || $id == $default_group; + $template_components = array_filter($types, function($group, $id) { + return strpos($id, 'me_template_') === 0; }, ARRAY_FILTER_USE_BOTH); - $orphaned_types = array_filter(array_keys($types), function($type) use ($variables) { - foreach ($variables['groups'] as $group) { - if (!empty($group['items'][$type])) { - return FALSE; - } - } - return TRUE; - }); - foreach ($orphaned_types as $type) { - $variables['groups'][$default_group]['items'][$type] = $types[$type]; + if (!empty($template_components)) { + $variables['groups']['templates'] = [ + 'items' => $template_components, + 'label' => t('Templates'), + ]; } + $variables['#attached']['library'][] = 'mercury_editor/menu'; $variables['#attached']['library'][] = 'mercury_editor/lpb_component_list'; } @@ -591,14 +640,6 @@ function mercury_editor_preprocess_layout_paragraphs_builder_component_menu(&$va * Implements hook_preprocess_layout_paragraphs_builder(). */ function mercury_editor_preprocess_layout_paragraphs_builder(&$variables) { - $entity = $variables['layout_paragraphs_layout']->getEntity(); - $field_name = $variables['layout_paragraphs_layout']->getFieldName(); - $field_ref = [ - $entity->getEntityTypeId(), - $entity->id(), - $field_name, - ]; - $variables['attributes']['data-lp-field-ref'] = implode('/', $field_ref); $variables['#attached']['library'][] = 'mercury_editor/mercury_editor'; } @@ -633,11 +674,16 @@ function mercury_editor_preprocess_field(&$variables) { * Implements hook_entity_type_build(). */ function mercury_editor_entity_type_build(array &$entity_types) { - if (!empty($entity_types['node'])) { - $entity_types['node']->setFormClass('mercury_editor', 'Drupal\mercury_editor\Entity\MercuryEditorNodeForm'); - } - if (!empty($entity_types['taxonomy_term'])) { - $entity_types['taxonomy_term']->setFormClass('mercury_editor', 'Drupal\mercury_editor\Entity\MercuryEditorTermForm'); + $entity_forms = [ + 'node' => MercuryEditorNodeForm::class, + 'taxonomy_term' => MercuryEditorTermForm::class, + 'block_content' => MercuryEditorBlockContentForm::class, + ]; + + foreach ($entity_forms as $entity_type => $form_class) { + if (isset($entity_types[$entity_type]) && $entity_types[$entity_type] instanceof EntityTypeInterface) { + $entity_types[$entity_type]->setFormClass('mercury_editor', $form_class); + } } } @@ -689,40 +735,6 @@ function mercury_editor_module_implements_alter(&$implementations, $hook) { } } -/** - * Implements hook_preprocess_toolbar(). - * - * Replaces the edit link with the Mercury Editor edit link. - */ -function mercury_editor_preprocess_toolbar(&$variables) { - if (!empty($variables['entity_edit_url'])) { - $entity = $variables['entity_edit_url']->getOption('entity'); - $content_type = $entity->bundle(); - if (_mercury_editor_applies_to_content_type($content_type)) { - $options = $variables['entity_edit_url']->getOptions(); - $parameters = $variables['entity_edit_url']->getRouteParameters(); - $parameters['mercury_editor_entity'] = $entity->uuid(); - $parameters['entity_type'] = $entity->getEntityTypeId(); - $variables['entity_edit_url'] = Url::fromRoute('mercury_editor.editor', $parameters, $options); - } - } -} - -/** - * Implements hook_entity_view(). - */ -function mercury_editor_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) { - // @todo Need a better way to define when this gets added. - $me_entity_types = \Drupal::config('mercury_editor.settings')->get('bundles'); - if ($view_mode == 'full' && isset($me_entity_types[$entity->getEntityTypeId()])) { - $build['#attributes']['data-me-edit-screen-key'] = $entity->uuid(); - $id = $entity->id(); - if ($id) { - $build['#attributes']['data-me-edit-screen-entity-id'] = $entity->id(); - } - } -} - /** * Impelements hook_preprocess_layout_paragraphs_builder_formatter(). */ @@ -731,13 +743,6 @@ function mercury_editor_preprocess_layout_paragraphs_builder_formatter(&$variabl $variables['#attached']['library'][] = 'mercury_editor/field_formatter'; } -/** - * Implements hook_entity_view_alter(). - */ -function mercury_editor_entity_view_alter(&$build, $entity, $display) { - $build['#attached']['library'][] = 'mercury_editor/me_dialog'; -} - /** * Implements hook_form_FORM_ID_alter(). * Implements hook_form_layout_paragraphs_builder_form_alter(). @@ -863,6 +868,9 @@ function mercury_editor_after_build_radios($element, $form_state) { return $element; } +/** + * After callback for attaching the horizontal tabs library. + */ function mercury_editor_after_build_form($form, $form_state) { if (isset($form['tabs']) && count($form['tabs']['#options']) < 2) { $form['tabs']['#access'] = FALSE; @@ -873,7 +881,6 @@ function mercury_editor_after_build_form($form, $form_state) { return $form; } - /** * Implements hook_form_FORM_ID_alter(). * Implements hook_layout_paragraphs_delete_component_form_alter(). @@ -882,20 +889,3 @@ function mercury_editor_form_layout_paragraphs_delete_component_form_alter(&$for $form['actions']['#attributes']['class'][] = 'me-form-actions'; $form['#attached']['library'][] = 'mercury_editor/lpb_component_delete_form'; } - -/** - * Returns TRUE if $type uses Mercury Editor. - * - * @param string $type - * The content type. - * - * @return bool - * TRUE if applies to content type. - */ -function _mercury_editor_applies_to_content_type($type) { - $content_types = \Drupal::configFactory()->get('mercury_editor.settings')->get('content_types'); - if (empty($content_types[$type])) { - return FALSE; - } - return TRUE; -} diff --git a/mercury_editor.routing.yml b/mercury_editor.routing.yml index 2e7310039f34be6bf6257d1a4c463f8898540898..22468492cab9c0ee67a3ae187a671682c3491ac4 100644 --- a/mercury_editor.routing.yml +++ b/mercury_editor.routing.yml @@ -45,7 +45,7 @@ mercury_editor.dialog_settings: # Mercury Editor replacements for Layout Paragraphs Builder routes. mercury_editor.builder.insert: - path: '/mercury-editor/{layout_paragraphs_layout}/insert/{paragraph_type}' + path: '/mercury-editor/{layout_paragraphs_layout}/insert/{paragraph_type_id}' defaults: _controller: '\Drupal\mercury_editor\Controller\InsertComponentController::skipInsertForm' operation: 'create' @@ -53,8 +53,6 @@ mercury_editor.builder.insert: parameters: layout_paragraphs_layout: layout_paragraphs_layout_tempstore: TRUE - paragraph_type: - type: entity:paragraphs_type requirements: _layout_paragraphs_builder_access: 'TRUE' mercury_editor.builder.edit_item: @@ -68,6 +66,17 @@ mercury_editor.builder.edit_item: layout_paragraphs_layout_tempstore: TRUE requirements: _layout_paragraphs_builder_access: 'TRUE' +mercury_editor.builder.duplicate_item: + path: '/mercury-editor/{layout_paragraphs_layout}/duplicate/{source_uuid}' + defaults: + _controller: '\Drupal\mercury_editor\Controller\DuplicateController::duplicate' + operation: 'duplicate' + options: + parameters: + layout_paragraphs_layout: + layout_paragraphs_layout_tempstore: TRUE + requirements: + _layout_paragraphs_builder_access: 'TRUE' mercury_editor.builder.delete_item: path: '/mercury-editor/{layout_paragraphs_layout}/delete/{component_uuid}' defaults: diff --git a/mercury_editor.services.yml b/mercury_editor.services.yml index b27f04b77a586f03ef7ce5b9281c97d3e77a846e..60fcea43d0ebd3fb496d093bf6ea3b1d0eba6c56 100644 --- a/mercury_editor.services.yml +++ b/mercury_editor.services.yml @@ -11,12 +11,6 @@ services: arguments: ['@config.factory', '@request_stack'] tags: - { name: theme_negotiator, priority: 1500 } - mercury_editor.ajax_adapter: - class: Drupal\mercury_editor\AjaxResponseIframeAdapter - arguments: - - '@theme.manager' - - '@config.factory' - - '@theme.initialization' mercury_editor.iframe_ajax_response_wrapper: class: Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper arguments: @@ -24,6 +18,7 @@ services: - '@config.factory' - '@theme.initialization' - '@mercury_editor.attachments_processor' + - '@mercury_editor.context' mercury_editor.attachments_processor: class: Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapperAttachmentsProcessor parent: ajax_response.attachments_processor @@ -32,7 +27,7 @@ services: arguments: ['@config.factory'] cache_context.route.name.is_mercury_editor_preview: class: Drupal\mercury_editor\Cache\MercuryEditorPreviewCacheContext - arguments: ['@current_route_match'] + arguments: ['@current_route_match', '@mercury_editor.context'] tags: - { name: cache.context } mercury_editor.param_converter: @@ -61,7 +56,7 @@ services: - { name: event_subscriber } mercury_editor.context: class: Drupal\mercury_editor\MercuryEditorContextService - arguments: ['@current_route_match'] + arguments: ['@current_route_match', '@layout_paragraphs.tempstore_repository', '@mercury_editor.tempstore_repository', '@request_stack'] mercury_editor.preview_routes_subscriber: class: \Drupal\mercury_editor\Routing\MercuryEditorPreviewRoutes arguments: ['@entity_type.manager'] diff --git a/modules/mercury_editor_templates/mercury_editor_templates.module b/modules/mercury_editor_templates/mercury_editor_templates.module index a616f8f46ae1ae48ff196d88d9714886aa033990..23aa9b28eaa427e54aca448710b20c98008eff5c 100644 --- a/modules/mercury_editor_templates/mercury_editor_templates.module +++ b/modules/mercury_editor_templates/mercury_editor_templates.module @@ -100,7 +100,7 @@ function mercury_editor_templates_preprocess_layout_paragraphs_builder_controls( '#attributes' => [ 'class' => [ 'lpb-save-as-template', - 'use-postmessage', + 'use-ajax', ], 'data-dialog-type' => 'dialog', 'data-dialog-options' => Json::encode($dialog_settings), diff --git a/source/css/components/drag-hint.css b/source/css/components/drag-hint.css index 65400868988160b2de3775921b66aba37a0e000f..1e98353fafbe014122a0211bfe227a23249a1768 100644 --- a/source/css/components/drag-hint.css +++ b/source/css/components/drag-hint.css @@ -1,3 +1,15 @@ .lp-hint { + z-index: 1000; +} +.lp-hint[data-orientation="vertical"] { + top: 0; + border-right: 5px solid blue; +} +.lp-hint[data-orientation="horizontal"] { + left: 0; + width: 100%; border-top: 5px solid blue; } +.gu-mirror { + opacity: 1 !important; +} diff --git a/source/css/components/form.css b/source/css/components/form.css index 90cc189cfc48cf653ba4775e665ef1d7274bc108..fb236a96595dbd8d3cbc07e274941fecb13952bc 100644 --- a/source/css/components/form.css +++ b/source/css/components/form.css @@ -64,7 +64,7 @@ mercury-dialog .form-item__description { } } - & input:not([type="checkbox"]):not([type="radio"]), + & input:not([type="checkbox"]):not([type="radio"]):not(.media-library-item__remove), & textarea, & select { height: auto; diff --git a/source/css/components/frontend-builder.css b/source/css/components/frontend-builder.css index 07af716f27ff9006d15cadf633c472755d9cb83d..18fc08d13404ab56306c8c1f3db95a5624ba48b0 100644 --- a/source/css/components/frontend-builder.css +++ b/source/css/components/frontend-builder.css @@ -28,13 +28,14 @@ * Component hover styles. Uses a class instead of hover state for slight * pause, to avoid jumpiness. */ -.js-lpb-component.focused { +.lp-builder:not(.is-navigating) .js-lpb-component.focused { outline: 1px solid blue; + z-index: 1000; } -.js-lpb-component.focused .js-lpb-region { +.lp-builder:not(.is-navigating) .js-lpb-component.focused .js-lpb-region { outline: 1px dotted rgba(0, 0, 255, 0.5); } -.js-lpb-component.focused > .js-lpb-ui { +.lp-builder:not(.is-navigating) .js-lpb-component.focused > .js-lpb-ui { opacity: 1; } @@ -42,6 +43,9 @@ * Mercury Editor controls modifications. */ .is-mercury-edit-mode { + & .lp-builder { + z-index: 100; + } & .lpb-controls { padding: 0 5px 0 0; border-radius: 4px; @@ -74,6 +78,25 @@ font-size: .7em; letter-spacing: 2px; } + & .lpb-tooltiptext { + left: var(--me-lpb-tooltip-text-left, -12px); + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); + width: 1px; + height: 1px; + word-wrap: normal; + } + & .lp-builder:not(.is-dragging) .lpb-tooltip--focus:focus + .lpb-tooltiptext, + & .lp-builder:not(.is-dragging) .lpb-tooltip--hover:hover + .lpb-tooltiptext, + & .lpb-tooltiptext--visible { + overflow: visible; + clip: auto; + width: auto; + height: auto; + } + & .lpb-tooltiptext::after { + left: var(--me-lpb-tooltip-text-arrow-left, 20px); + } } @keyframes controlsSlideOpen { diff --git a/source/css/menu.css b/source/css/menu.css index 25fed24c47079cc8c748f11434cb6c723da3ea5d..1e2641e40e57beb9ea4a837b366d78c2a89b4c41 100644 --- a/source/css/menu.css +++ b/source/css/menu.css @@ -89,4 +89,7 @@ &.type-card a::before { background: url('../../images/menu-icons/icon-card.png'); } + &.type-tabs a::before { + background: url('../../images/menu-icons/icon-tabs.png'); + } } diff --git a/source/js/component-form.js b/source/js/component-form.js index 69df0b01f9e9ff51b811a2d4fc11273d35f86a5f..8fee3d697d5e40e4fd62eb222d672278dd3606cc 100644 --- a/source/js/component-form.js +++ b/source/js/component-form.js @@ -10,7 +10,7 @@ layoutSelect.focus(); } } - const form = once('me-component-form', '.layout-paragraphs-component-form')[0]; + const form = once('me-component-form', 'mercury-dialog .layout-paragraphs-component-form')[0]; if (form) { form.closest('mercury-dialog').addEventListener('open', (e) => { const dialog = e.target.shadowRoot.querySelector('dialog'); diff --git a/source/js/edit-screen.js b/source/js/edit-screen.js index 6d8403a748b719c10267e54da4eef9c2e3e0e092..259b66661d3cbed996744a7afd633a5a6bc2d622 100644 --- a/source/js/edit-screen.js +++ b/source/js/edit-screen.js @@ -142,6 +142,7 @@ sidebarToggle.classList.remove('me-button--sidebar-expand'); sidebarToggle.classList.add('me-button--sidebar-collapse'); sidebarToggle.innerHTML = `<span>${Drupal.t('Hide sidebar')}</span>`; + sidebarToggle.setAttribute('title', Drupal.t('Hide sidebar')); localStorage.removeItem('mercury-dialog-dock-collapsed'); } else { @@ -149,6 +150,7 @@ sidebarToggle.classList.remove('me-button--sidebar-collapse'); sidebarToggle.classList.add('me-button--sidebar-expand'); sidebarToggle.innerHTML = `<span>${Drupal.t('Show sidebar')}</span>`; + sidebarToggle.setAttribute('title', Drupal.t('Show sidebar')); localStorage.setItem('mercury-dialog-dock-collapsed', 'true'); } @@ -217,6 +219,11 @@ document.addEventListener('mouseup', iFramePointerEventsToggle); } } + // Set the iframe URL once other js files have loaded. + if (once('me-preview-iframe', '#me-preview', context).length) { + const iframe = document.querySelector('#me-preview'); + iframe.src = iframe.getAttribute('data-src'); + } } } })(Drupal, drupalSettings, jQuery, once) diff --git a/source/js/horizontal-tabs.js b/source/js/horizontal-tabs.js index e03030056d887e9fc8fd0c40aca0b253cdbf695e..db5f8c48f5c065cd81eeeece5e82a01fa0ad872c 100644 --- a/source/js/horizontal-tabs.js +++ b/source/js/horizontal-tabs.js @@ -21,6 +21,17 @@ const selected = tabs.querySelector('input[type="radio"]:checked') || {}; toggleTabs(selected.value); }); + if (document.querySelector('.me-tab-group .error')) { + const tabGroup = document.querySelector('.me-tab-group .error').closest('.me-tab-group'); + // Get the class that starts with "me-tab-group--" and get the part after "--". + const selectedValue = tabGroup.className.match(/me-tab-group--([^ ]+)/)[1]; + // Get the tab radio button with the same value as the tab group. + const selected = document.querySelector(`.me-tabs input[type="radio"][value="${selectedValue}"]`); + if (selected) { + selected.checked = true; + toggleTabs(selectedValue); + } + } } } diff --git a/source/js/me-dragula.js b/source/js/me-dragula.js deleted file mode 100644 index 818ac68ec73b0cba8de73fa7ad17fc6cd4de9741..0000000000000000000000000000000000000000 --- a/source/js/me-dragula.js +++ /dev/null @@ -1,200 +0,0 @@ -(($, Drupal, once) => { - - /** - * Ensures that all layout paragraphs controls are fully within viewport. - * - * @param {jQuery} $builder - * The Layout Paragraphs container jQuery object. - */ - function repositionControls($builder) { - $builder.find('.lpb-controls').each((i, controls) => { - controls.setAttribute('style', controls.getAttribute('data-style')); - const bounding = controls.getBoundingClientRect(); - // Left viewport edge. - const l = 0; - // Right viewport edge. - const r = (window.innerWidth || document.documentElement.clientWidth); - // Overlapping left. - if (bounding.left < l) { - controls.setAttribute('data-style', controls.getAttribute('style')); - $(controls).offset({left: 0}); - } - // Overlapping right. - if (bounding.right > r) { - $(controls).css({right: (bounding.right - r) + 'px'}); - } - }); - } - - /** - * Simplifies drag and drop visual cues to prevent jumpiness. - * - * The default behavior of the dragula library can create excessive - * jumpiness in some cases. This function simplifies the UI and drag and drop - * experience in several key ways, including: - * - * - Detaches all layout paragraphs UI elements when dragging starts. - * - Provides a simple "hint" element to show where an item will be dropped. - * - Leaves a "ghost" copy of the grabbed element in place at the source. - * - Reattaches all UI elements when dragging ends. - * - * @see https://github.com/bevacqua/dragula#drakeon-events. - * - * @param {Object} drake - * The dragula object. - */ - function simplifyDragHints($builder, settings) { - - const drake = $builder.data('drake'); - let ghost, grabbed; - const hint = $('<div class="lp-hint hidden"></div>')[0]; - - // Hide UI elements when dragging starts. - drake.on('drag', (el) => { - el.parentNode.insertBefore(hint, el); - $builder.find('.js-lpb-ui').addClass('hidden'); - }); - // Provide a simple hint element to indicate where an item will be dropped. - drake.on('shadow', (item, container) => { - if (item.classList.contains('lp-hint')) { - return; - } - // Remove comments and text nodes from container. - [...container.childNodes].filter((e) => e.classList === undefined).forEach((e) => e.remove()); - container.replaceChild(hint, item); - - // Ensure the hint does not get at the end of the region after the add button. - if (hint.nextSibling === null && hint.previousSibling !== null && hint.previousSibling.classList.contains('lpb-btn--add')) { - container.insertBefore(hint, hint.previousSibling); - } - - const nextIsGhost = hint.nextSibling !== null ? hint.nextSibling.classList.contains('lp-ghost') : false; - const prevIsGhost = hint.previousSibling !== null ? hint.previousSibling.classList.contains('lp-ghost') : false; - const ghostAdjacent = nextIsGhost || prevIsGhost; - if (ghostAdjacent) { - hint.classList.add('hidden'); - ghost.classList.remove('gu-transit'); - } - else { - hint.classList.remove('hidden'); - ghost.classList.add('gu-transit'); - } - }); - // Leave a copy of the grabbed item in place at the original source. - drake.on('cloned', (mirror, item) => { - ghost = item.cloneNode(true); - ghost.classList.add('lp-ghost'); - item.parentNode.insertBefore(ghost, item); - grabbed = item; - item.remove(); - }); - // Show UI elements and remove ghost and hint elements when dragging stops. - drake.on('dragend', (el) => { - hint.replaceWith(grabbed); - ghost.remove(); - $builder.find('.js-lpb-ui').removeClass('hidden'); - repositionControls($builder); - $builder.trigger('lpb-component:drop', [el.getAttribute('data-uuid')]); - }); - } - - /** - * Waits for a condition to be met, then calls the provided callback. - * - * @param {Function} cond - * The condition to wait for. - * @param {Function} cb - * The callback to call when cond evaluates true. - */ - function waitFor(cond, cb) { - const i = setInterval(() => { - if (cond() === true) { - clearInterval(i); - cb(); - } - }, 100); - } - - function removeMovedFormActions() { - const outdatedFormActions = document.getElementsByClassName('lpb-form__actions repositioned'); - if (outdatedFormActions.length) { - outdatedFormActions[0].remove(); - } - } - - function moveFormActions(context) { - const formActions = context.querySelector('.lpb-form__actions'); - if (formActions) { - removeMovedFormActions(); - formActions.classList.add('repositioned'); - document.body.appendChild(formActions); - } - } - - function showControls(e) { - const el = e.target.closest('.lpb-controls, .js-lpb-component'); - el.classList.add('focused'); - el.classList.remove('transitioning'); - el.classList.remove('blurred'); - } - - function hideControls(e) { - const el = e.target.closest('.lpb-controls, .js-lpb-component'); - el.classList.add('transitioning'); - setTimeout(() => { - if (el.classList.contains('transitioning')) { - el.classList.remove('focused'); - el.classList.remove('transitioning'); - el.classList.add('blurred'); - } - }, 250); - } - - Drupal.behaviors.mercuryEditorDragula = { - attach: function attach(context, settings) { - // Append form-actions to body for better styling control. - if (once('me-builder-events', 'html', context).length) { - $(window).on('lpb-builder:open.lpb lpb-builder:save.lpb', (e) => { - moveFormActions(context); - }); - $(window).on('lpb-builder:close.lpb', (e) => { - removeMovedFormActions(); - }); - } - var events = ['lpb-component:insert.lpb', 'lpb-component:update.lpb', 'lpb-component:move.lpb', 'lpb-component:drop.lpb'].join(' '); - $(once('me-builder-form', '[data-lpb-id]', context)).on(events, function (e) { - const cancelButton = document.querySelector('lpb-form__actions repositioned .lpb-btn--cancel'); - if (cancelButton) { - cancelButton.value = Drupal.t('Cancel') - } - }); - once('me-dragula', '.lp-builder.has-components').forEach((builder) => { - const $builder = $(builder); - waitFor( - () => $builder.data('drake') !== undefined, - () => { - repositionControls($builder); - moveFormActions($builder[0]); - $(window).resize(() => repositionControls($builder)); - // Simplifies drag and drop functionality. - try { - simplifyDragHints($builder, settings); - } - catch (e) { - console.warn(e); - } - }); - }); - once('reveal-on-hover', '.js-lpb-component').forEach((component) => { - component.addEventListener('mouseenter', showControls); - component.addEventListener('mouseleave', hideControls); - }); - once('reveal-on-hover', '.lpb-controls').forEach((el) => { - el.addEventListener('mouseenter', showControls); - el.addEventListener("focusin", showControls); - el.addEventListener('mouseleave', hideControls); - el.addEventListener("focusout", hideControls); - }); - } - } -})(jQuery, Drupal, once); diff --git a/source/js/post-messages-listener.js b/source/js/post-messages-listener.js index 5df43c773d28f05d0a2fec21d2ecc593ce7496c5..6cd3b913ed118f22b57dc35a00ddfb916f44243b 100644 --- a/source/js/post-messages-listener.js +++ b/source/js/post-messages-listener.js @@ -8,14 +8,6 @@ * @param {Object} settings The ajax settings. */ drupalAjax: function (settings) { - console.log('drupalAjax', settings); - Drupal.ajax(settings).execute(); - }, - /** - * Ajax click handler for Layout Paragraphs UI elements in iframe. - * @param {Object} settings The ajax settings. - */ - lpbUiClick: function (settings) { Drupal.ajax(settings).execute(); }, /** diff --git a/source/js/preview-screen.js b/source/js/preview-screen.js index 33e6bc524cb6e97a9375d517c520f1161d264242..509423b0efb9d7bdabd71685435432ab30c5f1de 100644 --- a/source/js/preview-screen.js +++ b/source/js/preview-screen.js @@ -1,4 +1,4 @@ -((Drupal, drupalSettings, once) => { +((Drupal, drupalSettings, $, once) => { 'use strict'; /** @@ -30,7 +30,7 @@ e.currentTarget.classList.add('is-me-focused'); // Then, send the click event. const message = { - type: 'lpbUiClick', + type: 'drupalAjax', settings: { dialogType: e.currentTarget.getAttribute('data-dialog-type'), dialog: JSON.parse(e.currentTarget.getAttribute('data-dialog-options')), @@ -44,18 +44,202 @@ return false; } + function scaleMirror(e) { + const mirror = document.querySelector('.gu-mirror'); + if (!mirror) { + return; + } + const scaleAxis = mirror.offsetWidth > mirror.offsetHeight ? 'offsetHeight' : 'offsetWidth'; + const scale = Math.min(300 / mirror[scaleAxis], 1); + const boundingRect = mirror.getBoundingClientRect(); + const diffX = e.clientX - boundingRect.x; + const diffY = e.clientY - boundingRect.y; + mirror.style.setProperty('transform-origin', `${diffX}px ${diffY}px`); + mirror.style.setProperty('transform', `scale(${scale})`); + window.removeEventListener('mousemove', scaleMirror); + } + + /** + * Simplifies drag and drop visual cues to prevent jumpiness. + * + * The default behavior of the dragula library can create excessive + * jumpiness in some cases. This function simplifies the UI and drag and drop + * experience in several key ways, including: + * + * - Detaches all layout paragraphs UI elements when dragging starts. + * - Provides a simple "hint" element to show where an item will be dropped. + * - Leaves a "ghost" copy of the grabbed element in place at the source. + * - Reattaches all UI elements when dragging ends. + * + * @see https://github.com/bevacqua/dragula#drakeon-events. + * + * @param {Object} drake + * The dragula object. + */ + function simplifyDragHints($builder, drake) { + + let ghost, grabbed; + const hint = $('<div class="lp-hint hidden"></div>')[0]; + // Scales the mirror element to make it easier to drag. + drake.on('cloned', (clone, original, type) => { + window.addEventListener('mousemove', scaleMirror); + }); + // Hide UI elements when dragging starts. + drake.on('drag', (el) => { + if (el.parentNode) { + el.parentNode.insertBefore(hint, el); + } + $builder.find('.js-lpb-ui').addClass('hidden'); + }); + // Provide a simple hint element to indicate where an item will be dropped. + drake.on('shadow', (shadow, container, src) => { + + if (shadow.classList.contains('lp-hint')) { + return; + } + + hint.style = { + width: '', + height: '', + marginLeft: '', + marginTop: '', + } + + const sibling = shadow.nextElementSibling || shadow.previousElementSibling; + const orientation = sibling && shadow.getBoundingClientRect().top === sibling.getBoundingClientRect().top + ? 'vertical' + : 'horizontal'; + + if (orientation == 'horizontal') { + const offset = parseInt(window.getComputedStyle(shadow.parentNode).getPropertyValue('padding-left')); + hint.style.marginLeft = '-' + offset + 'px'; + } + + if (orientation == 'vertical') { + const offset = parseInt(window.getComputedStyle(shadow.parentNode).getPropertyValue('padding-top')); + hint.style.marginTop = '-' + offset + 'px'; + } + + if (orientation === 'vertical') { + hint.style.height = shadow.parentNode.clientHeight + 'px'; + } + if (orientation === 'horizontal') { + hint.style.width = shadow.parentNode.clientWidth + 'px'; + } + + hint.setAttribute('data-orientation', orientation); + + // Remove comments and text nodes from container. + [...container.childNodes].filter((e) => e.classList === undefined).forEach((e) => e.remove()); + container.replaceChild(hint, shadow); + + // Ensure the hint does not get at the end of the region after the add button. + if (hint.nextSibling === null && hint.previousSibling !== null && hint.previousSibling.classList.contains('lpb-btn--add')) { + container.insertBefore(hint, hint.previousSibling); + } + + const nextIsGhost = hint.nextSibling !== null ? hint.nextSibling.classList.contains('lp-ghost') : false; + const prevIsGhost = hint.previousSibling !== null ? hint.previousSibling.classList.contains('lp-ghost') : false; + const ghostAdjacent = nextIsGhost || prevIsGhost; + if (ghostAdjacent) { + hint.classList.add('hidden'); + ghost.classList.remove('gu-transit'); + } + else { + hint.classList.remove('hidden'); + ghost.classList.add('gu-transit'); + } + }); + // Leave a copy of the grabbed item in place at the original source. + drake.on('cloned', (mirror, item) => { + ghost = item.cloneNode(true); + ghost.classList.add('lp-ghost'); + if (item.parentNode) { + item.parentNode.insertBefore(ghost, item); + } + grabbed = item; + item.remove(); + }); + // Show UI elements and remove ghost and hint elements when dragging stops. + drake.on('dragend', (el) => { + hint.replaceWith(grabbed); + ghost.remove(); + $builder.find('.js-lpb-ui').removeClass('hidden'); + }); + } + + /** + * Calls simplifyDragHints() when the builder is initialized. + */ + $(document).on('lpb-builder:init', (e) => { + const builder = e.target; + const drake = $(builder).data('drake'); + if (drake) { + simplifyDragHints($(builder), drake); + } + }); + + function padElements(layout) { + [...layout.querySelectorAll('[data-region]'), layout].forEach((el) => { + const computed = getComputedStyle(el); + if (!el.hasAttribute('data-me-padding')) { + el.setAttribute('data-me-padding', `${computed.paddingTop} ${computed.paddingRight} ${computed.paddingBottom} ${computed.paddingLeft}`); + } + el.style.paddingTop = Math.max(10, parseInt(computed.paddingTop)) + 'px'; + el.style.paddingRight = Math.max(10, parseInt(computed.paddingRight)) + 'px'; + el.style.paddingBottom = Math.max(10, parseInt(computed.paddingBottom)) + 'px'; + el.style.paddingLeft = Math.max(10, parseInt(computed.paddingLeft)) + 'px'; + }); + } + + function unpadElements(layout) { + [...layout.querySelectorAll('[data-region]'), layout].forEach((el) => { + if (el.hasAttribute('data-me-padding')) { + const computed = getComputedStyle(el); + el.style.padding = el.getAttribute('data-me-padding'); + } + }); + } + + function showControls(e) { + const el = e.target.closest('.lpb-controls, .js-lpb-component'); + el.classList.add('focused'); + el.classList.remove('transitioning'); + el.classList.remove('blurred'); + } + + function hideControls(e) { + const el = e.target.closest('.lpb-controls, .js-lpb-component'); + el.classList.add('transitioning'); + setTimeout(() => { + if (el.classList.contains('transitioning')) { + el.classList.remove('focused'); + el.classList.remove('transitioning'); + el.classList.add('blurred'); + } + }, 250); + } + /** * Attaches the behavior to the edit screen. */ Drupal.behaviors.mercuryEditorPreviewScreen = { attach: function(context, _settings) { + const duplicateContainers = [...document.querySelectorAll('[data-me-edit-screen-key]')] + .map((container) => container.getAttribute('data-me-edit-screen-key')) + // check for duplicates in array + .filter((value, index, self) => self.indexOf(value) !== index); + if (duplicateContainers.length > 0) { + console.error('Multiple HTML elements found using the same data attribute, "data-me-edit-screen-key", which should be unique. Make sure attributes are not passed to child elements in twig templates.', duplicateContainers); + } // Send the initial ajaxPageState to the parent window. window.parent.postMessage({ type: 'ajaxPreviewPageState', settings: drupalSettings.ajaxPageState }); // Attaches click handlers to links that use window.postMessage(). - once('me-msg-broadcaster', '.use-postmessage').forEach((el) => { + once('me-msg-broadcaster', '.js-lpb-ui.use-ajax, .js-lpb-ui .use-ajax').forEach((el) => { + $(el).off(); el.addEventListener('mousedown', preventDefault); el.addEventListener('mouseup', preventDefault); el.addEventListener('click', lpbClickHander); @@ -71,16 +255,50 @@ return false; }); } + else { + // The dragula library prevents links from automatically focusing + // on mousedown, which can cause issues with keyboard navigation. + link.addEventListener('mousedown', (e) => e.target.focus()); + } }); once('me-prevent-focus', 'a, button, input, textarea, select, details', context).forEach((focussable) => { if ( focussable.closest('.lpb-controls') === null && + focussable.closest('.mercury-editor-ui') === null && !focussable.classList.contains('use-postmessage') ) { focussable.setAttribute('tabindex', '-1'); } }); + once('me-layout-hover', '.lpb-layout').forEach((layout) => { + layout.addEventListener('mouseenter', (e) => { + e.target.setAttribute('data-mouseover', 'true'); + setTimeout(() => { + if (e.target.getAttribute('data-mouseover')) { + padElements(e.target); + } + }, 100); + }); + layout.addEventListener('mouseleave', (e) => { + e.target.removeAttribute('data-mouseover'); + setTimeout(() => { + if (!e.target.getAttribute('data-mouseover')) { + unpadElements(e.target); + } + }, 100); + }); + }); } + once('reveal-on-hover', '.js-lpb-component').forEach((component) => { + component.addEventListener('mouseenter', showControls); + component.addEventListener('mouseleave', hideControls); + }); + once('reveal-on-hover', '.lpb-controls').forEach((el) => { + el.addEventListener('mouseenter', showControls); + el.addEventListener("focusin", showControls); + el.addEventListener('mouseleave', hideControls); + el.addEventListener("focusout", hideControls); + }); } } -})(Drupal, drupalSettings, once) +})(Drupal, drupalSettings, jQuery, once) diff --git a/src/Ajax/IFrameAjaxResponseWrapper.php b/src/Ajax/IFrameAjaxResponseWrapper.php index 5d558b5af57ef2c3cdd05ecb0ad3fce2909aaf85..e313531ae3914970c2ff1eafe4b97ae9cc145755 100644 --- a/src/Ajax/IFrameAjaxResponseWrapper.php +++ b/src/Ajax/IFrameAjaxResponseWrapper.php @@ -8,6 +8,7 @@ use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Theme\ThemeInitializationInterface; use Drupal\Core\Render\AttachmentsResponseProcessorInterface; +use Drupal\mercury_editor\MercuryEditorContextService; /** * Ajax response wrapper for IFrame. @@ -53,8 +54,15 @@ class IFrameAjaxResponseWrapper extends AjaxResponse { * The theme initializer. * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $ajax_response_attachments_processor * The ajax response attachments processor. + * @param \Drupal\mercury_editor\MercuryEditorContextService $mercuryEditorContext + * The mercury editor context service. */ - public function __construct(ThemeManagerInterface $theme_manager, ConfigFactoryInterface $config_factory, ThemeInitializationInterface $theme_initializer, AttachmentsResponseProcessorInterface $ajax_response_attachments_processor) { + public function __construct( + ThemeManagerInterface $theme_manager, + ConfigFactoryInterface $config_factory, + ThemeInitializationInterface $theme_initializer, + AttachmentsResponseProcessorInterface $ajax_response_attachments_processor, + protected MercuryEditorContextService $mercuryEditorContext) { parent::__construct(NULL, 200, [], FALSE); $default_theme_name = $config_factory->get('system.theme')->get('default'); $this->siteDefaultTheme = $theme_initializer->getActiveThemeByName($default_theme_name); @@ -76,7 +84,9 @@ class IFrameAjaxResponseWrapper extends AjaxResponse { */ public function addCommand(CommandInterface $command, $prepend = FALSE) { $this->themeManager->setActiveTheme($this->siteDefaultTheme); + $this->mercuryEditorContext->setPreview(TRUE); parent::addCommand($command, $prepend); + $this->mercuryEditorContext->setPreview(FALSE); $this->themeManager->setActiveTheme($this->currentActiveTheme); return $this; } diff --git a/src/AjaxResponseIframeAdapter.php b/src/AjaxResponseIframeAdapter.php deleted file mode 100644 index 4175cf7261c7b36997f24c74f67d1ab46ff2361e..0000000000000000000000000000000000000000 --- a/src/AjaxResponseIframeAdapter.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php - -namespace Drupal\mercury_editor; - -use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Theme\ThemeManagerInterface; -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Theme\ThemeInitializationInterface; -use Drupal\mercury_editor\Ajax\IFrameCommandsWrapperCommand; - -class AjaxResponseIframeAdapter { - - protected $defaultTheme; - - protected $updatingIframe = FALSE; - - protected $themeManager; - - public function __construct(ThemeManagerInterface $theme_manager, ConfigFactoryInterface $config_factory, ThemeInitializationInterface $theme_initializer) { - $default_theme_name = $config_factory->get('system.theme')->get('default'); - $this->defaultTheme = $theme_initializer->getActiveThemeByName($default_theme_name); - $this->themeManager = $theme_manager; - } - - public function updatingIframe(){ - $this->updatingIframe = TRUE; - $this->themeManager->setActiveTheme($this->defaultTheme); - } - - public function isUpdatingIframe() { - return $this->updatingIframe; - } - - public function ajaxRenderAlter(&$data) { - if ($this->updatingIframe) { - $wrapper_commands = array_filter($data, function ($command) { - return $command['command'] !== 'closeMercuryDialog'; - }); - $data = array_filter($data, function ($command) { - return $command['command'] == 'closeMercuryDialog'; - }); - $wrapper_command = new IFrameCommandsWrapperCommand($wrapper_commands); - $data[] = $wrapper_command->render(); - } - return $data; - } - -} diff --git a/src/Cache/MercuryEditorPreviewCacheContext.php b/src/Cache/MercuryEditorPreviewCacheContext.php index ab591ceb9dec623e9ce2d9a0a776a66d39264cc2..7be028cfeec156b1df3d6de7ce5e58a6d29a0fe5 100644 --- a/src/Cache/MercuryEditorPreviewCacheContext.php +++ b/src/Cache/MercuryEditorPreviewCacheContext.php @@ -2,7 +2,9 @@ namespace Drupal\mercury_editor\Cache; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Cache\Context\RouteNameCacheContext; +use Drupal\mercury_editor\MercuryEditorContextService; /** * Determines if an entity is being viewed in Mercury Editor Preview. @@ -14,6 +16,15 @@ use Drupal\Core\Cache\Context\RouteNameCacheContext; */ class MercuryEditorPreviewCacheContext extends RouteNameCacheContext { + /** + * {@inheritDoc} + */ + public function __construct( + RouteMatchInterface $route_match, + protected MercuryEditorContextService $mercuryEditorContext) { + parent::__construct($route_match); + } + /** * {@inheritdoc} */ @@ -25,13 +36,13 @@ class MercuryEditorPreviewCacheContext extends RouteNameCacheContext { * {@inheritdoc} */ public function getContext() { - $route_name = $this->routeMatch->getRouteName(); - $is_preview = (int) ( - $route_name == 'mercury_editor.preview' - || str_ends_with($route_name, '.mercury_editor_preview') - ); - $context_value = 'is_mercury_editor_preview.' . $is_preview; - return $context_value; + if ($this->mercuryEditorContext->isPreview()) { + $entity = $this->mercuryEditorContext->getEntity(); + return 'is_mercury_editor_preview.' . $entity->getEntityTypeId() . '.' . $entity->uuid(); + } + else { + return 'is_mercury_editor_preview.0'; + } } } diff --git a/src/Controller/ChooseComponentController.php b/src/Controller/ChooseComponentController.php index 32fe14c71a1a38aabb1edbdd271dcf4a6157846a..d2d873fc447392edf4b2c6dcc4782f136c3e6cc9 100644 --- a/src/Controller/ChooseComponentController.php +++ b/src/Controller/ChooseComponentController.php @@ -10,6 +10,7 @@ use Drupal\layout_paragraphs\LayoutParagraphsLayout; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Drupal\layout_paragraphs\Event\LayoutParagraphsComponentDefaultsEvent; use Drupal\layout_paragraphs\Controller\ChooseComponentController as LayoutParagraphsChooseComponentController; /** @@ -64,7 +65,16 @@ class ChooseComponentController extends LayoutParagraphsChooseComponentControlle * An ajax response or form render array. */ protected function componentForm(string $type_name, LayoutParagraphsLayout $layout_paragraphs_layout, array $context) { - $type = $this->entityTypeManager()->getStorage('paragraphs_type')->load($type_name); + + // Dispatch a LayoutParagraphsComponentDefaultsEvent to allow other modules + // to alter the paragraph type and default values. + $event = new LayoutParagraphsComponentDefaultsEvent($type_name, []); + $this->eventDispatcher->dispatch($event, $event::EVENT_NAME); + $type = $this + ->entityTypeManager() + ->getStorage('paragraphs_type') + ->load($event->getParagraphTypeId()); + $form = $this->formBuilder()->getForm( $this->getInsertComponentFormClass(), $layout_paragraphs_layout, @@ -72,7 +82,8 @@ class ChooseComponentController extends LayoutParagraphsChooseComponentControlle $context['parent_uuid'], $context['region'], $context['sibling_uuid'], - $context['placement'] + $context['placement'], + $event->getDefaultValues(), ); if ($this->isAjax()) { $response = new AjaxResponse(); diff --git a/src/Controller/DuplicateController.php b/src/Controller/DuplicateController.php new file mode 100644 index 0000000000000000000000000000000000000000..c2da11e540249d9fbfe8ed822c5b8cd99c51d483 --- /dev/null +++ b/src/Controller/DuplicateController.php @@ -0,0 +1,95 @@ +<?php + +namespace Drupal\mercury_editor\Controller; + +use Drupal\Core\Ajax\AfterCommand; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Ajax\AjaxHelperTrait; +use Drupal\Core\Controller\ControllerBase; +use Drupal\layout_paragraphs\LayoutParagraphsLayout; +use Drupal\mercury_editor\MercuryEditorContextService; +use Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\layout_paragraphs\Ajax\LayoutParagraphsEventCommand; +use Drupal\layout_paragraphs\LayoutParagraphsLayoutRefreshTrait; +use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository; + +/** + * Class DuplicateController. + * + * Duplicates a component of a Layout Paragraphs Layout. + * This is a copy of the DuplicateController class from the Layout Paragraphs + * module. + * + * @todo Consider refactoring this class to extend the original class. + */ +class DuplicateController extends ControllerBase { + + use LayoutParagraphsLayoutRefreshTrait; + use AjaxHelperTrait; + + /** + * {@inheritDoc} + */ + public function __construct( + protected LayoutParagraphsLayoutTempstoreRepository $tempstore, + protected IFrameAjaxResponseWrapper $iFrameAjaxResponseWrapper, + protected MercuryEditorContextService $mercuryEditorContext + ) {} + + /** + * {@inheritDoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('layout_paragraphs.tempstore_repository'), + $container->get('mercury_editor.iframe_ajax_response_wrapper'), + $container->get('mercury_editor.context') + ); + } + + /** + * Duplicates a component and returns appropriate response. + * + * @param \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout_paragraphs_layout + * The layout paragraphs layout object. + * @param string $source_uuid + * The source component to be cloned. + * + * @return array|\Drupal\Core\Ajax\AjaxResponse + * A build array or Ajax respone. + */ + public function duplicate(LayoutParagraphsLayout $layout_paragraphs_layout, string $source_uuid) { + $this->setLayoutParagraphsLayout($layout_paragraphs_layout); + $duplicate_component = $this->layoutParagraphsLayout->duplicateComponent($source_uuid); + $this->tempstore->set($this->layoutParagraphsLayout); + + if ($this->isAjax()) { + $response = new AjaxResponse(); + if ($this->needsRefresh()) { + $layout = $this->renderLayout(); + $dom_selector = '[data-lpb-id="' . $this->layoutParagraphsLayout->id() . '"]'; + $this->iFrameAjaxResponseWrapper->addCommand(new ReplaceCommand($dom_selector, $layout)); + $response->addCommand($this->iFrameAjaxResponseWrapper->getWrapperCommand()); + return $response; + } + $uuid = $duplicate_component->getEntity()->uuid(); + $rendered_item = [ + '#type' => 'layout_paragraphs_builder', + '#layout_paragraphs_layout' => $this->layoutParagraphsLayout, + '#uuid' => $uuid, + ]; + $this->iFrameAjaxResponseWrapper->addCommand(new AfterCommand('[data-uuid="' . $source_uuid . '"]', $rendered_item)); + $this->iFrameAjaxResponseWrapper->addCommand(new LayoutParagraphsEventCommand($this->layoutParagraphsLayout, $uuid, 'component:update')); + $response->addCommand($this->iFrameAjaxResponseWrapper->getWrapperCommand()); + return $response; + } + return [ + '#type' => 'layout_paragraphs_builder', + '#layout_paragraphs_layout' => $layout_paragraphs_layout, + ]; + + } + +} diff --git a/src/Controller/InsertComponentController.php b/src/Controller/InsertComponentController.php index 4b877046e1a1b4a6e978c970c8349e39ee820344..9b9442c1de4189e5daaee4c70f2279e1289ebe12 100644 --- a/src/Controller/InsertComponentController.php +++ b/src/Controller/InsertComponentController.php @@ -2,6 +2,7 @@ namespace Drupal\mercury_editor\Controller; +use Drupal\Core\Form\FormState; use Drupal\Core\Ajax\AfterCommand; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\AppendCommand; @@ -10,11 +11,10 @@ use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Ajax\OpenDialogCommand; use Drupal\Core\Ajax\CloseDialogCommand; use Drupal\mercury_editor\DialogService; -use Drupal\paragraphs\ParagraphInterface; use Drupal\layout_paragraphs\Utility\Dialog; use Symfony\Component\HttpFoundation\Request; -use Drupal\paragraphs\ParagraphsTypeInterface; use Drupal\layout_paragraphs\LayoutParagraphsLayout; +use Drupal\layout_paragraphs\Event\LayoutParagraphsComponentDefaultsEvent; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\layout_paragraphs\Ajax\LayoutParagraphsEventCommand; use Drupal\layout_paragraphs\Controller\ComponentFormController; @@ -45,7 +45,9 @@ class InsertComponentController extends ComponentFormController { /** * {@inheritDoc} */ - public function __construct(LayoutParagraphsLayoutTempstoreRepository $tempstore, DialogService $mercury_editor_dialog) { + public function __construct( + LayoutParagraphsLayoutTempstoreRepository $tempstore, + DialogService $mercury_editor_dialog) { $this->tempstore = $tempstore; $this->mercuryEditorDialog = $mercury_editor_dialog; } @@ -56,15 +58,23 @@ class InsertComponentController extends ComponentFormController { public static function create(ContainerInterface $container) { return new static( $container->get('layout_paragraphs.tempstore_repository'), - $container->get('mercury_editor.dialog') + $container->get('mercury_editor.dialog'), + $container->get('event_dispatcher') ); } /** * {@inheritDoc} */ - public function skipInsertForm(Request $request, LayoutParagraphsLayout $layout_paragraphs_layout, ParagraphsTypeInterface $paragraph_type) { + public function skipInsertForm(Request $request, LayoutParagraphsLayout $layout_paragraphs_layout, string $paragraph_type_id) { $skip_for_types = $this->config('mercury_editor.settings')->get('skip_create_form'); + $event = new LayoutParagraphsComponentDefaultsEvent($paragraph_type_id, []); + $this->eventDispatcher()->dispatch($event, $event::EVENT_NAME); + $paragraph_type = $this + ->entityTypeManager() + ->getStorage('paragraphs_type') + ->load($event->getParagraphTypeId()); + if (isset($skip_for_types[$paragraph_type->id()])) { $response = new AjaxResponse(); @@ -83,7 +93,27 @@ class InsertComponentController extends ComponentFormController { $paragraph = $this->entityTypeManager->getStorage('paragraph') ->create([$bundle_key => $paragraph_type->id()]); - $this->populateDefaultTextValues($paragraph); + $form_state = new FormState(); + $args = [ + $layout_paragraphs_layout, + $paragraph_type, + $parent_uuid, + $region, + $sibling_uuid, + $placement, + ]; + + $form_state + ->addBuildInfo('args', $args); + $form = $this->formBuilder() + ->buildForm('\Drupal\mercury_editor\Form\InsertComponentForm', $form_state); + + /** @var \Drupal\mercury_editor\Form\InsertComponentForm */ + $form_object = $form_state->getFormObject(); + $form_state->setUserInput([]); + $form_object->validateForm($form, $form_state); + $form_object->submitForm($form, $form_state); + $paragraph = $form_object->buildParagraphComponent($form, $form_state); if ($sibling_uuid && $placement) { switch ($placement) { @@ -133,65 +163,7 @@ class InsertComponentController extends ComponentFormController { $response->addCommand($iframe_ajax_response_wrapper->getWrapperCommand()); return $response; } - return $this->insertForm($request, $layout_paragraphs_layout, $paragraph_type); - } - - /** - * Populates a paragraph entity's text fields with their default values. - * - * For text fields without default values, this will insert an HTML comment - * placeholder to make sure the field is rendered so inline editing works. - * - * @param \Drupal\paragraphs\ParagraphInterface $paragraph - * The paragraph entity. - */ - protected function populateDefaultTextValues(ParagraphInterface &$paragraph) { - - $field_definitions = \Drupal::service('entity_field.manager') - ->getFieldDefinitions('paragraph', $paragraph->bundle()); - - // Build an array of default field values keyed by field name. - $field_defaults = array_map( - function ($def) use ($paragraph) { - return $def->getDefaultValue($paragraph); - }, - array_filter( - $field_definitions, - function ($def) use ($paragraph) { - return !empty($def->getDefaultValue($paragraph)); - } - )); - // Build an array of text field names. - $field_keys = array_keys( - array_filter( - $field_definitions, - function ($def) use ($paragraph) { - return (strpos($def->getType(), 'text') === 0) && empty($def->getDefaultValue($paragraph)); - } - )); - foreach ($field_defaults as $field_name => $default_value) { - $paragraph->set($field_name, $default_value); - } - } - - /** - * {@inheritDoc} - * - * Uses mercury editor form class instead of layout paragraphs form class. - */ - public function insertForm(Request $request, LayoutParagraphsLayout $layout_paragraphs_layout, ParagraphsTypeInterface | string $paragraph_type) { - - $parent_uuid = $request->query->get('parent_uuid'); - $region = $request->query->get('region'); - $sibling_uuid = $request->query->get('sibling_uuid'); - $placement = $request->query->get('placement'); - - if (is_string($paragraph_type)) { - $paragraph_type = $this->entityTypeManager()->getStorage('paragraphs_type')->load($paragraph_type); - } - - $form = $this->formBuilder()->getForm('\Drupal\mercury_editor\Form\InsertComponentForm', $layout_paragraphs_layout, $paragraph_type, $parent_uuid, $region, $sibling_uuid, $placement); - return $this->openForm($form, $layout_paragraphs_layout); + return $this->insertForm($request, $layout_paragraphs_layout, $paragraph_type->id()); } /** @@ -224,4 +196,11 @@ class InsertComponentController extends ComponentFormController { return $form; } + /** + * Returns the insert component form class. + */ + protected function getInsertComponentFormClass() { + return '\Drupal\mercury_editor\Form\InsertComponentForm'; + } + } diff --git a/src/Controller/MercuryEditorBlockContentController.php b/src/Controller/MercuryEditorBlockContentController.php new file mode 100644 index 0000000000000000000000000000000000000000..0b0488945ed08cdcc48c2af0fb22cf166e068a85 --- /dev/null +++ b/src/Controller/MercuryEditorBlockContentController.php @@ -0,0 +1,24 @@ +<?php + +namespace Drupal\mercury_editor\Controller; + +/** + * Provides a controller for editing block content in Mercury Editor. + */ +class MercuryEditorBlockContentController extends MercuryEditorController { + + /** + * Sets the Mercury Editor preview context to true. + * + * This controller returns an empty array and expects the actual block to be + * rendered by the block layout system. + * + * @return array + * An empty array. + */ + public function preview() { + $this->mercuryEditorContext->setPreview(TRUE); + return []; + } + +} diff --git a/src/Controller/MercuryEditorController.php b/src/Controller/MercuryEditorController.php index a7d6e6fbf455c837060b7d85cbcc9f576b19418e..bc7c7b08d3748234a6327bf38ba8352945d4ce96 100644 --- a/src/Controller/MercuryEditorController.php +++ b/src/Controller/MercuryEditorController.php @@ -123,7 +123,10 @@ class MercuryEditorController extends EntityViewController { */ public function preview() { $mercury_editor_entity = $this->mercuryEditorContext->getEntity(); - return $this->view($mercury_editor_entity); + $this->mercuryEditorContext->setPreview(TRUE); + $preview = $this->view($mercury_editor_entity); + $preview['#attached']['drupalSettings']['mercuryEditorId'] = $mercury_editor_entity->uuid(); + return $preview; } /** diff --git a/src/Entity/MercuryEditorBlockContentForm.php b/src/Entity/MercuryEditorBlockContentForm.php new file mode 100644 index 0000000000000000000000000000000000000000..2b73d65f3b7f3e28c0b185213920ef0c5875368a --- /dev/null +++ b/src/Entity/MercuryEditorBlockContentForm.php @@ -0,0 +1,218 @@ +<?php + +namespace Drupal\mercury_editor\Entity; + +use Drupal\Core\Render\Element; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Form\FormStateInterface; +use Drupal\block_content\BlockContentForm; +use Drupal\Core\Block\BlockPluginInterface; +use Drupal\Core\Block\TitleBlockPluginInterface; +use Drupal\Core\Plugin\ContextAwarePluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Defines the Mercury Editor block content form. + */ +class MercuryEditorBlockContentForm extends BlockContentForm { + + use MercuryEditorEntityFormTrait; + + /** + * The plugin.manager.block service. + * + * @var \Drupal\Component\Plugin\PluginManagerInterface + */ + protected $pluginManagerBlock; + + /** + * The context repository service. + * + * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface + */ + protected $contextRepository; + + /** + * The plugin context handler. + * + * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface + */ + protected $contextHandler; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $account; + + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + + /** + * The title resolver. + * + * @var \Drupal\Core\Controller\TitleResolverInterface + */ + protected $titleResolver; + + /** + * Injects dependencies from the container. + */ + public function injectDependencies(ContainerInterface $container) { + $this->tempstore = $container->get('mercury_editor.tempstore_repository'); + $this->layoutParagraphsTempstore = $container->get('layout_paragraphs.tempstore_repository'); + $this->iFrameAjaxResponseWrapper = $container->get('mercury_editor.iframe_ajax_response_wrapper'); + $this->entityTypeManager = $container->get('entity_type.manager'); + $this->pluginManagerBlock = $container->get('plugin.manager.block'); + $this->mercuryEditorContextService = $container->get('mercury_editor.context'); + $this->contextRepository = $container->get('context.repository'); + $this->contextHandler = $container->get('context.handler'); + $this->routeMatch = $container->get('current_route_match'); + $this->titleResolver = $container->get('title_resolver'); + $this->account = $container->get('current_user'); + } + + /** + * {@inheritDoc} + */ + public function setDefaultEntityValues() { + $this->entity->name = 'New block'; + } + + /** + * Ajax callback for rendering the form. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public function ajaxCallback(array &$form, FormStateInterface $form_state) { + $form['#updated'] = TRUE; + $response = new AjaxResponse(); + + if (empty($form_state->getErrors())) { + $selector = '[data-me-edit-screen-key="' . $this->entity->uuid() . '"]'; + $view = $this->buildBlock('block_content:' . $this->entity->uuid()); + $this->iFrameAjaxResponseWrapper->addCommand(new ReplaceCommand($selector, $view)); + $response->addCommand($this->iFrameAjaxResponseWrapper->getWrapperCommand()); + } + else { + $form['#attributes']['class'][] = 'unsaved-changes'; + } + $response->addCommand(new ReplaceCommand('.me-node-form', $form)); + + return $response; + } + + /** + * Returns a block render array. + * + * @param string $id + * The block id. + * @param array $configuration + * The block configuration. + * @param bool $wrapper + * Whether or not use block template for rendering. + * + * @return array + * The built block. + */ + protected function buildBlock(string $id, array $configuration = [], bool $wrapper = TRUE) { + + $is_preview = $this->mercuryEditorContextService->isPreview(); + $this->mercuryEditorContextService->setPreview(TRUE); + + $configuration += ['label_display' => BlockPluginInterface::BLOCK_LABEL_VISIBLE]; + /** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */ + $block_plugin = $this->pluginManagerBlock->createInstance($id, $configuration); + + // Inject runtime contexts. + if ($block_plugin instanceof ContextAwarePluginInterface) { + $contexts = $this->contextRepository->getRuntimeContexts($block_plugin->getContextMapping()); + $this->contextHandler->applyContextMapping($block_plugin, $contexts); + } + + $build = []; + $access = $block_plugin->access($this->account, TRUE); + if ($access->isAllowed()) { + // Title block needs a special treatment. + if ($block_plugin instanceof TitleBlockPluginInterface) { + // Account for the scenario that a NullRouteMatch is returned. This, for + // example, is the case when Search API is indexing the site during + // Drush cron. + if ($route = $this->routeMatch->getRouteObject()) { + $request = $this->requestStack->getCurrentRequest(); + $title = $this->titleResolver->getTitle($request, $route); + $block_plugin->setTitle($title); + } + } + + // Place the content returned by the block plugin into a 'content' child + // element, as a way to allow the plugin to have complete control of its + // properties and rendering (for instance, its own #theme) without + // conflicting with the properties used above. + $build['content'] = $block_plugin->build(); + + if ($block_plugin instanceof TitleBlockPluginInterface) { + $build['content']['#cache']['contexts'][] = 'url'; + } + // Some blocks return null instead of array when empty. + // @see https://www.drupal.org/project/drupal/issues/3212354 + if ($wrapper && is_array($build['content']) && !Element::isEmpty($build['content'])) { + $build += [ + '#theme' => 'block', + '#id' => $configuration['id'] ?? NULL, + '#attributes' => [], + '#contextual_links' => [], + '#configuration' => $block_plugin->getConfiguration(), + '#plugin_id' => $block_plugin->getPluginId(), + '#base_plugin_id' => $block_plugin->getBaseId(), + '#derivative_plugin_id' => $block_plugin->getDerivativeId(), + ]; + // Semantically, the content returned by the plugin is the block, and in + // particular, #attributes and #contextual_links is information about + // the *entire* block. Therefore, we must move these properties into the + // top-level element. + foreach (['#attributes', '#contextual_links'] as $property) { + if (isset($build['content'][$property])) { + $build[$property] = $build['content'][$property]; + unset($build['content'][$property]); + } + } + } + } + + CacheableMetadata::createFromRenderArray($build) + ->addCacheableDependency($access) + ->addCacheableDependency($block_plugin) + ->applyTo($build); + + if (!isset($build['#cache']['keys'])) { + $build['#cache']['keys'] = [ + 'mercury_editor', + $id, + '[configuration]=' . hash('sha256', serialize($configuration)), + '[wrapper]=' . (int) $wrapper, + ]; + } + + $this->mercuryEditorContextService->setPreview($is_preview); + return $build; + } + +} diff --git a/src/Entity/MercuryEditorEntityFormTrait.php b/src/Entity/MercuryEditorEntityFormTrait.php index d3a598915fb67fa252b00a31b638a8a7a46dcbef..69907b17d220775b85126652c0a6bcd0f3d9d799 100644 --- a/src/Entity/MercuryEditorEntityFormTrait.php +++ b/src/Entity/MercuryEditorEntityFormTrait.php @@ -17,6 +17,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ trait MercuryEditorEntityFormTrait { + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStackInterface + */ + protected $requestStack; + + /** * The mercury editor tray tempstore repository. * @@ -45,6 +53,13 @@ trait MercuryEditorEntityFormTrait { */ protected $entityTypeManager; + /** + * The Mercury Editor context service. + * + * @var \Drupal\mercury_editor\MercuryEditorContextService + */ + protected $mercuryEditorContextService; + /** * The fields to sync changes in UI. * @@ -69,6 +84,8 @@ trait MercuryEditorEntityFormTrait { $this->layoutParagraphsTempstore = $container->get('layout_paragraphs.tempstore_repository'); $this->iFrameAjaxResponseWrapper = $container->get('mercury_editor.iframe_ajax_response_wrapper'); $this->entityTypeManager = $container->get('entity_type.manager'); + $this->mercuryEditorContextService = $container->get('mercury_editor.context'); + $this->requestStack = $container->get('request_stack'); } /** @@ -153,6 +170,16 @@ trait MercuryEditorEntityFormTrait { // Set the Mercury Editor context on the layout. /** @var \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout */ $layout = $form[$field_name]['widget']['layout_paragraphs_builder']['#layout_paragraphs_layout']; + + // Set referring items so we can save the layout to the correct entity. + $items = $layout->getParagraphsReferenceField(); + foreach ($items as $key => $item) { + if (!empty($item->entity)) { + $items[$key]->entity->_referringItem = $items[$key]; + } + } + $layout->setParagraphsReferenceField($items); + $settings = $layout->getSettings(); $settings['mercury_editor_context'] = TRUE; $settings['is_translating'] = $form[$field_name]['widget']['layout_paragraphs_builder']['#is_translating'] ?? FALSE; @@ -165,15 +192,15 @@ trait MercuryEditorEntityFormTrait { } if (!$form_state->get('init')) { $form_state->set('init', TRUE); - $this->tempstore->set($this->entity); } + $this->tempstore->set($this->entity); return $form; } /** * {@inheritdoc} */ - protected function actions(array $form, FormStateInterface $form_state) { + protected function actions(array $form, FormStateInterface $form_state) : array { $element = parent::actions($form, $form_state); if (isset($element['delete'])) { @@ -325,7 +352,12 @@ trait MercuryEditorEntityFormTrait { */ public function processRedirectUrl(array $element, FormstateInterface $form_state) { $entity = $this->tempstore->get($this->entity->uuid()); - if ($entity->id()) { + $request = $this->requestStack->getCurrentRequest(); + + if ($request->get('destination')) { + $element['#value'] = $request->get('destination'); + } + elseif ($entity->id()) { if ($entity instanceof RevisionableInterface && ($entity->isDefaultRevision() || $entity->isLatestRevision())) { $element['#value'] = $entity->toUrl('canonical', ['absolute' => TRUE])->toString(); } @@ -336,6 +368,7 @@ trait MercuryEditorEntityFormTrait { else { $element['#value'] = Url::fromRoute('node.add_page', [], ['absolute' => TRUE])->toString(); } + return $element; } diff --git a/src/Event/BeforeCreateComponentEvent.php b/src/Event/BeforeCreateComponentEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..abb80e5c0c68c9cf43fb7badbdd217bdcbe99fc8 --- /dev/null +++ b/src/Event/BeforeCreateComponentEvent.php @@ -0,0 +1,74 @@ +<?php + +namespace Drupal\mercury_editor\Event; + +use Drupal\Component\EventDispatcher\Event; + +/** + * Event that is fired before a new component is created. + */ +class BeforeCreateComponentEvent extends Event { + + // This makes it easier for subscribers to reliably use our event name. + const EVENT_NAME = 'before_create_component'; + + /** + * Constructs the object. + * + * @param string $paragraphType + * The paragraph type. + * @param array $defaultValues + * The default values for the paragraph. + */ + public function __construct( + protected string $paragraphType, + protected array $defaultValues) { + } + + /** + * Sets the paragraph type. + * + * @param string $paragraphType + * The paragraph type. + * + * @return $this + */ + public function setParagraphType(string $paragraphType): self { + $this->paragraphType = $paragraphType; + return $this; + } + + /** + * Gets the paragraph type. + * + * @return string + * The paragraph type. + */ + public function getParagraphType(): string { + return $this->paragraphType; + } + + /** + * Sets the default values for the paragraph. + * + * @param array $defaultValues + * The default values for the paragraph. + * + * @return $this + */ + public function setDefaultValues(array $defaultValues): self { + $this->defaultValues = $defaultValues; + return $this; + } + + /** + * Gets the default values for the paragraph. + * + * @return array + * The default values for the paragraph. + */ + public function getDefaultValues(): array { + return $this->defaultValues; + } + +} diff --git a/src/Form/DeleteComponentForm.php b/src/Form/DeleteComponentForm.php index 7d5837bc4c163acb1977a56dff070ae9ecf6cf56..ff12df3cf84d56f07bc5d6f4a6605860970de196 100644 --- a/src/Form/DeleteComponentForm.php +++ b/src/Form/DeleteComponentForm.php @@ -8,25 +8,24 @@ use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Ajax\CloseDialogCommand; use Drupal\Core\Form\FormStateInterface; use Drupal\layout_paragraphs\Utility\Dialog; +use Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\layout_paragraphs\Ajax\LayoutParagraphsEventCommand; +use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository; use Drupal\layout_paragraphs\Form\DeleteComponentForm as LayoutParagraphsDeleteComponentForm; +/** + * Class for deleting a component in Mercury Editor. + */ class DeleteComponentForm extends LayoutParagraphsDeleteComponentForm { - /** - * Iframe Ajax Response Wrapper service. - * - * @var \Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper - */ - protected $iFrameAjaxResponseWrapper; - /** * {@inheritDoc} */ - protected function __construct($tempstore, $iframe_ajax_response_wrapper) { - $this->tempstore = $tempstore; - $this->iFrameAjaxResponseWrapper = $iframe_ajax_response_wrapper; + protected function __construct( + LayoutParagraphsLayoutTempstoreRepository $tempstore, + protected IFrameAjaxResponseWrapper $iFrameAjaxResponseWrapper) { + parent::__construct($tempstore); } /** @@ -35,7 +34,8 @@ class DeleteComponentForm extends LayoutParagraphsDeleteComponentForm { public static function create(ContainerInterface $container) { return new static( $container->get('layout_paragraphs.tempstore_repository'), - $container->get('mercury_editor.iframe_ajax_response_wrapper') + $container->get('mercury_editor.iframe_ajax_response_wrapper'), + $container->get('mercury_editor.context') ); } diff --git a/src/Form/EditComponentForm.php b/src/Form/EditComponentForm.php index 1192f9fd3f63278b7606b53440cbac04eb096e4b..01689bcc5d8dab6d618b703b10709f921a4b2c06 100644 --- a/src/Form/EditComponentForm.php +++ b/src/Form/EditComponentForm.php @@ -7,6 +7,7 @@ use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\mercury_editor\MercuryEditorTempstore; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Layout\LayoutPluginManagerInterface; use Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper; @@ -16,10 +17,11 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository; use Drupal\layout_paragraphs\Form\EditComponentForm as LayoutParagraphsEditComponentForm; +/** + * Class for editing a component in Mercury Editor. + */ class EditComponentForm extends LayoutParagraphsEditComponentForm { - protected $iFrameAjaxResponseWrapper; - /** * {@inheritDoc} */ @@ -30,10 +32,10 @@ class EditComponentForm extends LayoutParagraphsEditComponentForm { ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, EntityRepositoryInterface $entity_repository, - IFrameAjaxResponseWrapper $iframe_ajax_response_wrapper + protected IFrameAjaxResponseWrapper $iFrameAjaxResponseWrapper, + protected MercuryEditorTempstore $mercuryEditorTempstoreRepository ) { parent::__construct($tempstore, $entity_type_manager, $layout_plugin_manager, $module_handler, $event_dispatcher, $entity_repository); - $this->iFrameAjaxResponseWrapper = $iframe_ajax_response_wrapper; } /** @@ -47,7 +49,9 @@ class EditComponentForm extends LayoutParagraphsEditComponentForm { $container->get('module_handler'), $container->get('event_dispatcher'), $container->get('entity.repository'), - $container->get('mercury_editor.iframe_ajax_response_wrapper') + $container->get('mercury_editor.iframe_ajax_response_wrapper'), + $container->get('mercury_editor.tempstore_repository'), + $container->get('mercury_editor.context') ); } @@ -75,5 +79,4 @@ class EditComponentForm extends LayoutParagraphsEditComponentForm { return $response; } - } diff --git a/src/Form/InsertComponentForm.php b/src/Form/InsertComponentForm.php index fee014e4e18554da96763cddefc5f4f7c8efc411..224d63a92825f4739b1f92fe549038fba2dbab87 100644 --- a/src/Form/InsertComponentForm.php +++ b/src/Form/InsertComponentForm.php @@ -9,10 +9,12 @@ use Drupal\Core\Ajax\BeforeCommand; use Drupal\Core\Ajax\PrependCommand; use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Form\FormStateInterface; +use Drupal\paragraphs\ParagraphsTypeInterface; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Layout\LayoutPluginManagerInterface; +use Drupal\layout_paragraphs\LayoutParagraphsLayout; use Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\layout_paragraphs\Ajax\LayoutParagraphsEventCommand; @@ -20,9 +22,17 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository; use Drupal\layout_paragraphs\Form\InsertComponentForm as LayoutParagraphsInsertComponentForm; +/** + * Renders a form for inserting a new component in Mercury Editor. + */ class InsertComponentForm extends LayoutParagraphsInsertComponentForm { - protected $iFrameAjaxResponseWrapper; + /** + * Defaults for the paragraph. + * + * @var array + */ + protected $paragraphDefaults = []; /** * {@inheritDoc} @@ -34,10 +44,9 @@ class InsertComponentForm extends LayoutParagraphsInsertComponentForm { ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, EntityRepositoryInterface $entity_repository, - IFrameAjaxResponseWrapper $iframe_ajax_response_wrapper + protected IFrameAjaxResponseWrapper $iFrameAjaxResponseWrapper ) { parent::__construct($tempstore, $entity_type_manager, $layout_plugin_manager, $module_handler, $event_dispatcher, $entity_repository); - $this->iFrameAjaxResponseWrapper = $iframe_ajax_response_wrapper; } /** @@ -51,10 +60,28 @@ class InsertComponentForm extends LayoutParagraphsInsertComponentForm { $container->get('module_handler'), $container->get('event_dispatcher'), $container->get('entity.repository'), - $container->get('mercury_editor.iframe_ajax_response_wrapper') + $container->get('mercury_editor.iframe_ajax_response_wrapper'), + $container->get('mercury_editor.context') ); } + /** + * {@inheritDoc} + */ + public function buildForm( + array $form, + FormStateInterface $form_state, + LayoutParagraphsLayout $layout_paragraphs_layout = NULL, + ParagraphsTypeInterface $paragraph_type = NULL, + string $parent_uuid = NULL, + string $region = NULL, + string $sibling_uuid = NULL, + string $placement = NULL, + array $paragraph_defaults = [], + ) { + $this->paragraphDefaults = $paragraph_defaults; + return parent::buildForm($form, $form_state, $layout_paragraphs_layout, $paragraph_type, $parent_uuid, $region, $sibling_uuid, $placement); + } /** * {@inheritDoc} @@ -97,4 +124,23 @@ class InsertComponentForm extends LayoutParagraphsInsertComponentForm { return $response; } + /** + * {@inheritDoc} + */ + protected function newParagraph(ParagraphsTypeInterface $paragraph_type, string $langcode) { + $entity_type = $this->entityTypeManager->getDefinition('paragraph'); + $langcode_key = $entity_type->getKey('langcode'); + $bundle_key = $entity_type->getKey('bundle'); + $values = [ + $bundle_key => $paragraph_type->id(), + $langcode_key => $langcode, + '_layoutParagraphsLayout' => $this->getLayoutParagraphsLayout(), + ] + $this->paragraphDefaults; + /** @var \Drupal\paragraphs\ParagraphInterface $paragraph */ + $paragraph = $this->entityTypeManager->getStorage('paragraph') + ->create($values); + $behavior_settings = $paragraph->getAllBehaviorSettings(); + return $paragraph; + } + } diff --git a/src/MercuryEditorContextService.php b/src/MercuryEditorContextService.php index 0340bb03c84313200e09d1c4b21efa1a07e3583c..c925a6fed8beb6f7743b1d9dd82c107a96f45f71 100644 --- a/src/MercuryEditorContextService.php +++ b/src/MercuryEditorContextService.php @@ -4,6 +4,10 @@ namespace Drupal\mercury_editor; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Entity\ContentEntityInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Drupal\layout_paragraphs\LayoutParagraphsLayout; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; +use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository; /** * Provides a service for Mercury Editor context. @@ -11,21 +15,30 @@ use Drupal\Core\Entity\ContentEntityInterface; class MercuryEditorContextService { /** - * The route match. + * Whether the current route is a "Mercury Editor" preview. * - * @var \Drupal\Core\Routing\RouteMatchInterface + * @var bool */ - protected RouteMatchInterface $routeMatch; + protected $preview = FALSE; /** * MercuryEditorContextService constructor. * - * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch * The route match. + * @param \Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository $layoutParagraphsTempstore + * The layout paragraphs layout tempstore repository. + * @param \Drupal\mercury_editor\MercuryEditorTempstore $mercuryEditorTempstore + * The mercury editor tempstore. + * @param \Drupal\Core\Routing\RequestStack $requestStack + * The request. */ - public function __construct(RouteMatchInterface $route_match) { - $this->routeMatch = $route_match; - } + public function __construct( + protected RouteMatchInterface $routeMatch, + protected LayoutParagraphsLayoutTempstoreRepository $layoutParagraphsTempstore, + protected MercuryEditorTempstore $mercuryEditorTempstore, + protected RequestStack $requestStack, + ) {} /** * Determines if the current route is a "Mercury Editor" preview. @@ -38,11 +51,35 @@ class MercuryEditorContextService { * FALSE. */ public function isPreview(): bool { - return ($route_name = $this->routeMatch->getRouteName()) - && ( - $route_name === 'mercury_editor.preview' - || str_ends_with($route_name, '.mercury_editor_preview') - ); + $route_name = $this->routeMatch->getRouteName(); + if ($route_name == 'mercury_editor.preview') { + return TRUE; + } + if (str_ends_with($route_name, '.mercury_editor_preview')) { + return TRUE; + } + $layout_paragraphs_layout = $this->routeMatch->getParameter('layout_paragraphs_layout'); + if ($layout_paragraphs_layout) { + $entity = $layout_paragraphs_layout->getEntity(); + if (!empty($entity->lp_storage_keys)) { + return TRUE; + } + } + return $this->preview; + } + + /** + * Sets the preview state for the current Mercury Editor context. + * + * @param bool $preview + * The preview state, true if the current context is a preview. + * + * @return $this + * The current instance. + */ + public function setPreview(bool $preview = TRUE) { + $this->preview = $preview; + return $this; } /** @@ -55,8 +92,8 @@ class MercuryEditorContextService { * returns false. * * @return bool - * Returns TRUE if the current route is for a "Mercury Editor" editor; FALSE - * otherwise. + * Returns TRUE if the current route is for a "Mercury Editor" editor; FALSE + * otherwise. */ public function isEditor(): bool { $route_name = $this->routeMatch->getRouteName(); @@ -70,16 +107,90 @@ class MercuryEditorContextService { * The mercury editor entity. */ public function getEntity(): ?ContentEntityInterface { + if ($this->requestStack->getCurrentRequest()->query->has('me_id')) { + $entity = $this->mercuryEditorTempstore->get($this->requestStack->getCurrentRequest()->query->get('me_id')); + return $entity; + } if ($this->isEditor()) { return $this->routeMatch->getParameter('mercury_editor_entity'); } + if ($this->isPreview()) { + $route_name = $this->routeMatch->getRouteName(); + if ($route_name === 'mercury_editor.preview') { + return $this->routeMatch->getParameter('entity'); + } + if (str_ends_with($route_name, '.mercury_editor_preview')) { + return $this->routeMatch->getParameter(explode('.', $route_name)[1]); + } + } + return NULL; + } + + /** + * Sets the mercury editor entity in the tempstore. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity. + */ + public function setEntity(ContentEntityInterface $entity) { + $this->mercuryEditorTempstore->set($entity); + } + + /** + * Saves a layout to the parent Mercury Editor Entity in the tempstore. + * + * @param \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout + * The layout paragraphs layout object. + */ + public function saveLayout(LayoutParagraphsLayout $layout) { + + $entity = $layout->getEntity(); + $item_list = $layout->getParagraphsReferenceField(); - if ($this->isPreview() && $route_name = $this->routeMatch->getRouteName()) { - $entity_type = explode('.', $route_name)[1]; - return $this->routeMatch->getParameter($entity_type); + while ($entity->_referringItem) { + $field_name = $item_list->getName(); + $entity->$field_name = $item_list; + $item_list = $entity->_referringItem->getParent(); + $parent_entity = $item_list->getEntity(); + if ($mercury_editor_entity = $this->mercuryEditorTempstore->get($parent_entity->uuid())) { + $storage_key = $mercury_editor_entity->lp_storage_keys[$field_name]; + $layout = \Drupal::service('layout_paragraphs.tempstore_repository')->getWithStorageKey($storage_key); + $layout->setParagraphsReferenceField($item_list); + $mercury_editor_entity->$field_name = $item_list; + $this->layoutParagraphsTempstore->set($layout); + $this->mercuryEditorTempstore->set($mercury_editor_entity); + $parent_entity = $mercury_editor_entity; + } + $entity = $parent_entity; } + } - return NULL; + /** + * Recursively finds the child entity and saves it. + * + * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $reference_field + * The paragraph entity. + * @param string $uuid + * The uuid of the entity. + */ + protected function recursivelyFindChild(EntityReferenceFieldItemListInterface &$reference_field, string $uuid) { + + foreach ($reference_field as &$item) { + if ($item->entity->uuid() == $uuid) { + return $item->entity; + } + $definitions = array_filter( + $item->entity->getFieldDefinitions(), + function ($defintion) { + return $defintion->getType() == 'entity_reference_revisions'; + } + ); + foreach ($definitions as $definition) { + $field_name = $definition->getName(); + $reference_field =& $item->entity->$field_name; + return $this->recursivelyFindChild($reference_field, $uuid); + } + } } } diff --git a/src/Plugin/Field/FieldFormatter/LayoutParagraphsBuilder.php b/src/Plugin/Field/FieldFormatter/LayoutParagraphsBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..6c63bf6f67c8bfbf54c0b22f079606746db569ba --- /dev/null +++ b/src/Plugin/Field/FieldFormatter/LayoutParagraphsBuilder.php @@ -0,0 +1,112 @@ +<?php + +namespace Drupal\mercury_editor\Plugin\Field\FieldFormatter; + +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\TypedData\TranslatableInterface; +use Drupal\layout_paragraphs\LayoutParagraphsLayout; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\layout_paragraphs\LayoutParagraphsComponent; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\layout_paragraphs\Access\LayoutParagraphsBuilderAccess; +use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository; +use Drupal\layout_paragraphs\Plugin\Field\FieldFormatter\LayoutParagraphsFormatter; +use Drupal\mercury_editor\MercuryEditorContextService; + +/** + * Layout Paragraphs Builder field formatter. + * + * @FieldFormatter( + * id = "mercury_editor_layout_paragraphs_builder", + * label = @Translation("Mercury Editor Layout Paragraphs Builder"), + * description = @Translation("Renders paragraphs with layout."), + * field_types = { + * "entity_reference_revisions" + * } + * ) + */ +class LayoutParagraphsBuilder extends LayoutParagraphsFormatter implements ContainerFactoryPluginInterface { + + /** + * {@inheritDoc} + */ + public function __construct( + $plugin_id, + $plugin_definition, + FieldDefinitionInterface $field_definition, + array $settings, + $label, + $view_mode, + array $third_party_settings, + LoggerChannelFactoryInterface $logger_factory, + EntityDisplayRepositoryInterface $entity_display_repository, + protected LayoutParagraphsBuilderAccess $layoutParagraphsBuilderAccess, + protected LayoutParagraphsLayoutTempstoreRepository $tempstore, + protected AccountProxyInterface $account, + protected MercuryEditorContextService $mercuryEditorContextService) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $logger_factory, $entity_display_repository); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('logger.factory'), + $container->get('entity_display.repository'), + $container->get('layout_paragraphs.builder_access'), + $container->get('layout_paragraphs.tempstore_repository'), + $container->get('current_user'), + $container->get('mercury_editor.context') + ); + } + + /** + * {@inheritDoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode = NULL) { + $elements = parent::viewElements($items, $langcode); + if (!$this->mercuryEditorContextService->isPreview() && !$this->mercuryEditorContextService->isEditor()) { + return $elements; + } + foreach ($items as $key => $item) { + if (!empty($item->entity)) { + $items[$key]->entity->_referringItem = $items[$key]; + } + } + $mercury_editor_entity = $this->mercuryEditorContextService->getEntity(); + $settings = $this->getSettings() + [ + 'reference_field_view_mode' => $this->viewMode, + 'mercury_editor_context' => TRUE, + 'mercury_editor_uuid' => $mercury_editor_entity->uuid(), + ]; + $layout = new LayoutParagraphsLayout($items, $settings); + if (!$this->layoutParagraphsBuilderAccess->access($this->account, $layout)->isAllowed()) { + return $elements; + } + $this->tempstore->set($layout); + $build = [ + '#type' => 'layout_paragraphs_builder', + '#layout_paragraphs_layout' => $layout, + ]; + return [ + [ + 'builder' => $build, + ], + ]; + } + +} diff --git a/src/Routing/MercuryEditorPreviewRoutes.php b/src/Routing/MercuryEditorPreviewRoutes.php index 6be6303eebaba023a8b84296b89d26b255cf0527..4284f01c92e3e1a8ca0300423d0e14c0bbea8ccb 100644 --- a/src/Routing/MercuryEditorPreviewRoutes.php +++ b/src/Routing/MercuryEditorPreviewRoutes.php @@ -36,10 +36,18 @@ class MercuryEditorPreviewRoutes { if (!$definition->getFormClass('mercury_editor')) { continue; } + if ($entity_type == 'block_content') { + $route = '/block-content/{block_content}/mercury-editor-preview'; + $controller = '\Drupal\mercury_editor\Controller\MercuryEditorBlockContentController::preview'; + } + else { + $route = $definition->getLinkTemplate('canonical') . '/mercury-editor-preview'; + $controller = '\Drupal\mercury_editor\Controller\MercuryEditorController::preview'; + } $routes['entity.' . $entity_type . '.mercury_editor_preview'] = new Route( - $definition->getLinkTemplate('canonical') . '/mercury-editor-preview', + $route, [ - '_controller' => '\Drupal\mercury_editor\Controller\MercuryEditorController::preview', + '_controller' => $controller, ], [ '_custom_access' => '\Drupal\mercury_editor\Controller\MercuryEditorController::access', @@ -51,7 +59,6 @@ class MercuryEditorPreviewRoutes { 'mercury_editor_entity' => TRUE, ], ], - '_admin_route' => FALSE, '_hide_admin_toolbar' => 'TRUE', 'no_cache' => 'TRUE', ] diff --git a/templates/layout-paragraphs-builder-component-menu--mercury-editor.html.twig b/templates/layout-paragraphs-builder-component-menu--mercury-editor.html.twig index 65b08907c29b1f869cebc55f55b9b8bb88b6c3b3..d3955ab2dbdf7d34a0146ef4c37c8698beab066c 100644 --- a/templates/layout-paragraphs-builder-component-menu--mercury-editor.html.twig +++ b/templates/layout-paragraphs-builder-component-menu--mercury-editor.html.twig @@ -2,32 +2,11 @@ {{ status_messages }} <div{{attributes}}> <h4 class="visually-hidden">{{ 'Add Item'|t }}</h4> - {% if all_types|length > 0 %} + {% if count > 0 %} <div class="lpb-component-list__search"> <input class="lpb-component-list-search-input" type="text" placeholder="Filter items..."/> </div> <div class="lpb-component-list__group"> - {% if types.layout %} - <div class="lpb-component-list__group--layout"> - <h3 class="lpb-component-list__group-label">{{'Layout'|t}}</h3> - {% endif %} - {% for type in types.layout %} - <div class="lpb-component-list__item type-{{type.id}} is-layout"> - <a{{type.link_attributes.setAttribute('href',type.url)}}> - {% if type.image %} - <style> - .lpb-component-list__item.type-{{type.id}} a::before { - background: url({{ type.image }}); - background-size: cover; - } - </style> - {% endif %} - {{ type.label }}</a> - </div> - {% endfor %} - {% if types.layout %} - </div> - {% endif %} {% for group in groups %} {% if group.items|length > 0 %} <div class="lpb-component-list__group--content"> diff --git a/templates/page--mercury-editor.html.twig b/templates/page--mercury-editor.html.twig index 18f12a6ba455ea78f747ae4038c52c11ed35470c..205e56d6cb13c9d021513e2d23ff517c3d6b28e7 100644 --- a/templates/page--mercury-editor.html.twig +++ b/templates/page--mercury-editor.html.twig @@ -57,8 +57,7 @@ </div> </div> <div id="me-iframe-wrapper"> -<iframe id="me-preview" width="100%" height="100%" style="border:none;" src="{{ preview_url }}"> - + <iframe id="me-preview" width="100%" height="100%" style="border:none;" data-src="{{ preview_url }}"> </iframe> </div> <mercury-dialog id="me-edit-screen" hide-close-button push resizable dock="right"> diff --git a/tests/cypress/cypress/e2e/mercury-editor/mercury-editor.cy.js b/tests/cypress/cypress/e2e/mercury-editor/mercury-editor.cy.js index 58e634ca2ff4b3d1a6ccde0b0cd6103b1e7299dc..270e7c880373756f85e4e0d9a88e734b49f3bdba 100644 --- a/tests/cypress/cypress/e2e/mercury-editor/mercury-editor.cy.js +++ b/tests/cypress/cypress/e2e/mercury-editor/mercury-editor.cy.js @@ -142,4 +142,24 @@ describe('Mercury Editor e2e tests.', () => { cy.drush('pmu mercury_editor_block_visibility_test') }); + it('tests field validation with Mercury Editor component', () => { + // Install the test module. + cy.drush('en mercury_editor_field_validation_test'); + // Creates a new 2-column section and attempts to save it without enterring a value for the "label" field. + cy.visit('/node/add/me_test_ct'); + cy.meAddComponent('me_test_section'); + cy.meChooseLayout('layout_twocol'); + // Save the section without entering a value for the "label" field, wich + // will trigger a validation error. + cy.meSaveComponent().then((invalid_field) => { + // The "invalid_field" form field should be visible and have an error class. + cy.get(invalid_field) + .should('be.visible') + .and('have.class', 'error'); + }); + // Uninstall the test module. + cy.drush('cr'); + cy.drush('pmu mercury_editor_field_validation_test'); + }); + }); diff --git a/tests/cypress/package-lock.json b/tests/cypress/package-lock.json index 7e652bf0025b069c60a44fbd44979cb63c221cc4..93c763fce952466ae3e69100d4d053ff9d8364ef 100644 --- a/tests/cypress/package-lock.json +++ b/tests/cypress/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "cypress-mercury-editor": "^0.0.29" + "cypress-mercury-editor": "^0.0.32" }, "devDependencies": { "cypress": "^13.6.0", @@ -578,9 +578,9 @@ } }, "node_modules/cypress-mercury-editor": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/cypress-mercury-editor/-/cypress-mercury-editor-0.0.29.tgz", - "integrity": "sha512-5cBi/d0Y+6YGIl6sfYR+Qxx0YDBs0/4wcdGJJsFBvifjeW+JntwbFC+i4qdORZVUZ1ysuD+5T5qT90hyeiC5Fw==", + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/cypress-mercury-editor/-/cypress-mercury-editor-0.0.32.tgz", + "integrity": "sha512-d61djmmsg8pVZS08TAR2KaGzTZA8DLtSsuwYLJpYJYp91XB2HIHNgqXE8UF822aCa9zr+ccOTt3UEtEft4zj6g==", "dependencies": { "cypress-iframe": "^1.0.1" } diff --git a/tests/cypress/package.json b/tests/cypress/package.json index cdbf2f23c08c9460327f0305fc2bd4ac2505f276..70ebee94e61e5ea6a5b994de13fcc10078d437de 100644 --- a/tests/cypress/package.json +++ b/tests/cypress/package.json @@ -15,6 +15,6 @@ "cypress-iframe": "^1.0.1" }, "dependencies": { - "cypress-mercury-editor": "^0.0.29" + "cypress-mercury-editor": "^0.0.32" } } diff --git a/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.info.yml b/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..50247677945f6f7c035dc6a319f20ce17d414b78 --- /dev/null +++ b/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.info.yml @@ -0,0 +1,7 @@ +name: Mercury Editor Validation Test +description: 'Setup for Mercury Editor validation tests.' +package: 'Testing' +type: module +core_version_requirement: ^10 || ^11 +dependencies: + - mercury_editor:mercury_editor diff --git a/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.install b/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.install new file mode 100644 index 0000000000000000000000000000000000000000..1e4b4aea2f0a4e0294c4d442fd0cba1029a40467 --- /dev/null +++ b/tests/modules/mercury_editor_field_validation_test/mercury_editor_field_validation_test.install @@ -0,0 +1,56 @@ +<?php + +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; + +function mercury_editor_field_validation_test_install() { + + $field_storage = FieldStorageConfig::loadByName('paragraph', 'field_me_test_label'); + if (!$field_storage) { + // Add a paragraphs field. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_me_test_label', + 'entity_type' => 'paragraph', + 'type' => 'text', + 'cardinality' => '1', + 'required' => TRUE, + ]); + $field_storage->save(); + } + $field_config = FieldConfig::loadByName('paragraph', 'me_test_section', 'field_me_test_label'); + if (!$field_config) { + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'me_test_section', + 'label' => 'Label (Validation Test)', + 'required' => TRUE, + ]); + $field->save(); + } + + $form_display = \Drupal::service('entity_display.repository')->getFormDisplay('paragraph', 'me_test_section'); + $form_display->setComponent('field_me_test_label', ['type' => 'text_textfield']); + $form_display->save(); + + $view_display = \Drupal::service('entity_display.repository')->getViewDisplay('paragraph', 'me_test_section'); + $view_display->setComponent('field_me_test_label', ['type' => 'text_default', 'label' => 'hidden']); + $view_display->save(); + +} + +/** + * Implements hook_uninstall(). + * + * Delete the field config and field storage. + */ +function mercury_editor_field_validation_test_uninstall() { + $field_config = FieldConfig::loadByName('paragraph', 'me_test_section', 'field_me_test_label'); + if ($field_config) { + $field_config->delete(); + } + $field_storage = FieldStorageConfig::loadByName('paragraph', 'field_me_test_label'); + if ($field_storage) { + $field_storage->delete(); + } +} +