Skip to content

[6.x] Stop core's control panel loading jQuery and Garnish - #20320

Closed
brianjhanson wants to merge 47 commits into
chore/js-modulesfrom
chore/js-modules-jquery
Closed

brianjhanson wants to merge 47 commits into
chore/js-modulesfrom
chore/js-modules-jquery

Conversation

@brianjhanson

@brianjhanson brianjhanson commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #20276. Core's control panel JavaScript no longer needs jQuery or Garnish; the Yii2 adapter loads them for itself and for plugins.

Description

 CpAsset (core)
   AnimationBlockerAsset, D3Asset, ThemeAsset
-  JqueryAsset, GarnishAsset (+ touch events, Velocity)

 CraftJsAsset (adapter)
   CpAsset
+  GarnishAsset (+ jQuery, touch events, Velocity)
   jQuery UI, other vendor assets

What moved where:

core (native DOM)                         adapter (jQuery, for plugins)
├── grid, slideout, charts, modals        ├── Craft.BaseElementSelectInput,
├── element select inputs                 │   EntrySelectInput, AssetSelectInput,
│   <craft-element-select-input> etc.     │   TagSelectInput (jQuery copies)
├── MenuHtml::disclosureMenu()            ├── LegacyMenuHtml → legacy disclosure markup
│   → <craft-action-menu>                 │   (its Matrix drives it with jQuery)
└── legacy screens' tabs → <craft-tabs>   └── Craft.Tabs
  • The ported classes drop their $-prefixed jQuery getters. A GitHub search found few plugins reading them. CpScreenSlideout keeps $container for adapter callers.
  • elementSelect.twig always renders the custom element. A plugin's jsClass boots through input-class, so subclasses of the adapter's Craft.BaseElementSelectInput keep working.
  • Server-rendered menus keep group headings, the current item, statuses and info icons. A slideout opened from one returns focus to the menu's button.
  • The inline scripts that core templates emit no longer call $.

Evidence

New regression tests fail on the old code and pass now:

LegacyAssetsTest
  CpAsset html  → contains theme CSS, no jquery.js, no garnish.js
  MoneyAsset    → garnish.js before Money.js
MenuHtmlTest
  disclosureMenu(items with heading, selected, status, info)
    → <craft-action-menu> with group heading, current item, status dot, info icon
slideout.browser.test
  open from craft-action-menu item, item hidden, close
    → focus lands on the menu's invoker
base-element-select-input.test (native)
  add/remove/reorder chips without jQuery on the page

In a browser on a core-only install (window.jQuery undefined), these load with no console errors: the dashboard, the entry editor and its tabs, the entry type and section settings, the assets and users indexes, and the system report. The element select inputs and a rendered menu work there too.

Local CI run on this branch:

  • vp fmt --check, vp lint, lint:styles, the Tailwind class check, typecheck, @craftcms/ui#check:types: pass
  • test:ui 3409, unit 2315, browser 122: pass
  • pint, rector --dry-run, phpstan: pass
  • pest 12,039 passed; composer tests-adapter 784 passed

Server-built menus on a bridged cp/Screen, driven in Chromium on a core-only install: a throwaway asCpScreen() route with action and context menu items, plus the breadcrumb template rendered into its content.

  • Action menu: group heading, current item, status dot, info icon and destructive item all render.
  • Context menu: labels itself from its current item.
  • All three menus open from their buttons. Escape closes them and returns focus to the button, arrow keys move between items, and a link item navigates.
  • There were no console errors, and window.jQuery was undefined.

In core, breadcrumbs reach the page as data and Breadcrumbs.vue renders them, so _layouts/components/crumbs.twig only runs for a screen that turns off fragment collection. Nothing in core does that.

🤖 Generated with Claude Code

brianjhanson and others added 21 commits November 2, 2026 16:30
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Craft.Grid laid out dashboard widgets and field layout tabs through
jQuery collections and jQuery's box-model measurements. It works on plain
elements now, measuring content boxes itself, and still offers $container
and $items as jQuery collections for the legacy dashboard when jQuery is
loaded. The dashboard and field layout designer pass it elements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preview modal builds its markup with the DOM and inserts preview HTML
with appendElementHtml(), which runs its scripts as jQuery's append() did.
$container stays, as a jQuery collection when jQuery is loaded, for
preview templates. The focal point editor drags with pointer events, which
also covers touch, and its listeners now come off when it's destroyed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CP modal builds its dialog with the DOM, inserts content with
appendElementHtml(), and keeps its delta-tracking state in private fields
instead of jQuery's data(); a serializer legacy code sets through data()
is still honoured. The element deletion manager builds its blockers with
the DOM, and both keep $container-style getters for legacy code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Craft.charts draws into plain elements, deep-merges its settings with a
helper that matches jQuery's $.extend(true, …), and measures and positions
its tooltip itself. $container, $chart and $tip stay as jQuery getters for
legacy chart widgets. The content-box measurement the grid and focal point
editor each had moves to a shared contentSize() helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Craft.Slideout keeps its container, outer container, shade and trigger as
plain elements and positions and transitions them with the DOM. The
$-prefixed properties stay as jQuery getters for the CP screen slideout and
other legacy subclasses. The auth method setup slideout and two-step
verification read the plain container.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The create button listens for clicks, which a craft-button also fires on
Enter and Space, instead of Garnish's jQuery activate event. A chip's edit
shortcut uses a dblclick listener and VueUse's onLongPress for touch,
replacing the jQuery touch events plugin's taphold. The legacy disclosure
menu is only booted through jQuery when the adapter has loaded it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a handful of plugins read them, and they need Craft 6 updates anyway.
CP screen slideouts keep `$container`, since they're jQuery-based and plugins
subclass them through the adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The element select template rendered an inline `new Craft.XSelectInput()`, so
core relation fields needed the Craft globals. It now always renders the
matching custom element, passing a plugin's class name as `input-class`, and
the Craft.* registrations move to the adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins subclass Craft.BaseElementSelectInput and Craft.AssetSelectInput and
use their jQuery members, so the adapter keeps copies of the current classes
for Craft.*. That frees core's classes to drop jQuery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refresh queued before unmount ran against the destroyed grid and threw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base, entry and asset select inputs keep plain elements (container,
elementsContainer, elements…) and use native events: click for the Add
button, dblclick and a long press for editing, and craft-reorder for the
reorder buttons. The element markup helpers and file preview modal take plain
elements too, and the preview works with native selects. The adapter's copies
keep the jQuery versions for Craft.*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Field layout and nested element refreshes serialize the form natively, the
legacy element editor is looked up only when jQuery is present, element
editors read a chip's data attributes directly, and the asset mover's Undo
button listens for click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The disclosure menu items, the 2FA setup page and the entry type override
chip listen with addEventListener; the menu only reaches for the legacy
instance when jQuery is loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core's control panel no longer needs them. The adapter's Craft.js asset brings
jQuery, Garnish, touch events and Velocity for plugins, and the legacy money
input depends on Garnish itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Categories fields no longer emit an inline script; their input boots from
the element select custom element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MenuHtml::disclosureMenu() builds a <craft-action-menu>, so core's menus no
longer need the jQuery disclosure menu plugin. Groups keep their headings,
the selected item is marked current, and statuses and info icons carry over.
The adapter binds a MenuHtml that keeps the legacy markup its jQuery code
and plugins rely on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the adapter's legacy screens, slideouts and element editor create it;
core renders <craft-tabs>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/ui — open Storybook

No changed components detected in this Storybook.

resources/js — open Storybook

No changed components detected in this Storybook.

brandonkelly and others added 8 commits November 4, 2026 01:21
Core's FocalPointAsset and Craft.FocalPoint were removed, so the
deprecated adapter bundle referenced a class that no longer exists,
which failed PHPStan.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
CpAsset no longer loads jQuery, so the Link field, the entry type
compatibility check, the range input and the relation field settings
threw on installs without the Yii2 adapter, stopping every inline
script after them. customSelect.twig's legacy menu only works with the
adapter, so it now returns early without jQuery.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
Moving a controller element (Matrix move up/down, drag-sorting)
destroyed and rebuilt its controller, duplicating chip actions and, on
Assets fields, the uploader. Teardown now waits a microtask and keeps
the controller when the element is back in the document.

A plugin's input-class is now waited for, for up to 10 seconds, so
markup inserted before the plugin's script boots once it loads.
AssetSelectInput now tears down its uploader and the controls it adds.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
The template had become macros only, so plugin includes rendered
nothing. It now renders through disclosureMenu(), so the adapter's
legacy menu applies there too.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
The tip was measured while hidden, so its width was 0: tips near the
right edge never flipped, and RTL tips covered their point. jQuery's
width() used to measure hidden elements by showing them briefly.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
Edit and Open in Image Editor went to the global window listeners, so
Edit lost the input's prevalidate setting and an image saved as a new
asset no longer replaced its chip. The input now claims those events
for its own chips, as the jQuery version did.

Also drop replaced chips from the selection after a structured
selection, and use the field's own spinner rather than the search
input's.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
LinkField was only reachable through jQuery's data cache, so LinkInput
and the element link types' script couldn't reach it without the
adapter. The script also read the element select input from that
cache, which the ported input no longer sets. LinkField.forContainer()
finds it now, and the script listens for the input's DOM events.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
Screen.vue read Tabs from window.Craft, which only the adapter sets now,
and read each tab's href through jQuery. It imports Tabs instead, and
Tabs events carry the tab element as well as $tab.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
# Conflicts:
#	yii2-adapter/legacy/web/assets/focalpoint/FocalPointAsset.php

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Wrapped initialization scripts no longer execute in modal content, which can break plugin controls across the affected modal and deletion flows.

1 open issue
2 resolved since last review

Review effort: Balanced

appendElementHtml() recreated only top-level scripts, so a script inside a
wrapper element was cloned as already started and never ran. jQuery's .html()
ran them, so modal, file preview and element deletion content that wraps its
init scripts stopped working when those moved off jQuery.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Descendant scripts bypass awaited asset loading, allowing initialization to run before dependencies load and the returned promise to resolve prematurely.

1 open issue
1 resolved since last review

Review effort: Balanced

Recreating nested scripts all at once let an inline init script run before an
external library ahead of it had loaded, and let the promise resolve before
that load finished. Activate them one at a time, awaiting each external
script through the same path as top-level ones, so they run in order and
rejectOnError covers their failures.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unawaited blocker-details insertion can leave controls uninitialized and execute body scripts before dependencies load, while unstable nested action IDs can recreate grouped actions and lose keyboard focus.

2 open issues
1 resolved since last review

Review effort: Balanced

Comment thread src/Cp/Components/ActionMenu.php Resolved
brandonkelly and others added 2 commits November 4, 2026 15:29
Groups now keep their items, which get random IDs. Only top-level IDs were
removed, so an unchanged menu's payload differed on every render and
ActionList.vue recreated its grouped items.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>
The details markup was appended without waiting, so initLegacyUi() could run
before controls after an external script were added, and the response's body
scripts could run before those scripts loaded.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a41f-35f1-75f6-a070-f628cc4e7276
Co-authored-by: Amp <amp@ampcode.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed to evaluate asynchronous preview races and overlapping image-editor handlers that can cause stale content or duplicate editors.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

@brandonkelly

Copy link
Copy Markdown
Member

@brianjhanson Claude made some changes in response to a few rounds of Copilot reviews. If they look good to you, feel free to merge.

@brianjhanson

Copy link
Copy Markdown
Contributor Author

After chatting with @i-just this morning, I'm doing a pass of this branch against CKEditor to make sure we still have everything we need in place. Hoping to wrap that up today.

@brianjhanson
brianjhanson added this pull request to stack #20363 November 4, 2026 22:22
@brianjhanson

Copy link
Copy Markdown
Contributor Author

Collapsed into #20378, which carries this work plus #20276 and #20359 on a single branch rebased onto 6.x. Reviewing one branch beats keeping three bases in sync.

Everything here came across unchanged, including the LegacyAssetsTest and MenuHtmlTest regression tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants