diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index 77ac9c274..f5d93ff70 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -64,7 +64,7 @@ const inject = { // if the injection shall add a history entry and HTML5 pushState // is missing, then don't initialize the injection. log.warn("HTML5 pushState is missing, aborting"); - return; + return $el; } $el.data("pat-inject", cfgs); @@ -350,6 +350,7 @@ const inject = { return false; } cfg.$target = this.createTarget(cfg.target); + cfg.$created_target = cfg.$target; } return true; }, @@ -453,7 +454,7 @@ const inject = { return $target; }, - _performInjection(target, $el, $source, cfg, trigger, title) { + _performInjection(target, $el, $source, cfg, trigger, $title) { /* Called after the XHR has succeeded and we have a new $source * element to inject. */ @@ -484,13 +485,13 @@ const inject = { // Now the injection actually happens. if (this._inject(trigger, source_nodes, target, cfg)) { // Update history - this._update_history(cfg, trigger, title); + this._update_history(cfg, trigger, $title); // Post-injection - this._afterInjection($el, $(source_nodes), cfg); + this._afterInjection($el, cfg.$created_target || $(source_nodes), cfg); } }, - _update_history(cfg, trigger, title) { + _update_history(cfg, trigger, $title) { // History support. if subform is submitted, append form params if (cfg.history !== "record" || !history?.pushState) { return; @@ -502,10 +503,10 @@ const inject = { } history.pushState({ url: url }, "", url); // Also inject title element if we have one - if (title) { + if ($title.length) { const title_el = document.querySelector("title"); if (title_el) { - this._inject(trigger, title, title_el, { + this._inject(trigger, $title, title_el, { action: "element", }); } @@ -584,14 +585,14 @@ const inject = { ]); /* pick the title source for dedicated handling later Title - if present - is always appended at the end. */ - let title; + let $title; if ( sources$ && sources$[sources$.length - 1] && sources$[sources$.length - 1][0] && sources$[sources$.length - 1][0].nodeName === "TITLE" ) { - title = sources$[sources$.length - 1]; + $title = sources$[sources$.length - 1]; } cfgs.forEach((cfg, idx1) => { const perform_inject = () => { @@ -603,7 +604,7 @@ const inject = { sources$[idx1], cfg, ev.target, - title + $title ); } } @@ -674,8 +675,8 @@ const inject = { // clean up for (const cfg of cfgs) { - if ("$injected" in cfg) { - cfg.$injected.remove(); + if ("$created_target" in cfg) { + cfg.$created_target.remove(); } cfg.$target.removeClass(cfg.loadingClass); $el.removeClass(cfg.executingClass); @@ -779,14 +780,14 @@ const inject = { } }, - _inject(trigger, source, target, cfg) { + _inject(trigger, source_nodes, target, cfg) { if (cfg.source === "none") { // Special case. Clear the target after ajax call. target.replaceWith(""); return true; } - if (source.length === 0) { - log.warn("Aborting injection, source not found:", source); + if (source_nodes.length === 0) { + log.warn("Aborting injection, source not found:", source_nodes); $(trigger).trigger("pat-inject-missingSource", { url: cfg.url, selector: cfg.source, @@ -816,7 +817,7 @@ const inject = { }[cfg.action]; // Inject the content HERE! - target[method](...source); + target[method](...source_nodes); return true; }, @@ -966,8 +967,11 @@ const inject = { let clean_html = html .replace(/)<[^<]*)*<\/script>/gi, "") .replace(/)<[^<]*)*<\/head>/gi, "") + .replace(/]*?)>/gi, "") + .replace(/<\/html([^>]*?)>/gi, "") .replace(/]*?)>/gi, '
') .replace(/<\/body([^>]*?)>/gi, "
"); + if (title && title.length == 2) { clean_html = title[0] + clean_html; } @@ -1108,7 +1112,8 @@ const inject = { sources(cfgs, data) { const sources = cfgs.map((cfg) => cfg.source); sources.push("title"); - return this._sourcesFromHtml(data, cfgs[0].url, sources); + const result = this._sourcesFromHtml(data, cfgs[0].url, sources); + return result; }, }, }, diff --git a/src/pat/inject/inject.test.js b/src/pat/inject/inject.test.js index 82a04b161..4acec02e5 100644 --- a/src/pat/inject/inject.test.js +++ b/src/pat/inject/inject.test.js @@ -1557,12 +1557,191 @@ describe("pat-inject", function () { expect(catched).toBe(true); expect(pattern.execute).toHaveBeenCalled(); - }); - }); }); }); + + describe("9.3 - inject with not existing target element", function () { + let spy_ajax; + + beforeEach(function () { + spy_ajax = jest.spyOn($, "ajax").mockImplementation(() => deferred); + }); + + afterEach(function () { + spy_ajax.mockRestore(); + }); + + it("9.3.1 - Create non-existing target", async function () { + // Test a real-world scenario with modals where the target + // element does not exist and the source is implicitly defined + // via the href attribue. + // NOTE: pat-modal does the injecton part on it's own, so you + // normally use the `pat-modal` class and do not specify a + // target. + // For examples, see the pat-modal pattern. + + document.body.innerHTML = ` + link + `; + + answer(` + + +
+ +

hello

+
+
+ + + `); + + const inject = document.querySelector(".pat-inject"); + + pattern.init($(inject)); + await utils.timeout(1); // wait a tick for async to settle. + + inject.click(); + + await utils.timeout(1); // wait a tick for async to settle. + + console.log(document.body.innerHTML); + const modal = document.querySelector("#pat-modal"); + expect(modal).toBeTruthy(); + expect(modal.innerHTML.replace(/\s/g, "")).toBe( + "

hello

" + ); + }); + + it("9.3.2 - Does not create non-existing target without a target specifier", async function () { + document.body.innerHTML = ` + link + `; + + answer(` + + +
+ +

hello

+
+
+ + + `); + + const inject = document.querySelector(".pat-inject"); + + pattern.init($(inject)); + await utils.timeout(1); // wait a tick for async to settle. + + inject.click(); + + await utils.timeout(1); // wait a tick for async to settle. + + console.log(document.body.innerHTML); + const modal = document.querySelector("#pat-modal"); + expect(modal).toBeFalsy(); + }); + }); + + describe("9.4 - injecton of the title element.", function () { + let spy_ajax; + + beforeEach(function () { + spy_ajax = jest.spyOn($, "ajax").mockImplementation(() => deferred); + }); + + afterEach(function () { + spy_ajax.mockRestore(); + }); + + it("9.4.1 - Injects a title element with history:record", async function () { + document.head.innerHTML = ` + test + `; + document.body.innerHTML = ` + link + `; + + answer(` + + + hello + + + OK + + + `); + + const inject = document.querySelector(".pat-inject"); + + pattern.init($(inject)); + await utils.timeout(1); // wait a tick for async to settle. + + inject.click(); + + await utils.timeout(1); // wait a tick for async to settle. + + expect(document.body.textContent.trim()).toBe("OK"); + + const title = document.head.querySelector("title"); + expect(title).toBeTruthy(); + expect(title.textContent.trim()).toBe("hello"); + }); + + it("9.4.2 - Does not inject a title element without history:record", async function () { + document.head.innerHTML = ` + test + `; + document.body.innerHTML = ` + link + `; + + answer(` + + + hello + + + OK + + + `); + + const inject = document.querySelector(".pat-inject"); + + pattern.init($(inject)); + await utils.timeout(1); // wait a tick for async to settle. + + inject.click(); + + await utils.timeout(1); // wait a tick for async to settle. + + expect(document.body.textContent.trim()).toBe("OK"); + + const title = document.head.querySelector("title"); + expect(title).toBeTruthy(); + expect(title.textContent.trim()).toBe("test"); // Old title + }); + }); }); describe("10 - Error handling", () => {