[6.x] Feature/module port iii - #19352
Merged
Merged
Conversation
Core no longer instantiates Craft.ComponentSelectInput — every core CP surface renders the self-booting <craft-component-select> web component. The legacy jQuery class survived only for the componentSelect.twig `jsClass` escape hatch (plugin subclasses). Move the real 681-line implementation verbatim out of the craftcms-legacy CP bundle into legacy/web/assets/cpcompat/component-select-input.js so the bundle can drop it while plugin subclasses keep a working base class. It defines its class at top-level eval (not on DOMContentLoaded like cp-compat.js) so a plugin's Craft.Foo = Craft.ComponentSelectInput.extend() can run whenever its own bundle loads, and is registered ahead of cp-compat.js via CpCompatAsset. Drop the Craft.js import and fix the component-select README/index comments that claimed the class still ships in the legacy bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bundle
Both were self-contained native custom elements still defined in the
craftcms-legacy CP bundle. Move their definitions to resources/js modules
(proxy-scrollbar, element-label), registered via defineElement and imported
from both cp.ts and legacy.ts so the tags stay defined on every CP page —
matching the coverage the webpack bundle gave them.
proxy-scrollbar was already jQuery-free. element-label's `$('#tabs')`
tab-change subscription is kept as a documented jq() seam (the tab set is
still the legacy Craft.Tabs instance in jQuery's data cache) and its
`$(ready)` deferral becomes a native document-ready check.
Drop both files and their Craft.js imports; PHP/CSS/legacy consumers that
emit the tags now resolve against the Vite-registered elements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
element-label's only remaining jQuery use was reading the Craft.Tabs
instance out of jQuery's `.data('tabs')` cache to hook `selectTab` — needed
because a label inside a `display: none` tab measures width 0 until the tab
is shown. Craft.Tabs fires only a Garnish event (no native DOM event), so
there was nothing to addEventListener for.
Replace the whole tabs coupling with a ResizeObserver on the element, which
fires when the label (or an ancestor tab pane) leaves `display: none` and on
any later resize — covering both the selectTab hook and the old `$(ready)`
re-measure. The module is now fully jQuery-free with no dependency on the
still-legacy Tabs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add createTextInput to the factory as a jQuery-free plain <input> builder — the shared building block many CP inputs are made of, and the next step of the Craft.ui factory rollout. Faithfully maps the legacy attribute surface (including a jQuery-free port of addAttributes for aria/data/class/style), using logical padding-inline-end for the chars-left case instead of the Craft.orientation branch. Reduce Craft.ui.createTextInput to a thin shim over the factory that keeps the Garnish-specific pieces the factory leaves out: mobile autofocus suppression, the NiceText enhancement, the .passwordwrapper wrapper, and the legacy jQuery return type. Covered by three new factory tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FormObserver watches a form (or any container of named inputs) and fires a debounced callback when its serialized value actually changes — backing draft autosave in ElementEditor/ContentBlockEditor (and matrix-entry). Port it onto @craftcms/garnish Base, fully jQuery-free: serializeFormInputs (from @craftcms/ui) replaces jQuery's .serialize(), and change detection rides on native MutationObserver + Base.addListener (which splits the comma event grammar). It's instantiated imperatively (new Craft.FormObserver(...)), so the class is exposed on window.Craft as a seam for the still-legacy callers rather than as a custom element. Imported from cp.ts + legacy.ts; legacy file and its Craft.js import removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A thin setInterval wrapper (start/stop around settings.onInterval). Port it
onto @craftcms/garnish Base; the only jQuery was the $.noop default, now
() => {}. Craft.ProgressBar still instantiates it, so the class is exposed on
window.Craft as a seam. Imported from cp.ts + legacy.ts; legacy file and its
Craft.js import removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The imperative CP ports had each grown their own copies of these. Pull them into resources/js/common: - common/utils/dom.ts — resolveElement() (selector/element/array-like incl. jQuery → HTMLElement) + queryAll(); repoints field-toggle and form-observer, which had identical local copies. - common/utils/jquery.ts — jq(), the single documented jQuery seam; repoints field-toggle, elevated-session, and matrix/interop (which keeps its typed JQueryLike wrapper, now delegating). Every remaining jQuery dependency in resources/js is now greppable to this one accessor. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add common/craft-global.ts — registerCraftGlobals({X}) assigns onto the legacy
window.Craft namespace (creating it if needed) so PHP-emitted markup and the
still-legacy cp bundle reach ported classes via Craft.*.
Collapse the `const craft = (window as any).Craft ?? ((window as any).Craft = {})`
boilerplate that 17 module index files were each repeating down to a single
typed call. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pilot) EntryMover opens a modal listing the sections the selected entries can move to, and performs the move on confirm (booted from MoveToSection.php). Port it onto @craftcms/garnish Base — the first resources/js consumer of the modern Modal and Select, which drop in 1:1 for their legacy Garnish.* equivalents (Modal autoShows + attaches to <body>; Select's settings — vertical/checkboxMode/filter/onSelectionChange — and its HTMLElement[] $selectedItems map directly). Modal markup is built with plain DOM. Two documented seams remain: Craft.ui.createSubmitButton (not yet in @craftcms/ui/factory) and the legacy jQuery elementIndex passed in by the still-legacy element index. Craft.EntryMover stays a window.Craft global; legacy file and its Craft.js import removed. This proves the modal substrate is a drop-in, unblocking PromptHandler, DeleteUserModal, CpModal, and createCopyTextPrompt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PromptHandler shows a modal to resolve conflicts (e.g. filename clashes on
upload), one prompt at a time across a batch with an optional "apply to
remaining" checkbox; booted by Craft.AssetIndex.
Second modal-substrate port. jQuery-free on @craftcms/garnish Base + Modal:
modal markup built with plain DOM, and the legacy jQuery .fadeOut('fast')
becomes a WAAPI opacity animation. The one adaptation flagged in the
EntryMover pilot — legacy pokes the shared static Garnish.Modal.$shade to make
a shade click cancel the prompt — becomes a per-instance this.modal.$shade
listener wired once at modal creation, paired with hideOnShadeClick:false so
the modal doesn't also hide. Public API (resetPrompts/addPrompt/
getPromptCount/showBatchPrompts) is unchanged for AssetIndex; Craft.PromptHandler
stays a window.Craft global. Legacy file and its Craft.js import removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
createCopyTextPrompt opens a modal showing a value in a read-only field with a
copy-to-clipboard button (used by LinkInput's "Copy URL", the plugin manager,
and the upgrade utility).
@craftcms/ui doesn't depend on @craftcms/garnish, so this factory is built on
@craftcms/ui's own components rather than the garnish Modal: <craft-dialog>
(the modal) + <craft-copy-button> (which copies via navigator.clipboard and
shows its own feedback, replacing the legacy execCommand('copy') +
displayNotice). Supports the text and textarea variants its callers use, and
closes on copy. Reduce Craft.ui.createCopyTextPrompt to a thin shim delegating
to the factory. Covered by two new factory tests.
This unblocks the deferred LinkField/LinkInput port (its last non-disclosure
seam was this prompt).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Link field's client code: LinkField (outer orchestrator — syncs label/
filename placeholders to the active link type) and LinkInput (inner control —
toggles between a text input and a committed-value chip, validating against the
type's pattern/prefixes). Booted from Link.php / BaseTextLinkType.php.
Port both onto @craftcms/garnish Base in resources/js/modules/link-field:
- The chip's action menu (legacy Garnish.DisclosureMenu addItem/addHr) becomes
a Craft.addActionsToChip descriptor array — the smart craft-action-menu
helper, same seam component-select uses. String icon names, and the
destructive Remove item auto-groups, so no manual divider.
- createTextInput + createCopyTextPrompt come from @craftcms/ui/factory (the
earlier factory ports); modal markup and chips are built with plain DOM.
- LinkField reuses the FieldToggle peer module directly (get-or-create via its
WeakMap) instead of the jQuery .fieldtoggle() plugin, and moves its
linkLabel/filename storage from jQuery .data to a WeakMap.
- Remaining documented seams: Craft.addActionsToChip, the Craft.* string
helpers, and the .data('linkField') pair handoff. punycode is imported (with
a local .d.ts) for the IDN-aware URL validation.
Removed both legacy files and their Craft.js imports; Craft.LinkField/LinkInput
stay window.Craft globals.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Craft.DeleteUserModal was deprecated in 5.10.0 and is now unused — nothing in core (JS, Twig, or PHP) instantiates it. Remove it from the cp bundle and add a cpcompat defineRemovedClass warn-stub so a stray plugin caller gets a clear deprecation notice instead of an opaque "undefined is not a constructor". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CpModal loads a control-panel screen into a modal form, submits it with
delta-encoded data, and renders validation errors inline; booted by the PHP
element-deletion blockers. This is the first extends-Modal subclass port,
proving that shape on the modern @craftcms/garnish Modal (which de-risks the
P4 modal heavies — AssetImageEditor, PreviewFileModal, CustomizeSourcesModal).
Key pieces:
- Subclass pre-super pattern: the parent Modal is constructed container-less
and non-auto-showing (super(undefined, {autoShow:false})) since TS forbids
`this` before super(); the form container is built after, then setContainer +
an explicit show() once the screen loads.
- onShow/onHide are overridden as real methods (calling super), and shade/esc
closing routes through closeMeMaybe() (dirty-check confirm), so the Modal's
hideOnShadeClick/hideOnEsc are disabled.
- uiLayerManager() save (Ctrl-S) / Esc shortcuts, mirroring cp-screen-slideout.
Modeled on its sibling cp-screen-slideout: keeps the declare-const-$ seam for
the container/form ops, because the delta-tracking data
(.data('initialSerializedValue'|'delta-names'|'serializer')) and Craft.ui
field-error helpers share those conventions with the CP-screen framework.
Removed the legacy file and its Craft.js import; Craft.CpModal stays a global.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tabs manages an ARIA tablist — click/keyboard tab selection, roving-tabindex
focus, horizontal scroll-into-view, and an overflow "more" menu. Booted by CP,
Preview, cp-screen-slideout, and matrix-entry.
Port it onto @craftcms/garnish Base with the selection/keyboard-nav/scroll
logic all plain DOM (native key names, getBoundingClientRect/scrollLeft,
prev/next sibling walking). Two documented jQuery seams remain: the overflow
menu button is still the legacy .disclosureMenu() plugin (read via
.data('trigger')), and the selectTab/deselectTab events keep a jQuery-wrapped
$tab payload because every consumer reads ev.$tab.attr('href'). The
.data('tabs') self-storage is kept for BC (no active reader since element-label
moved to a ResizeObserver). Craft.Tabs stays a window.Craft global; legacy file
and its Craft.js import removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grid is a masonry / bin-packing layout manager, not a CSS-grid layout: its
default fillMode 'top' runs a combinatorial optimizer (Craft.Grid.LayoutGenerator)
that enumerates every colspan/position combination to minimize total height and
empty space, then absolutely-positions variable-width (data-colspan) items —
something CSS display:grid can't express (native masonry isn't shipped
cross-browser). Booted via new Craft.Grid(...) and the .grid jQuery plugin;
used by the Dashboard (masonry widgets) and field-layout-designer.
Port it onto @craftcms/garnish Base with the LayoutGenerator optimizer and all
the colspan/position/layout math transcribed to typed TS (dropping the legacy
`this.method._ = {}` scratch-object pattern for normal locals). Following the
CpModal precedent, it keeps a declare-const-$ seam for the DOM: masonry
correctness depends on jQuery's box-model measurement, and the public
$items/$container API + addItems(jQuery) are consumed by the still-legacy
Dashboard and field-layout-designer. muteResizeEvents comes from
@craftcms/garnish. Craft.Grid stays a window.Craft global (with
Craft.Grid.LayoutGenerator for BC); the legacy file and its Craft.js import are
removed, while the .grid plugin in Craft.js keeps working through the global.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chart.js is the Craft.charts namespace: DataTable (typed cell parsing), Tip (tooltip), BaseChart (settings/lifecycle), Area (a d3 time-series area/line chart with axes, gridlines, plots, and hover tooltips), and a utils formatter object. Booted via new Craft.charts.Area(...) by the chart widgets (e.g. NewUsersWidget). Port the namespace onto @craftcms/garnish Base (Area extends BaseChart extends Base; this.base(...) -> super.*). d3 stays a real ESM import — Vite code-splits it into a separate chart-*.js chunk, so it no longer sits in the main bundle. Like the other complex legacy managers it keeps a declare-const-$ seam: the $container API is passed as jQuery by the still-legacy widgets, and the tooltip/measurement rely on jQuery's box model. Fixed the getDuration helper's self-referencing object literal (a latent bug that would throw if called). Craft.charts stays a window.Craft namespace (with utils for BC); legacy file and its Craft.js import removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First two leaves of the element-index/select clusters: - DataTableSorter onto @craftcms/garnish DragSort — proves the modern DragSort subclass extension (build config → super($rows, settings) → wire the drag-time `helper` callback after super, since it needs `this`). Booted by Craft.AdminTable and the modern editable-table module. - ElementActionTrigger onto @craftcms/garnish Base — an element-index action button's enable/disable + activate wiring; booted by PHP element actions and the element index. Both keep documented seams for the still-legacy surfaces they interop with: the jQuery `$trigger`/`$selectedItems`/table, and Craft.currentElementIndex (the P4a element index, not yet ported). Craft.* globals preserved; legacy files and Craft.js imports removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ElementThumbLoader was already modern code — a thin bridge over @craftcms/ui's ThumbnailLoader (it imported the package even from the legacy bundle). This just moves it into resources/js/modules/element-thumb-loader; Craft.ElementThumbLoader stays a window.Craft global. Legacy file and its Craft.js import removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The structure-aware element-table row sorter: drag-reorders rows including
descendant rows, computes target indentation levels/ancestors, and persists
moves to the server. Created by Craft.TableElementIndexView.
Port onto @craftcms/garnish DragSort (subclass pre-super pattern; overrides
findDraggee/onDrag*/onSortChange/canInsert* calling super). The one real
adaptation: modern DragSort exposes $targetItem/$draggee/helpers as
HTMLElement(s) rather than jQuery, so those are wrapped with $(...) at the
boundaries where the legacy structural traversal (.next()/.first()/.add()/…)
needs them, and findDraggee returns a plain array. The scratch-object locals
(this.method._ = {}) become normal locals. Keeps the declare-const-$ +
Craft.* seams and interop with the still-legacy table view. Craft global;
legacy file and Craft.js import removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the two Garnish.Base leaves off the legacy jQuery bundle onto @craftcms/garnish Base. ElementDeletionManager composes the modern Modal for its blocker dialog; the server-provided callback eval contract is preserved. Both keep their window.Craft.* globals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the asset uploader pair off the legacy jQuery bundle onto @craftcms/garnish Base. The blueimp jQuery File Upload plugin has no modern equivalent, so the $ transport seam is retained. The Craft.createUploader/registerUploaderClass registry (still in the legacy bundle) resolves the reassigned window.Craft.* globals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the legacy Garnish.CustomSelect (floating listbox) and Garnish.MenuBtn (combobox trigger) to jQuery-free TypeScript on Base, unblocking the P3 element-select cluster on the modern stack. Exposed as named exports and on the core Garnish namespace, so the compat layer auto-wraps them with extend()/base and activates the Garnish.Menu alias. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lectorModal to TypeScript
Moves the element selector modal sub-tree (Phase B step 2) out of the legacy
jQuery webpack bundle and into a modern TypeScript module under
resources/js/modules/element-selector-modal/.
The three classes extend Garnish's Modal (via the modern @craftcms/garnish
package) using the same new.target init-guard pattern as the BaseUploader/
Uploader port, allowing the subclass hierarchy to share a single Modal
construction.
Key seam decisions:
- doubletap on element-index elements uses $(el).on('doubletap') because
doubletap is a jQuery synthetic event; native addEventListener would miss it
- Buttons use addListener(el, 'click') — <button> fires native click on
keyboard activation, so jQuery activate is not needed here
- this.settings is declared as any (shadowing Modal's typed settings) to hold
Craft-specific settings alongside the Modal settings we need (resizable,
hideOnShadeClick, lifecycle no-op callbacks)
- Sidebar layer management via uiLayerManager() from the slideout module
- Craft.t() values resolved lazily in init(), not at static class field time
AssetSelectorModal registers itself via Craft.registerElementSelectorModalClass
for the Asset element type, matching the legacy registration call.
VolumeFolderSelectorModal forces foldersOnly mode and allows selecting the
currently open folder even with no element highlighted.
Both Craft.ts entry points (cp.ts and legacy.ts) import the new index.ts,
which registers all three classes on window.Craft.* via registerCraftGlobals.
The three legacy .js source files have been emptied and their import lines
removed from packages/craftcms-legacy/cp/src/Craft.js.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Script modules Migrate three legacy jQuery CP classes from packages/craftcms-legacy/cp/src/js/ to the modern TypeScript module pattern under resources/js/modules/element-select-input/. - base-element-select-input.ts: port of BaseElementSelectInput (~580 lines); extends Base; handles element selection, sorting (DragSort), search autocomplete, replace/create flows, and modal launching - entry-select-input.ts: thin subclass that opens an EntrySelectModal - tag-select-input.ts: replaces the 'add' button with an inline text input; searches tags via the tags/search-for-tags action and creates new tags via tags/create-tag - index.ts: barrel + registerCraftGlobals wiring Remove the corresponding legacy JS files from the webpack bundle (Craft.js imports removed; legacy files emptied). Import both modules from cp.ts and legacy.ts so they boot on every CP page. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eScript modules Migrate three legacy jQuery CP classes and seven nested companion classes from packages/craftcms-legacy/cp/src/js/ to the modern TypeScript module pattern under resources/js/modules/. preview-file-modal/ - preview-file-modal.ts: port of Craft.PreviewFileModal; extends Modal; uses quickShow() to skip fade-in; supports arrow-key navigation between sibling assets in an element select; static openInstance, showForAsset, resizePreviewImage preserved and registered on window.Craft.PreviewFileModal asset-select-input/ - asset-select-input.ts: port of Craft.AssetSelectInput; extends BaseElementSelectInput; adds upload button + hidden file input wired to Craft.createUploader, ProgressBar overlay during uploads, and Shift-Space keyboard shortcut to open PreviewFileModal customize-sources-modal/ - customize-sources-modal.ts: port of Craft.CustomizeSourcesModal and all seven nested classes (PageSettingsModal, SourceDrag, Page, BaseSource, Source, CustomSource, Heading); extends Modal/DragSort/Base as appropriate; all nested classes registered on window.Craft.CustomizeSourcesModal.* Remove the corresponding legacy JS files from the webpack bundle (Craft.js imports removed; legacy files emptied). Import all new modules from cp.ts and legacy.ts so they boot on every CP page. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The PreviewFileModal, AssetSelectInput, and CustomizeSourcesModal ports defined a local registerCraftGlobals() that was never called, so window.Craft.PreviewFileModal / .AssetSelectInput / .CustomizeSourcesModal (and its nested classes) were never assigned at runtime — a break that typecheck and bundling can't catch. Switch all three to the shared @/common/craft-global helper and invoke it at import time, matching the element-selector-modal and element-select-input modules. Also delete the emptied legacy .js stubs (they were truncated rather than removed) and pick up the Prettier reformats left in the working tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolved import-list conflicts in the CP entrypoints — kept this branch's ported-module set and added 6.x's new nested-element-manager + ui module imports (cp.ts, legacy.ts); kept the ported files unimported in the legacy Craft.js bundle; took 6.x's slideout craft:edit-element listener addition. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📚 Storybook previews@craftcms/ui — open Storybook No changed components detected in this Storybook. resources/js — open Storybook No changed components detected in this Storybook. |
# Conflicts: # packages/craftcms-legacy/cp/src/js/BaseElementSelectInput.js
[ci skip]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Related issues