diff --git a/package.json b/package.json
index 6cbe7b6d42..17a873f034 100644
--- a/package.json
+++ b/package.json
@@ -36,9 +36,7 @@
"datatables.net-select-bs5": "^3.1.3",
"dropzone": "6.0.0-beta.2",
"jqtree": "^1.8.11",
- "jquery": "3.7.1",
- "jquery-form": "4.3.0",
- "jquery.browser": "0.1.0",
+ "jquery": "^4.0.0",
"js-cookie": "^3.0.5",
"select2": "github:ivaynberg/select2#95a977f674b6938af55ec5f28b7772df93786c5c",
"sortablejs": "^1.15.7",
@@ -66,7 +64,7 @@
"resolutions": {
"@patternslib/patternslib": "9.10.4",
"backbone": "1.6.1",
- "jquery": "3.7.1",
+ "jquery": "4.0.0",
"sass": "~1.77.8",
"underscore": "1.13.8"
},
diff --git a/src/index.js b/src/index.js
index e4684e3fbd..8aa82fbb13 100644
--- a/src/index.js
+++ b/src/index.js
@@ -29,6 +29,20 @@ async function register_global_libraries() {
// Register jQuery globally
const jquery = (await import("jquery")).default;
+
+ // BBB: jQuery4 backports for select2
+ jquery.isFunction = function (obj) {
+ return typeof obj === "function";
+ };
+ jquery.isArray = function (obj) {
+ return Array.isArray
+ ? Array.isArray(obj)
+ : Object.prototype.toString.call(obj) === "[object Array]";
+ };
+ jquery.trim = function (str) {
+ return str == null ? "" : String.prototype.trim.call(str);
+ };
+
window.jQuery = jquery;
window.$ = jquery;
}
diff --git a/src/pat/manageportlets/manageportlets.js b/src/pat/manageportlets/manageportlets.js
index d15d360706..e009f5f953 100644
--- a/src/pat/manageportlets/manageportlets.js
+++ b/src/pat/manageportlets/manageportlets.js
@@ -17,7 +17,6 @@ export default Base.extend({
isModal: false,
dirty: false,
init: async function () {
- (await import("jquery-form")).default;
var that = this;
var $modal = that.$el.parents(".plone-modal");
if ($modal.length === 1) {
diff --git a/src/pat/modal/modal.js b/src/pat/modal/modal.js
index d174cae264..12f77809df 100644
--- a/src/pat/modal/modal.js
+++ b/src/pat/modal/modal.js
@@ -7,8 +7,6 @@ import dom from "@patternslib/patternslib/src/core/dom";
import utils from "../../core/utils";
import _t from "../../core/i18n-wrapper";
-import "jquery-form";
-
export default Base.extend({
name: "plone-modal",
trigger: ".pat-plone-modal",
@@ -158,8 +156,8 @@ export default Base.extend({
self[actionOptions.modalFunction]();
// handle event on input/button using jquery.form library
} else if (
- $.nodeName($action[0], "input") ||
- $.nodeName($action[0], "button") ||
+ ($action[0].nodeName.toLowerCase() == "input") ||
+ ($action[0].nodeName.toLowerCase() == "button") ||
options.isForm === true
) {
self.options.handleFormAction.apply(self, [
@@ -231,29 +229,53 @@ export default Base.extend({
$form.trigger("submit");
self.loading.show(false);
- $form.ajaxSubmit({
- timeout: options.timeout,
- data: extraData,
- url: url,
- error: function (xhr, textStatus, errorStatus) {
- self.loading.hide();
- if (textStatus === "timeout" && options.onTimeout) {
- options.onTimeout.apply(self, xhr, errorStatus);
- // on "error", "abort", and "parsererror"
- } else if (options.onError) {
- if (typeof options.onError === "string") {
- window[options.onError](xhr, textStatus, errorStatus);
- } else {
- options.onError(xhr, textStatus, errorStatus);
- }
- } else {
- // window.alert(_t('There was an error submitting the form.'));
- console.log("error happened", textStatus, " do something");
- }
- self.emit("formActionError", [xhr, textStatus, errorStatus]);
- },
- success: function (response, state, xhr, form) {
+
+ // Use native fetch API with FormData
+ const formData = new FormData($form[0]);
+
+ // Add extra data from the clicked action
+ for (const key in extraData) {
+ if (extraData[key]) {
+ formData.append(key, extraData[key]);
+ }
+ }
+
+ // Setup timeout using AbortController
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), options.timeout);
+
+ fetch(url, {
+ method: $form.attr("method") || "POST",
+ body: formData,
+ signal: controller.signal,
+ credentials: "same-origin",
+ })
+ .then((response) => {
+ clearTimeout(timeoutId);
+
+ // Create a mock xhr object for compatibility with existing callbacks
+ const mockXhr = {
+ status: response.status,
+ statusText: response.statusText,
+ getResponseHeader: (header) => response.headers.get(header),
+ getAllResponseHeaders: () => {
+ let headers = "";
+ response.headers.forEach((value, key) => {
+ headers += `${key}: ${value}\r\n`;
+ });
+ return headers;
+ },
+ };
+
+ return response.text().then((text) => ({
+ response: text,
+ state: "success",
+ xhr: mockXhr,
+ }));
+ })
+ .then(({ response, state, xhr }) => {
self.loading.hide();
+
// if error is found (NOTE: check for both the portal errors
// and the form field-level errors)
if (
@@ -267,10 +289,10 @@ export default Base.extend({
response,
state,
xhr,
- form
+ $form[0]
);
} else {
- options.onFormError(self, response, state, xhr, form);
+ options.onFormError(self, response, state, xhr, $form[0]);
}
} else {
self.redraw(response, patternOptions);
@@ -292,9 +314,9 @@ export default Base.extend({
if (options.onSuccess) {
if (typeof options.onSuccess === "string") {
- window[options.onSuccess](self, response, state, xhr, form);
+ window[options.onSuccess](self, response, state, xhr, $form[0]);
} else {
- options.onSuccess(self, response, state, xhr, form);
+ options.onSuccess(self, response, state, xhr, $form[0]);
}
}
@@ -307,9 +329,37 @@ export default Base.extend({
self.reloadWindow();
}
}
- self.emit("formActionSuccess", [response, state, xhr, form]);
- },
- });
+ self.emit("formActionSuccess", [response, state, xhr, $form[0]]);
+ })
+ .catch((error) => {
+ clearTimeout(timeoutId);
+ self.loading.hide();
+
+ const textStatus = error.name === "AbortError" ? "timeout" : "error";
+ const errorStatus = error.message;
+
+ // Create a mock xhr object for error callbacks
+ const mockXhr = {
+ status: 0,
+ statusText: textStatus,
+ responseText: "",
+ };
+
+ if (textStatus === "timeout" && options.onTimeout) {
+ options.onTimeout.apply(self, [mockXhr, errorStatus]);
+ // on "error", "abort", and "parsererror"
+ } else if (options.onError) {
+ if (typeof options.onError === "string") {
+ window[options.onError](mockXhr, textStatus, errorStatus);
+ } else {
+ options.onError(mockXhr, textStatus, errorStatus);
+ }
+ } else {
+ // window.alert(_t('There was an error submitting the form.'));
+ console.log("error happened", textStatus, " do something");
+ }
+ self.emit("formActionError", [mockXhr, textStatus, errorStatus]);
+ });
},
handleLinkAction: function ($action, options, patternOptions) {
var self = this;
@@ -531,7 +581,7 @@ export default Base.extend({
import("./modal.scss");
var self = this;
- self.options.loadLinksWithinModal = $.parseJSON(
+ self.options.loadLinksWithinModal = JSON.parse(
self.options.loadLinksWithinModal
);
@@ -652,7 +702,7 @@ export default Base.extend({
self.$wrapper.addClass("image-modal");
var src = self.$el.attr("href");
var srcset = self.$el.attr("data-modal-srcset") || "";
- var title = $.trim(self.$el.context.innerText) || "Image";
+ var title = (self.$el[0]?.innerText || "").trim() || "Image";
// XXX aria?
self.$raw = $(
"
@@ -159,11 +153,9 @@ describe("Querystring", function () {
).toEqual(3);
// check for correct initialized values
- expect($(".querystring-sort-wrapper select").val()).toEqual("start");
+ expect(document.querySelector(".querystring-sort-wrapper select").value).toEqual("start");
expect(
- $(
- ".querystring-sort-wrapper .querystring-sortreverse input[type='checkbox']"
- )[0].checked
+ document.querySelectorAll(".querystring-sort-wrapper .querystring-sortreverse input[type='checkbox']")[0].checked
).toBeTruthy();
global.fetch.mockClear();
diff --git a/src/pat/relateditems/relateditems.js b/src/pat/relateditems/relateditems.js
index 203664aa80..fa0792753b 100644
--- a/src/pat/relateditems/relateditems.js
+++ b/src/pat/relateditems/relateditems.js
@@ -7,8 +7,8 @@ import registry from "@patternslib/patternslib/src/core/registry";
import Select2 from "../select2/select2";
const KEY = {
- LEFT: 37,
- RIGHT: 39,
+ LEFT: "ArrowLeft",
+ RIGHT: "ArrowRight",
};
export default Base.extend({
@@ -702,11 +702,11 @@ export default Base.extend({
return;
}
- if (event.which === KEY.LEFT || event.which === KEY.RIGHT) {
+ if (event.key === KEY.LEFT || event.key === KEY.RIGHT) {
event.stopPropagation();
const selectorContext =
- event.which === KEY.LEFT
+ event.key === KEY.LEFT
? ".pat-relateditems-result.one-level-up"
: ".select2-highlighted";
diff --git a/src/pat/select2/select2.test.js b/src/pat/select2/select2.test.js
index 346536c981..b47f9656a8 100644
--- a/src/pat/select2/select2.test.js
+++ b/src/pat/select2/select2.test.js
@@ -226,8 +226,8 @@ describe("Select2", function () {
var $results = $("li.select2-search-choice");
expect($results.length).toEqual(2);
- expect($.trim($results.eq(0).text())).toEqual("Yellow");
- expect($.trim($results.eq(1).text())).toEqual("Red");
+ expect($results.eq(0).text().trim()).toEqual("Yellow");
+ expect($results.eq(1).text().trim()).toEqual("Red");
var firstElem = $results.eq(0);
// css class is set and proxy is created when starting to drag
@@ -330,7 +330,7 @@ describe("Select2", function () {
expect($("#test-select2").val()).toEqual("1;3");
var $results = $("li.select2-search-choice");
expect($results.length).toEqual(2);
- expect($.trim($results.eq(0).text())).toEqual("One");
- expect($.trim($results.eq(1).text())).toEqual("Three");
+ expect($results.eq(0).text().trim()).toEqual("One");
+ expect($results.eq(1).text().trim()).toEqual("Three");
});
});
diff --git a/src/pat/structure/structure.test.js b/src/pat/structure/structure.test.js
index dfd23b537b..3c01378461 100644
--- a/src/pat/structure/structure.test.js
+++ b/src/pat/structure/structure.test.js
@@ -42,12 +42,7 @@ function getQueryVariable(url, variable) {
var extraDataJsonItem = null;
-const mockFetch =
- (json = {}) =>
- () =>
- Promise.resolve({
- json: () => Promise.resolve(json),
- });
+const mockFetch = global.mockFetch;
/* ==========================
TEST: AppView constructor internal attribute/object correctness
diff --git a/src/pat/tinymce/tinymce.test.js b/src/pat/tinymce/tinymce.test.js
index 80985da35d..e5c3e3ff69 100644
--- a/src/pat/tinymce/tinymce.test.js
+++ b/src/pat/tinymce/tinymce.test.js
@@ -59,7 +59,7 @@ describe("TinyMCE", function () {
for (const val of vars) {
const pair = val.split("=");
if (decodeURIComponent(pair[0]) === "query") {
- query = $.parseJSON(decodeURIComponent(pair[1]));
+ query = JSON.parse(decodeURIComponent(pair[1]));
}
}
const results = [];
diff --git a/src/pat/tree/tree.js b/src/pat/tree/tree.js
index 325b21a30a..5295bd291a 100644
--- a/src/pat/tree/tree.js
+++ b/src/pat/tree/tree.js
@@ -1,4 +1,3 @@
-import $ from "jquery";
import Base from "@patternslib/patternslib/src/core/base";
import utils from "../../core/utils";
@@ -39,20 +38,29 @@ export default Base.extend({
}
if (self.options.data && typeof self.options.data === "string") {
- self.options.data = $.parseJSON(self.options.data);
+ self.options.data = JSON.parse(self.options.data);
}
if (self.options.onLoad !== null) {
// delay generating tree...
- var options = $.extend({}, self.options);
+ const options = { ...self.options };
+
+ try {
+ const response = await fetch(options.dataUrl);
+
+ if (!response.ok) {
+ throw new Error("HTTP error " + response.status);
+ }
+
+ const data = await response.json();
- $.getJSON(options.dataUrl, function (data) {
options.data = data;
delete options.dataUrl;
+
self.tree = self.$el.tree(options);
self.options.onLoad(self);
- }).fail(function (response) { // eslint-disable-line no-unused-vars
- console.log("failed to load json data");
- });
+ } catch (error) {
+ console.log(`failed to load json data: ${error}`);
+ }
} else {
self.tree = self.$el.tree(self.options);
}
diff --git a/src/pat/upload/upload.js b/src/pat/upload/upload.js
index 920ee5e9e5..b68ded4831 100644
--- a/src/pat/upload/upload.js
+++ b/src/pat/upload/upload.js
@@ -177,7 +177,7 @@ export default Base.extend({
// upload pattern, e.g. the TinyMCE pattern's link plugin.
var data;
try {
- data = $.parseJSON(response);
+ data = JSON.parse(response);
} catch {
data = response;
}
diff --git a/src/setup-tests.js b/src/setup-tests.js
index fa2a2ef059..1f93252bc9 100644
--- a/src/setup-tests.js
+++ b/src/setup-tests.js
@@ -1,5 +1,37 @@
// Extra test setup.
+// Mock fetch for jsdom environment which doesn't support it natively.
+// Default implementation returns an empty 200 response.
+if (!global.fetch) {
+ global.fetch = () =>
+ Promise.resolve({
+ status: 200,
+ statusText: "OK",
+ headers: {
+ get: () => null,
+ forEach: () => {},
+ },
+ json: () => Promise.resolve({}),
+ text: () => Promise.resolve(""),
+ });
+}
+
+// Factory for creating fetch mocks that return a specific JSON payload.
+// Usage in tests: global.fetch = jest.fn().mockImplementation(mockFetch({ ... }))
+global.mockFetch =
+ (json = {}) =>
+ () =>
+ Promise.resolve({
+ status: 200,
+ statusText: "OK",
+ headers: {
+ get: () => null,
+ forEach: () => {},
+ },
+ json: () => Promise.resolve(json),
+ text: () => Promise.resolve(JSON.stringify(json)),
+ });
+
// provide jquery
import jquery from "jquery";
global.$ = global.jQuery = jquery;
@@ -10,6 +42,19 @@ jquery.expr.pseudos.visible = function () {
return true;
};
+// BBB: jQuery4 backports for select2
+jquery.isFunction = function (obj) {
+ return typeof obj === "function";
+};
+jquery.isArray = function (obj) {
+ return Array.isArray
+ ? Array.isArray(obj)
+ : Object.prototype.toString.call(obj) === "[object Array]";
+};
+jquery.trim = function (str) {
+ return str == null ? "" : String.prototype.trim.call(str);
+};
+
// Do not output error messages
import logging from "@patternslib/patternslib/src/core/logging";
logging.setLevel(50);
@@ -31,5 +76,5 @@ import "css.escape";
// Add structuredClone polyfill for jsdom.
// See: https://github.com/jsdom/jsdom/issues/3363
global.structuredClone = val => {
- return JSON.parse(JSON.stringify(val))
+ return JSON.parse(JSON.stringify(val))
}
diff --git a/webpack.config.js b/webpack.config.js
index e07a594493..b36233a0b9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -73,11 +73,6 @@ module.exports = () => {
test: /[\\/]node_modules[\\/]select2.*[\\/]/,
chunks: "async",
},
- jquery_plugins: {
- name: "jquery_plugins",
- test: /[\\/]node_modules[\\/]jquery\..*[\\/]/,
- chunks: "async",
- },
},
},
},
@@ -145,7 +140,7 @@ module.exports = () => {
},
jquery: {
singleton: true,
- requiredVersion: package_json.dependencies["jquery"],
+ version: package_json.dependencies["jquery"],
eager: true,
},
},
@@ -179,7 +174,7 @@ module.exports = () => {
);
}
- //console.log(JSON.stringify(config, null, 4));
+ console.log(JSON.stringify(config, null, 4));
return config;
};