Refactor BoundWidget to accept an iterable of elements

pull/11966/head
Matt Westcott 2024-05-20 18:17:11 +01:00
rodzic 0cc274f36a
commit 0ae74677b2
4 zmienionych plików z 61 dodań i 28 usunięć

Wyświetl plik

@ -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,

Wyświetl plik

@ -133,7 +133,7 @@ describe('telepath: wagtail.widgets.Widget with multiple top-level nodes', () =>
widgetDef = window.telepath.unpack({
_type: 'wagtail.widgets.Widget',
_args: [
'<input type="text" name="__NAME__" maxlength="255" id="__ID__"><button data-button-state="idle">Click me</button><script>document.getElementById("__ID__").className = "custom-class";</script>',
'<!-- here comes a widget --><input type="text" name="__NAME__" maxlength="255" id="__ID__"><button data-button-state="idle">Click me</button><script>document.getElementById("__ID__").className = "custom-class";</script>',
'__ID__',
],
});

Wyświetl plik

@ -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<HTMLScriptElement>;
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<HTMLScriptElement>;
scripts.forEach(runScript);
}
};
export { runInlineScripts };

Wyświetl plik

@ -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