diff --git a/client/src/entrypoints/admin/telepath/widgets.js b/client/src/entrypoints/admin/telepath/widgets.js
index d4a88cad2d..0981c0b5fb 100644
--- a/client/src/entrypoints/admin/telepath/widgets.js
+++ b/client/src/entrypoints/admin/telepath/widgets.js
@@ -3,18 +3,38 @@ import { runInlineScripts } from '../../../utils/runInlineScripts';
class BoundWidget {
constructor(
- element,
+ elementOrNodeList,
name,
idForLabel,
initialState,
parentCapabilities,
options,
) {
+ // if elementOrNodeList not iterable, it must be a single element
+ const nodeList = elementOrNodeList.forEach
+ ? elementOrNodeList
+ : [elementOrNodeList];
+
+ // look for an input element with the given name, as either a direct element of nodeList
+ // or a descendant
const selector = `:is(input,select,textarea,button)[name="${name}"]`;
- // find, including element itself
- this.input = element.matches(selector)
- ? element
- : element.querySelector(selector);
+
+ for (let i = 0; i < nodeList.length; i += 1) {
+ const element = nodeList[i];
+ if (element.nodeType === Node.ELEMENT_NODE) {
+ if (element.matches(selector)) {
+ this.input = element;
+ break;
+ } else {
+ const input = element.querySelector(selector);
+ if (input) {
+ this.input = input;
+ break;
+ }
+ }
+ }
+ }
+
this.idForLabel = idForLabel;
this.setState(initialState);
this.parentCapabilities = parentCapabilities || new Map();
@@ -71,27 +91,33 @@ class Widget {
const html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id);
const idForLabel = this.idPattern.replace(/__ID__/g, id);
- /* write the HTML into a temp container to parse it into an element */
+ /* write the HTML into a temp container to parse it into a node list */
const tempContainer = document.createElement('div');
tempContainer.innerHTML = html.trim();
- const dom = tempContainer.firstChild;
+ const childNodes = Array.from(tempContainer.childNodes);
+
+ /* replace the placeholder with the new nodes */
+ placeholder.replaceWith(...childNodes);
+
+ const childElements = childNodes.filter(
+ (node) => node.nodeType === Node.ELEMENT_NODE,
+ );
/* execute any scripts in the new element(s) */
- runInlineScripts(tempContainer);
-
- /* replace the placeholder with the new element(s) */
- placeholder.replaceWith(...tempContainer.childNodes);
+ childElements.forEach((element) => {
+ runInlineScripts(element);
+ });
// Add any extra attributes we received to the first element of the widget
if (typeof options?.attributes === 'object') {
Object.entries(options.attributes).forEach(([key, value]) => {
- dom.setAttribute(key, value);
+ childElements[0].setAttribute(key, value);
});
}
// eslint-disable-next-line new-cap
return new this.boundWidgetClass(
- dom,
+ childElements.length === 1 ? childElements[0] : childNodes,
name,
idForLabel,
initialState,
diff --git a/client/src/entrypoints/admin/telepath/widgets.test.js b/client/src/entrypoints/admin/telepath/widgets.test.js
index f2551a44b0..4b045122e8 100644
--- a/client/src/entrypoints/admin/telepath/widgets.test.js
+++ b/client/src/entrypoints/admin/telepath/widgets.test.js
@@ -133,7 +133,7 @@ describe('telepath: wagtail.widgets.Widget with multiple top-level nodes', () =>
widgetDef = window.telepath.unpack({
_type: 'wagtail.widgets.Widget',
_args: [
- '',
+ '',
'__ID__',
],
});
diff --git a/client/src/utils/runInlineScripts.ts b/client/src/utils/runInlineScripts.ts
index 902bf44afe..5d0a1ad83f 100644
--- a/client/src/utils/runInlineScripts.ts
+++ b/client/src/utils/runInlineScripts.ts
@@ -1,20 +1,27 @@
/**
* Runs any inline scripts contained within the given DOM element or fragment.
*/
+const runScript = (script: HTMLScriptElement) => {
+ if (!script.type || script.type === 'application/javascript') {
+ const newScript = document.createElement('script');
+ Array.from(script.attributes).forEach((key) =>
+ newScript.setAttribute(key.nodeName, key.nodeValue || ''),
+ );
+ newScript.text = script.text;
+ script.replaceWith(newScript);
+ }
+};
+
const runInlineScripts = (element: HTMLElement | DocumentFragment) => {
- const scripts = element.querySelectorAll(
- 'script:not([src])',
- ) as NodeListOf;
- scripts.forEach((script) => {
- if (!script.type || script.type === 'application/javascript') {
- const newScript = document.createElement('script');
- Array.from(script.attributes).forEach((key) =>
- newScript.setAttribute(key.nodeName, key.nodeValue || ''),
- );
- newScript.text = script.text;
- script.replaceWith(newScript);
- }
- });
+ const selector = 'script:not([src])';
+ if (element instanceof HTMLElement && element.matches(selector)) {
+ runScript(element as HTMLScriptElement);
+ } else {
+ const scripts = element.querySelectorAll(
+ selector,
+ ) as NodeListOf;
+ scripts.forEach(runScript);
+ }
};
export { runInlineScripts };
diff --git a/docs/releases/6.1.md b/docs/releases/6.1.md
index ea6bfb49b3..59c669ebfb 100644
--- a/docs/releases/6.1.md
+++ b/docs/releases/6.1.md
@@ -351,7 +351,7 @@ In the new approach, we no longer need to attach an inline script but instead us
### Removal of jQuery from base client-side Widget and BoundWidget classes
-The JavaScript base classes `Widget` and `BoundWidget` that provide client-side access to form widgets (see [](streamfield_widget_api)) no longer use jQuery. The `element` argument passed to the `BoundWidget` constructor, and the `input` property of `BoundWidget`, are now native DOM elements rather than jQuery collections. User code that extends these classes should be updated accordingly.
+The JavaScript base classes `Widget` and `BoundWidget` that provide client-side access to form widgets (see [](streamfield_widget_api)) no longer use jQuery. The `input` property of `BoundWidget` (previously a jQuery collection) is now a native DOM element, and the `element` argument passed to the `BoundWidget` constructor (previously a jQuery collection) is now passed as a native DOM element if the HTML representation consists of a single element, and an iterable of elements (`NodeList` or array) otherwise. User code that extends these classes should be updated accordingly.
### `window.URLify` deprecated