diff --git a/client/src/entrypoints/admin/telepath/__snapshots__/widgets.test.js.snap b/client/src/entrypoints/admin/telepath/__snapshots__/widgets.test.js.snap
index c8a10974dd..465c79ed4f 100644
--- a/client/src/entrypoints/admin/telepath/__snapshots__/widgets.test.js.snap
+++ b/client/src/entrypoints/admin/telepath/__snapshots__/widgets.test.js.snap
@@ -1,7 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`telepath: wagtail.widgets.CheckboxInput it renders correctly 1`] = `""`;
-
exports[`telepath: wagtail.widgets.RadioSelect it renders correctly 1`] = `
"
-
diff --git a/client/src/entrypoints/admin/telepath/widgets.js b/client/src/entrypoints/admin/telepath/widgets.js
index 79dc6891ff..201e0f8bd5 100644
--- a/client/src/entrypoints/admin/telepath/widgets.js
+++ b/client/src/entrypoints/admin/telepath/widgets.js
@@ -1,4 +1,3 @@
-/* global $ */
import { gettext } from '../../../utils/gettext';
class BoundWidget {
@@ -10,8 +9,11 @@ class BoundWidget {
parentCapabilities,
options,
) {
- var selector = ':input[name="' + name + '"]';
- this.input = element.find(selector).addBack(selector); // find, including element itself
+ var selector = ':is(input,select,textarea,button)[name="' + name + '"]';
+ // find, including element itself
+ this.input = element.matches(selector)
+ ? element
+ : element.querySelector(selector);
this.idForLabel = idForLabel;
this.setState(initialState);
this.parentCapabilities = parentCapabilities || new Map();
@@ -19,15 +21,15 @@ class BoundWidget {
}
getValue() {
- return this.input.val();
+ return this.input.value;
}
getState() {
- return this.input.val();
+ return this.input.value;
}
setState(state) {
- this.input.val(state);
+ this.input.value = state;
}
getTextLabel(opts) {
@@ -65,17 +67,31 @@ class Widget {
parentCapabilities,
options = {},
) {
- var html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id);
- var idForLabel = this.idPattern.replace(/__ID__/g, id);
- var dom = $(html);
+ 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 */
+ const tempContainer = document.createElement('div');
+ tempContainer.innerHTML = html.trim();
+ const dom = tempContainer.firstChild;
+
+ /* replace the placeholder with the new element */
+ placeholder.replaceWith(dom);
+
+ /* execute any scripts in the new element */
+ dom.querySelectorAll('script').forEach((script) => {
+ const newScript = document.createElement('script');
+ newScript.text = script.text;
+ script.replaceWith(newScript);
+ });
// Add any extra attributes we received to the HTML of the widget
if (typeof options?.attributes === 'object') {
Object.entries(options.attributes).forEach(([key, value]) => {
- dom.attr(key, value);
+ dom.setAttribute(key, value);
});
}
- $(placeholder).replaceWith(dom);
+
// eslint-disable-next-line new-cap
return new this.boundWidgetClass(
dom,
@@ -91,16 +107,15 @@ window.telepath.register('wagtail.widgets.Widget', Widget);
class BoundCheckboxInput extends BoundWidget {
getValue() {
- return this.input.is(':checked');
+ return this.input.checked;
}
getState() {
- return this.input.is(':checked');
+ return this.input.checked;
}
setState(state) {
- // if false, set attribute value to null to remove it
- this.input.attr('checked', state || null);
+ this.input.checked = state;
}
}
@@ -119,19 +134,24 @@ class BoundRadioSelect {
}
getValue() {
- return this.element.find(this.selector).val();
+ return this.element.querySelector(this.selector)?.value;
}
getState() {
- return this.element.find(this.selector).val();
+ return this.element.querySelector(this.selector)?.value;
}
setState(state) {
- this.element.find('input[name="' + this.name + '"]').val([state]);
+ const inputs = this.element.querySelectorAll(
+ 'input[name="' + this.name + '"]',
+ );
+ for (let i = 0; i < inputs.length; i += 1) {
+ inputs[i].checked = inputs[i].value === state;
+ }
}
focus() {
- this.element.find('input[name="' + this.name + '"]').focus();
+ this.element.querySelector('input[name="' + this.name + '"]')?.focus();
}
}
@@ -142,7 +162,8 @@ window.telepath.register('wagtail.widgets.RadioSelect', RadioSelect);
class BoundSelect extends BoundWidget {
getTextLabel() {
- return this.input.find(':selected').text();
+ const selectedOption = this.input.selectedOptions[0];
+ return selectedOption ? selectedOption.text : '';
}
}
diff --git a/client/src/entrypoints/admin/telepath/widgets.test.js b/client/src/entrypoints/admin/telepath/widgets.test.js
index 55fef28555..e1085025d2 100644
--- a/client/src/entrypoints/admin/telepath/widgets.test.js
+++ b/client/src/entrypoints/admin/telepath/widgets.test.js
@@ -46,10 +46,14 @@ describe('telepath: wagtail.widgets.Widget', () => {
test('getValue() returns the current value', () => {
expect(boundWidget.getValue()).toBe('The Value');
+ document.querySelector('input').value = 'New Value';
+ expect(boundWidget.getValue()).toBe('New Value');
});
test('getState() returns the current state', () => {
expect(boundWidget.getState()).toBe('The Value');
+ document.querySelector('input').value = 'New Value';
+ expect(boundWidget.getState()).toBe('New Value');
});
test('setState() changes the current state', () => {
@@ -87,6 +91,37 @@ describe('telepath: wagtail.widgets.Widget', () => {
});
});
+describe('telepath: wagtail.widgets.Widget with inline JS', () => {
+ let boundWidget;
+ let widgetDef;
+
+ beforeEach(() => {
+ // Create a placeholder to render the widget
+ document.body.innerHTML = '';
+
+ widgetDef = window.telepath.unpack({
+ _type: 'wagtail.widgets.Widget',
+ _args: [
+ '',
+ '__ID__',
+ ],
+ });
+ boundWidget = widgetDef.render(
+ document.getElementById('placeholder'),
+ 'the-name',
+ 'the-id',
+ 'The Value',
+ );
+ });
+
+ test('it renders correctly', () => {
+ expect(document.body.querySelector('input').outerHTML).toBe(
+ '',
+ );
+ expect(document.querySelector('input').value).toBe('The Value');
+ });
+});
+
describe('telepath: wagtail.widgets.RadioSelect', () => {
let boundWidget;
@@ -141,9 +176,8 @@ describe('telepath: wagtail.widgets.RadioSelect', () => {
test('focus() focuses the text input', () => {
boundWidget.focus();
- // Note: This widget always focuses the last element
expect(document.activeElement).toBe(
- document.querySelector('input[value="coffee"]'),
+ document.querySelector('input[value="tea"]'),
);
});
});
@@ -168,7 +202,6 @@ describe('telepath: wagtail.widgets.CheckboxInput', () => {
});
test('it renders correctly', () => {
- expect(document.body.innerHTML).toMatchSnapshot();
expect(document.querySelector('input[id="id-sugar"]').checked).toBe(true);
});
diff --git a/docs/releases/6.1.md b/docs/releases/6.1.md
index 00dbb1f4d3..a4eb6b600d 100644
--- a/docs/releases/6.1.md
+++ b/docs/releases/6.1.md
@@ -276,3 +276,7 @@ In the new approach, we no longer need to attach an inline script but instead us
data-w-block-arguments-value="[{value_json},{error_json}]"
>
```
+
+### 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.