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 = $( "

" + diff --git a/src/pat/querystring/querystring.test.js b/src/pat/querystring/querystring.test.js index 834742840f..2d00d910d5 100644 --- a/src/pat/querystring/querystring.test.js +++ b/src/pat/querystring/querystring.test.js @@ -1,14 +1,8 @@ import "./querystring"; -import $ from "jquery"; import registry from "@patternslib/patternslib/src/core/registry"; import utils from "@patternslib/patternslib/src/core/utils"; -const mockFetch = - (json = {}) => - () => - Promise.resolve({ - json: () => Promise.resolve(json), - }); +const mockFetch = global.mockFetch; describe("Querystring", function () { beforeEach(() => { @@ -16,7 +10,7 @@ describe("Querystring", function () {
+ }'>

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