From 25ed18a24c6acb3a6370ad2a933e2f0234c59b82 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Mon, 2 Nov 2026 16:30:43 -0600 Subject: [PATCH 01/41] Load the adapter-only jQuery modules only with the adapter Core never runs the CP screen slideout, element action trigger, createElementEditor(), data and element table sorters, sortable checkbox select or tag select input; only the adapter and its legacy scripts do. Core's entry points stop loading and exporting them, and their Craft.* globals move to compat.ts, so they can keep jQuery. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG-WIP.md | 1 + resources/js/common/slideouts/index.ts | 6 +----- resources/js/cp.ts | 3 --- resources/js/legacy.ts | 3 --- .../js/modules/data-table-sorter/index.ts | 3 --- .../{globals.ts => compat.ts} | 0 .../js/modules/element-action-trigger/index.ts | 4 ---- .../js/modules/element-select-input/index.ts | 1 - .../js/modules/element-table-sorter/index.ts | 3 --- resources/js/modules/slideout/compat.ts | 14 ++++++++++++++ resources/js/modules/slideout/globals.ts | 11 ----------- resources/js/modules/slideout/index.ts | 18 +----------------- .../modules/sortable-checkbox-select/index.ts | 2 -- yii2-adapter/resources/js/cp-compat.ts | 1 + 14 files changed, 18 insertions(+), 52 deletions(-) delete mode 100644 resources/js/modules/data-table-sorter/index.ts rename resources/js/modules/element-action-trigger/{globals.ts => compat.ts} (100%) delete mode 100644 resources/js/modules/element-action-trigger/index.ts delete mode 100644 resources/js/modules/element-table-sorter/index.ts diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index acc6f6cc9c2..f4c14a4b428 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -811,6 +811,7 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/ - The legacy element index JavaScript is now provided by the Yii2 adapter: `Craft.createElementIndex()`, `Craft.registerElementIndexClass()`, `Craft.BaseElementIndex`, `Craft.BaseElementIndexView` and its subclasses, `Craft.AssetIndex`, `Craft.CategoryIndex`, `Craft.EntryIndex`, `Craft.UserIndex`, `Craft.Structure`, `Craft.StructureDrag`, `Craft.ProgressBar`, `Craft.ElementTableSorter`, `Craft.IntervalManager`, `Craft.PromptHandler`, and `Craft.openCustomizeSourcesModal()`. - `Craft.AdminTable` and `Craft.DataTableSorter` are now provided by the Yii2 adapter. - `Craft.ui` and `Craft.EditableTable` are now provided by the Yii2 adapter. +- `Craft.CpScreenSlideout`, `Craft.ElementActionTrigger`, and `Craft.createElementEditor()` are now provided by the Yii2 adapter. - `Craft.ElementEditor` and `Craft.ElementEditorSlideout` are now provided by the Yii2 adapter. Core’s element editors, slideouts included, all use the Vue editor. - `Craft.CP` and `Craft.cp` are now provided by the Yii2 adapter. Control panel messages, the element clipboard and screen reader announcements no longer use them. `showNotice()`, `showSuccess()`, `showError()`, `copyElements()` and `announce()` are exported from `@craftcms/cp`. - The legacy `Craft` helpers (`Craft.t()`, `Craft.sendActionRequest()`, `Craft.getUrl()`, `Craft.initUiElements()` and the rest of `Craft.js`), its jQuery plugins (`$.fn.disclosureMenu()`, `$.fn.menubtn()`, `$.fn.formsubmit()`…) and `Craft.queue` are now provided by the Yii2 adapter. Core uses `@craftcms/cp` and `@craftcms/ui` modules instead. diff --git a/resources/js/common/slideouts/index.ts b/resources/js/common/slideouts/index.ts index f1add24e1a0..e1126432613 100644 --- a/resources/js/common/slideouts/index.ts +++ b/resources/js/common/slideouts/index.ts @@ -1,4 +1,3 @@ -import {createElementEditor} from './create-element-editor'; import {closeAllSlideouts, closeSlideout, openSlideout} from './store'; export {default as SlideoutHost} from './SlideoutHost.vue'; @@ -19,9 +18,7 @@ export type { /** * Expose the opener on `window.Craft` so legacy bundles — and anyone poking at - * the console — can open a Vue slideout without importing anything. This also - * replaces the legacy bundle's `Craft.createElementEditor()`, so element - * editors open in a Vue slideout too. + * the console — can open a Vue slideout without importing anything. */ export function registerSlideoutGlobals(): void { const craft = window.Craft; @@ -34,6 +31,5 @@ export function registerSlideoutGlobals(): void { openSlideout, closeSlideout, closeAllSlideouts, - createElementEditor, }); } diff --git a/resources/js/cp.ts b/resources/js/cp.ts index fd52ba8e8c0..1cc1d41da69 100644 --- a/resources/js/cp.ts +++ b/resources/js/cp.ts @@ -47,10 +47,7 @@ import './modules/cp-modal'; import './modules/tabs'; import './modules/grid'; import './modules/chart'; -import './modules/data-table-sorter'; -import './modules/element-action-trigger'; import './modules/element-thumb-loader'; -import './modules/element-table-sorter'; import './modules/asset-mover'; import './modules/element-selector-modal'; import './modules/element-select-input'; diff --git a/resources/js/legacy.ts b/resources/js/legacy.ts index 659f28f4b93..4cccc39507f 100644 --- a/resources/js/legacy.ts +++ b/resources/js/legacy.ts @@ -61,10 +61,7 @@ import './modules/cp-modal/index'; import './modules/tabs/index'; import './modules/grid/index'; import './modules/chart/index'; -import './modules/data-table-sorter/index'; -import './modules/element-action-trigger/index'; import './modules/element-thumb-loader/index'; -import './modules/element-table-sorter/index'; import './modules/asset-mover/index'; import './modules/element-selector-modal/index'; import './modules/element-select-input/index'; diff --git a/resources/js/modules/data-table-sorter/index.ts b/resources/js/modules/data-table-sorter/index.ts deleted file mode 100644 index 0f3c2a68ce6..00000000000 --- a/resources/js/modules/data-table-sorter/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {DataTableSorter} from './data-table-sorter'; - -export {DataTableSorter}; diff --git a/resources/js/modules/element-action-trigger/globals.ts b/resources/js/modules/element-action-trigger/compat.ts similarity index 100% rename from resources/js/modules/element-action-trigger/globals.ts rename to resources/js/modules/element-action-trigger/compat.ts diff --git a/resources/js/modules/element-action-trigger/index.ts b/resources/js/modules/element-action-trigger/index.ts deleted file mode 100644 index 4fa67dfee1e..00000000000 --- a/resources/js/modules/element-action-trigger/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './globals'; -import {ElementActionTrigger} from './element-action-trigger'; - -export {ElementActionTrigger}; diff --git a/resources/js/modules/element-select-input/index.ts b/resources/js/modules/element-select-input/index.ts index cba35d7cad0..be18a249bc2 100644 --- a/resources/js/modules/element-select-input/index.ts +++ b/resources/js/modules/element-select-input/index.ts @@ -12,6 +12,5 @@ defineElement('craft-entry-select-input', CraftEntrySelectInput); export {BaseElementSelectInput} from './base-element-select-input'; export {EntrySelectInput} from './entry-select-input'; -export {TagSelectInput} from './tag-select-input'; export {CraftAssetSelectInput, CraftElementSelectInput, CraftEntrySelectInput}; export {elementSelectInputData} from './support'; diff --git a/resources/js/modules/element-table-sorter/index.ts b/resources/js/modules/element-table-sorter/index.ts deleted file mode 100644 index d04b6713baf..00000000000 --- a/resources/js/modules/element-table-sorter/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {ElementTableSorter} from './element-table-sorter'; - -export {ElementTableSorter}; diff --git a/resources/js/modules/slideout/compat.ts b/resources/js/modules/slideout/compat.ts index a82f672139c..2a78f7e8962 100644 --- a/resources/js/modules/slideout/compat.ts +++ b/resources/js/modules/slideout/compat.ts @@ -1,7 +1,18 @@ import {compatify} from '@craftcms/garnish/compat'; +import {createElementEditor} from '@/common/slideouts/create-element-editor'; +import {CpScreenSlideout} from './cp-screen-slideout'; import {ElementEditorSlideout} from './element-editor-slideout'; import {registerCraftGlobals} from '@/common/craft-global'; +// Plugins subclass CP screen slideouts via the legacy `.extend()` API. +const CompatCpScreenSlideout = compatify(CpScreenSlideout); + +Object.assign(CompatCpScreenSlideout, { + defaults: CpScreenSlideout.defaults, +}); + +registerCraftGlobals({CpScreenSlideout: CompatCpScreenSlideout}); + // It runs the legacy `Craft.ElementEditor`, which only the adapter loads. // Compatified so plugin subclasses keep working. const CompatElementEditorSlideout = compatify(ElementEditorSlideout); @@ -11,3 +22,6 @@ Object.assign(CompatElementEditorSlideout, { }); registerCraftGlobals({ElementEditorSlideout: CompatElementEditorSlideout}); + +// The legacy element indexes open element editors through it. +registerCraftGlobals({createElementEditor}); diff --git a/resources/js/modules/slideout/globals.ts b/resources/js/modules/slideout/globals.ts index 89b33ac2a62..965ffe1e4a1 100644 --- a/resources/js/modules/slideout/globals.ts +++ b/resources/js/modules/slideout/globals.ts @@ -1,6 +1,5 @@ import {compatify} from '@craftcms/garnish/compat'; import {Slideout} from './slideout'; -import {CpScreenSlideout} from './cp-screen-slideout'; import {registerCraftGlobals} from '@/common/craft-global'; // `compatify`, not the plain class: plugins subclass this global via the @@ -25,13 +24,3 @@ Object.assign(CompatSlideout, { }); registerCraftGlobals({Slideout: CompatSlideout}); - -// Same reasoning one level down: plugins subclass CP screen slideouts via -// the legacy `.extend()` API. -const CompatCpScreenSlideout = compatify(CpScreenSlideout); - -Object.assign(CompatCpScreenSlideout, { - defaults: CpScreenSlideout.defaults, -}); - -registerCraftGlobals({CpScreenSlideout: CompatCpScreenSlideout}); diff --git a/resources/js/modules/slideout/index.ts b/resources/js/modules/slideout/index.ts index 25b472f5cff..de4430cb58e 100644 --- a/resources/js/modules/slideout/index.ts +++ b/resources/js/modules/slideout/index.ts @@ -1,13 +1,5 @@ import './globals'; import {Slideout, type SlideoutSettings} from './slideout'; -import { - CpScreenSlideout, - type CpScreenSlideoutSettings, -} from './cp-screen-slideout'; -import { - ElementEditorSlideout, - type ElementEditorSlideoutSettings, -} from './element-editor-slideout'; import {containerSlideouts} from './support'; /** @@ -20,12 +12,4 @@ import {containerSlideouts} from './support'; // up here, via `Garnish.on(Slideout, 'open close')`. It now lives in the // shared panel stack, so Vue slideouts trigger it too. -export { - Slideout, - type SlideoutSettings, - CpScreenSlideout, - type CpScreenSlideoutSettings, - ElementEditorSlideout, - type ElementEditorSlideoutSettings, - containerSlideouts, -}; +export {Slideout, type SlideoutSettings, containerSlideouts}; diff --git a/resources/js/modules/sortable-checkbox-select/index.ts b/resources/js/modules/sortable-checkbox-select/index.ts index 9db6c3811c5..75d32b9979e 100644 --- a/resources/js/modules/sortable-checkbox-select/index.ts +++ b/resources/js/modules/sortable-checkbox-select/index.ts @@ -1,3 +1 @@ import '@craftcms/ui/components/checkbox-select/checkbox-select'; - -export {SortableCheckboxSelect} from './sortable-checkbox-select'; diff --git a/yii2-adapter/resources/js/cp-compat.ts b/yii2-adapter/resources/js/cp-compat.ts index 1b6f970a85d..58d450f52fd 100644 --- a/yii2-adapter/resources/js/cp-compat.ts +++ b/yii2-adapter/resources/js/cp-compat.ts @@ -10,6 +10,7 @@ import '@/modules/auth-method-setup/compat'; import '@/modules/customize-sources/compat'; import '@/modules/data-table-sorter/compat'; import '@/modules/editable-table/compat'; +import '@/modules/element-action-trigger/compat'; import '@/modules/element-select-input/compat'; import '@/modules/element-selector-modal/compat'; import '@/modules/element-table-sorter/compat'; From bff7fcac90751ed3176a85155359953495912c6f Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Mon, 2 Nov 2026 16:33:18 -0600 Subject: [PATCH 02/41] Port the auth method setup and entry type override settings off jQuery The auth method listing refresh parses the new markup with a template element, and the entry type override settings serialize their inputs with serializeFormInputs(). The grouped entry type manager only declared $. Co-Authored-By: Claude Opus 5.5 --- .../auth-method-setup.test.ts | 37 +++++++++++++++++++ .../auth-method-setup/auth-method-setup.ts | 16 +++++--- .../EntryTypeOverrideSettings.vue | 4 +- .../grouped-entry-type-manager.ts | 2 - 4 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 resources/js/modules/auth-method-setup/auth-method-setup.test.ts diff --git a/resources/js/modules/auth-method-setup/auth-method-setup.test.ts b/resources/js/modules/auth-method-setup/auth-method-setup.test.ts new file mode 100644 index 00000000000..919315394e8 --- /dev/null +++ b/resources/js/modules/auth-method-setup/auth-method-setup.test.ts @@ -0,0 +1,37 @@ +import {afterEach, expect, it, vi} from 'vite-plus/test'; +import {actionClient} from '@craftcms/ui/utilities/api/actionClient'; +import {AuthMethodSetup} from './auth-method-setup'; + +afterEach(() => { + document.body.innerHTML = ''; + vi.restoreAllMocks(); +}); + +it('replaces the method listing with the refreshed one', async () => { + document.body.innerHTML = ` +
+
+
`; + vi.spyOn(actionClient, 'post').mockResolvedValue({ + data: { + html: `
+
+
+
`, + headHtml: '', + bodyHtml: '', + }, + }); + const onRefresh = vi.fn(); + const setup = new AuthMethodSetup({onRefresh}); + + setup.refresh(); + await vi.waitFor(() => expect(onRefresh).toHaveBeenCalled()); + + const container = document.getElementById('auth-method-setup')!; + expect( + Array.from(container.children).map((el) => el.getAttribute('data-method')) + ).toEqual(['totp', 'webauthn']); + expect(container.textContent).toContain('Remove'); + expect(Object.keys(setup.methodListings!)).toEqual(['totp', 'webauthn']); +}); diff --git a/resources/js/modules/auth-method-setup/auth-method-setup.ts b/resources/js/modules/auth-method-setup/auth-method-setup.ts index 0d69963d05e..23f841c9a53 100644 --- a/resources/js/modules/auth-method-setup/auth-method-setup.ts +++ b/resources/js/modules/auth-method-setup/auth-method-setup.ts @@ -19,7 +19,6 @@ import {announce} from '@/common/announcer'; import {initLegacyUi} from '@/common/craft/legacy-ui'; declare const Craft: any; -declare const $: any; export interface AuthMethodSetupSettings extends GarnishBaseSettings { /** Called after {@link AuthMethodSetup.refresh} re-renders the listing. */ @@ -182,10 +181,17 @@ export class AuthMethodSetup extends Base { ); } - const $container = $('#auth-method-setup').html( - $(data.html).children() - ); - initLegacyUi($container); + const container = document.getElementById('auth-method-setup'); + if (container) { + const template = document.createElement('template'); + template.innerHTML = data.html; + container.replaceChildren( + ...Array.from(template.content.children).flatMap((element) => + Array.from(element.children) + ) + ); + initLegacyUi(container); + } await appendHeadHtml(data.headHtml); await appendBodyHtml(data.bodyHtml); this.initUi(); diff --git a/resources/js/modules/grouped-entry-type-manager/EntryTypeOverrideSettings.vue b/resources/js/modules/grouped-entry-type-manager/EntryTypeOverrideSettings.vue index 0c3867c3587..b10f2ddee4f 100644 --- a/resources/js/modules/grouped-entry-type-manager/EntryTypeOverrideSettings.vue +++ b/resources/js/modules/grouped-entry-type-manager/EntryTypeOverrideSettings.vue @@ -5,6 +5,7 @@ */ import {useForm} from '@inertiajs/vue3'; import {t} from '@craftcms/ui'; + import {serializeFormInputs} from '@craftcms/ui/utilities/dom'; import CpContainer from '@/common/components/CpContainer.vue'; import HtmlFragmentRenderer from '@/common/components/HtmlFragmentRenderer.vue'; import {useAppLayout} from '@/common/composables/useAppLayout'; @@ -14,7 +15,6 @@ import {showError} from '@/modules/messages/messages'; declare const Craft: any; - declare const $: any; const props = defineProps<{ contextId: string; @@ -37,7 +37,7 @@ })); function serialize(): string { - return container ? $(container).find(':input').serialize() : ''; + return container ? serializeFormInputs(container) : ''; } function onReady(element: HTMLElement): void { diff --git a/resources/js/modules/grouped-entry-type-manager/grouped-entry-type-manager.ts b/resources/js/modules/grouped-entry-type-manager/grouped-entry-type-manager.ts index ac94d5a9e88..27634ac94ca 100644 --- a/resources/js/modules/grouped-entry-type-manager/grouped-entry-type-manager.ts +++ b/resources/js/modules/grouped-entry-type-manager/grouped-entry-type-manager.ts @@ -23,8 +23,6 @@ import {actionClient} from '@craftcms/ui/utilities/api/actionClient'; import {hasMousePointerEvents} from '@/common/craft/pointer'; declare const Craft: any; -declare const $: any; - /** `$defaultColumnsContainer` is accepted as a jQuery alias. */ export interface GroupedEntryTypeManagerSettings extends GarnishBaseSettings { /** Applied to cloned select ids and the default-columns inputs. */ From 36e866477b3b970639138e46e81426f9bd69dec4 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Mon, 2 Nov 2026 16:36:39 -0600 Subject: [PATCH 03/41] Build chip action fallbacks without jQuery Chips with no action container fall back to the legacy disclosure menu, which only the adapter provides. The fallback now checks for that plugin first and builds its markup with the DOM, so without the adapter it does nothing instead of throwing. Co-Authored-By: Claude Opus 5.5 --- .../js/common/craft/chip-actions.test.ts | 9 +++ resources/js/common/craft/chip-actions.ts | 62 ++++++++++--------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/resources/js/common/craft/chip-actions.test.ts b/resources/js/common/craft/chip-actions.test.ts index a54aea92776..b4f623a33d0 100644 --- a/resources/js/common/craft/chip-actions.test.ts +++ b/resources/js/common/craft/chip-actions.test.ts @@ -67,3 +67,12 @@ it('puts prepended actions ahead of the existing ones', async () => { const menu = el.querySelector('craft-action-menu')!; await vi.waitFor(() => expect(itemLabels(menu)).toEqual(['Replace', 'Edit'])); }); + +it('leaves a chip without an action container alone when the legacy menu plugin isn’t loaded', () => { + const el = document.createElement('div'); + el.className = 'chip'; + document.body.append(el); + + expect(() => addActionsToChip(el, [{label: 'Replace'}])).not.toThrow(); + expect(el.children).toHaveLength(0); +}); diff --git a/resources/js/common/craft/chip-actions.ts b/resources/js/common/craft/chip-actions.ts index f04e51cdb97..94e3cde385d 100644 --- a/resources/js/common/craft/chip-actions.ts +++ b/resources/js/common/craft/chip-actions.ts @@ -2,8 +2,6 @@ import {t} from '@craftcms/ui/utilities/translate'; import {setElementAttributes} from './attributes'; import {initLegacyUi} from './legacy-ui'; -declare const $: any; - /** An action in the shape the legacy `disclosureMenu()` plugin accepts. */ export interface ChipAction { label?: string; @@ -172,9 +170,16 @@ function addActionsToDisclosureMenu( actions: ChipAction[], prepend: boolean ): void { - let $trigger = $(trigger); + // The legacy menu plugin comes with the Yii2 adapter + // oxlint-disable-next-line typescript/no-explicit-any + const $ = (window as {jQuery?: any}).jQuery; + if (typeof $?.fn.disclosureMenu !== 'function') { + return; + } + + let triggerElement = trigger; - if (!trigger) { + if (!triggerElement) { console.warn( 'A chip was given actions without an action container, so a deprecated ' + 'disclosure menu was built for it. Render the chip with a ' + @@ -185,34 +190,31 @@ function addActionsToDisclosureMenu( const menuId = `actions-${Math.floor(Math.random() * 1000000)}`; const labelId = `${menuId}-label`; - const $label = $('