From f1a67cba613f5c5980118e59b9e53f1c6280f8bf Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Fri, 24 Jul 2026 11:23:34 -0500 Subject: [PATCH 01/15] Port Craft.NestedElementManager to a modern module Moves the legacy jQuery NestedElementManager into resources/js/modules/nested-element-manager/ following the shared module pattern: a logic class on @craftcms/garnish Base (modern Select/DragSort for card selection and drag-sorting, documented jQuery seams for the Craft.ui buttons, disclosure/expandable menus, element index, editor slideouts, and Craft.cp), a ControllerElement that boots from an element-type attribute and a JSON settings attribute, a WeakMap instance registry, and an index shim that assigns window.Craft.NestedElementManager (the constructor still accepts the legacy three-argument signature for plugin boots). NestedElementManager::createView() now emits the custom element around its rendered markup instead of an HtmlStack JS boot, so the manager self-boots and re-boots across Inertia fragment swaps. destroy() now actually tears down listeners and the Select/DragSort instances. Co-Authored-By: Claude Fable 5 --- packages/craftcms-legacy/cp/src/Craft.js | 1 - .../cp/src/js/NestedElementManager.js | 1220 -------------- resources/js/cp.ts | 1 + resources/js/legacy.ts | 1 + .../modules/nested-element-manager/README.md | 62 + .../modules/nested-element-manager/index.ts | 26 + .../nested-element-manager.ce.ts | 50 + .../nested-element-manager.ts | 1419 +++++++++++++++++ .../modules/nested-element-manager/support.ts | 14 + src/Element/NestedElementManager.php | 19 +- 10 files changed, 1580 insertions(+), 1233 deletions(-) delete mode 100644 packages/craftcms-legacy/cp/src/js/NestedElementManager.js create mode 100644 resources/js/modules/nested-element-manager/README.md create mode 100644 resources/js/modules/nested-element-manager/index.ts create mode 100644 resources/js/modules/nested-element-manager/nested-element-manager.ce.ts create mode 100644 resources/js/modules/nested-element-manager/nested-element-manager.ts create mode 100644 resources/js/modules/nested-element-manager/support.ts diff --git a/packages/craftcms-legacy/cp/src/Craft.js b/packages/craftcms-legacy/cp/src/Craft.js index 622c9f7b839..cfc2f150510 100644 --- a/packages/craftcms-legacy/cp/src/Craft.js +++ b/packages/craftcms-legacy/cp/src/Craft.js @@ -73,7 +73,6 @@ import './js/LightSwitch.js'; import './js/LinkField.js'; import './js/LinkInput.js'; import './js/LivePreview.js'; -import './js/NestedElementManager.js'; import './js/PasswordInput.js'; import './js/Preview.js'; import './js/PreviewFileModal.js'; diff --git a/packages/craftcms-legacy/cp/src/js/NestedElementManager.js b/packages/craftcms-legacy/cp/src/js/NestedElementManager.js deleted file mode 100644 index c7e63ff0400..00000000000 --- a/packages/craftcms-legacy/cp/src/js/NestedElementManager.js +++ /dev/null @@ -1,1220 +0,0 @@ -/** global: Craft */ - -/** - * Nested element manager - */ -Craft.NestedElementManager = Garnish.Base.extend( - { - $container: null, - $btnContainer: null, - elementType: null, - $createBtn: null, - $pasteBtn: null, - - // cards - $elements: null, - elementSort: null, - elementSelect: null, - - // index - elementIndex: null, - - /** - * @type {Craft.ElementEditor|null} - */ - elementEditor: null, - creatingElement: false, - - init: function (container, elementType, settings) { - this.$container = $(container); - this.elementType = elementType; - this.setSettings(settings, Craft.NestedElementManager.defaults); - - // Is this already a nested element manager? - if (this.$container.data('nestedElementManager')) { - console.warn( - 'Double-instantiating a nested element manager on an element' - ); - this.$container.data('nestedElementManager').destroy(); - } - - this.$container.data('nestedElementManager', this); - - if (this.settings.mode === 'cards') { - if (this.$container.is(':has(.elements)')) { - this.initCards(); - } - } else { - this.initElementIndex(); - } - - if (this.settings.canCreate) { - let $createBtn = Craft.ui - .createButton({ - icon: 'plus', - label: this.settings.createButtonLabel, - spinner: true, - }) - .addClass('icon disabled'); - - if (this.settings.mode === 'cards') { - $createBtn.addClass('dashed wrap'); - } - - this.addButton($createBtn); - - if (Array.isArray(this.settings.createAttributes)) { - const createMenuId = `menu-${Math.floor(Math.random() * 1000000)}`; - $('
', { - id: createMenuId, - class: 'menu menu--disclosure', - 'data-with-search-input': - this.settings.createAttributes.length > 5 ? 'true' : null, - }).insertAfter($createBtn); - $createBtn - .attr('aria-controls', createMenuId) - .attr('data-disclosure-trigger', 'true') - .addClass('menubtn') - .disclosureMenu(); - const disclosureMenu = $createBtn.data('disclosureMenu'); - - // can't use Object.groupBy() here because the group order matters - const groupedCreateAttributes = {}; - const groupOrder = []; - this.settings.createAttributes.forEach((attributes) => { - const group = attributes.group || Craft.t('app', 'General'); - if (!groupedCreateAttributes[group]) { - groupedCreateAttributes[group] = []; - groupOrder.push(group); - } - groupedCreateAttributes[group].push(attributes); - }); - const multiGroup = groupOrder.length > 1; - - groupOrder.forEach((group) => { - if (multiGroup) { - disclosureMenu.addHr(); - disclosureMenu.addGroup(group, false); - } - - groupedCreateAttributes[group].forEach((attributes) => { - disclosureMenu.addItem({ - icon: attributes.icon ? $(attributes.icon)[0] : null, - label: attributes.label, - iconColor: attributes.color, - onActivate: async () => { - $createBtn.addClass('loading'); - await this.createElement(attributes.attributes); - $createBtn.removeClass('loading'); - }, - }); - }); - }); - - if (multiGroup && this.settings.mode === 'cards') { - const $collapsedContainer = $( - '
' - ).insertAfter($createBtn); - $collapsedContainer.append($createBtn); - const $expandedContainer = $( - '