From 92fb40017c335f8c4c54a1aacacf81a4e4d2fc1d Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 10 Dec 2020 11:37:21 +0100 Subject: [PATCH 01/11] core dom: Add create_from_string to create a DOM Element from a string. --- CHANGES.md | 1 + src/core/dom.js | 8 ++++++++ src/core/dom.test.js | 29 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 41559e0b1..47b71f562 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,6 +48,7 @@ - core dom: Add ``find_parents`` to find all parents of an element matching a CSS selector. - core dom: Add ``find_scoped`` to search for elements matching the given selector within the current scope of the given element - core dom: Add ``is_visible`` to check if an element is visible or not. +- core dom: Add ``create_from_string`` to create a DOM Element from a string. unless an ``id`` selector is given - in that case the search is done globally. - pat date picker: Support updating a date if it is before another dependent date. - pat tabs: Refactor based on ``ResizeObserver`` and fix problems calculating the with with transitions. diff --git a/src/core/dom.js b/src/core/dom.js index 3514a1ed0..66f0c9052 100644 --- a/src/core/dom.js +++ b/src/core/dom.js @@ -80,6 +80,13 @@ const is_visible = (el) => { return el.offsetWidth > 0 && el.offsetHeight > 0; }; +const create_from_string = (string) => { + // Create a DOM element from a string. + const div = document.createElement("div"); + div.innerHTML = string.trim(); + return div.firstChild; +}; + const dom = { toNodeArray: toNodeArray, querySelectorAllAndMe: querySelectorAllAndMe, @@ -89,6 +96,7 @@ const dom = { find_parents: find_parents, find_scoped: find_scoped, is_visible: is_visible, + create_from_string: create_from_string, }; export default dom; diff --git a/src/core/dom.test.js b/src/core/dom.test.js index baf851fa4..b0f178262 100644 --- a/src/core/dom.test.js +++ b/src/core/dom.test.js @@ -249,4 +249,33 @@ describe("core.dom tests", () => { done(); }); }); + + describe("create_from_string", () => { + it("Creates a DOM element from a string", (done) => { + let res = dom.create_from_string(` +
+ does work. +
`); + + expect(res.getAttribute("id")).toEqual("section1"); + expect(res.querySelector("span.yo").textContent).toEqual( + "does work." + ); + + res = dom.create_from_string(` +
+
+ `); + // Section 2 is not returned. + expect(res.getAttribute("id")).toEqual("section1"); + + // TD elements or others which can not be direct children of a + //
are not yet supported. + // Also see: https://stackoverflow.com/a/494348/1337474 + res = dom.create_from_string(``); + expect(res).toBeFalsy(); + + done(); + }); + }); }); From 8f501fd6708e2d890f301169b891e3e144a012b1 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 15 Dec 2020 12:41:41 +0100 Subject: [PATCH 02/11] core utils debounce: Remove unnecessary var --- src/core/utils.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/utils.js b/src/core/utils.js index 5ae1eef47..02c0c238c 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -536,9 +536,8 @@ const debounce = (func, ms) => { return function () { clearTimeout(timer); const args = arguments; - const context = this; timer = setTimeout(() => { - func.apply(context, args); + func.apply(this, args); }, ms); }; }; From 0ad45228db504c894a82fdf4026cd6a65653c0c8 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 9 Dec 2020 15:09:57 +0100 Subject: [PATCH 03/11] pat-inject: Modernize - functions --- src/pat/inject/inject.js | 174 +++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 100 deletions(-) diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index e2ba97a99..99fee6c0f 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -52,12 +52,10 @@ const inject = { name: "inject", trigger: ".raptor-ui .ui-button.pat-inject, a.pat-inject, form.pat-inject, .pat-subform.pat-inject", - init: function inject_init($el, opts) { + init($el, opts) { var cfgs = inject.extractConfig($el, opts); if ( - cfgs.some(function (e) { - return e.history === "record"; - }) && + cfgs.some((e) => e.history === "record") && !("pushState" in history) ) { // if the injection shall add a history entry and HTML5 pushState @@ -88,7 +86,7 @@ const inject = { } } if (cfgs[0].pushMarker) { - $("body").on("push", function (event, data) { + $("body").on("push", (event, data) => { console.log("received push message: " + data); if (data == cfgs[0].pushMarker) { console.log("re-injecting " + data); @@ -107,15 +105,15 @@ const inject = { clearTimeout(timer); }; - var onInteraction = utils.debounce(function onInteraction() { + var onInteraction = utils.debounce(() => { clearTimeout(timer); timer = setTimeout(onTimeout, cfgs[0].trigger); }, timeout); const unsub = () => { - ["scroll", "resize"].forEach(function (e) { - window.removeEventListener(e, onInteraction); - }); + ["scroll", "resize"].forEach((e) => + window.removeEventListener(e, onInteraction) + ); [ "click", "keypress", @@ -123,16 +121,16 @@ const inject = { "mousemove", "touchstart", "touchend", - ].forEach(function (e) { - document.removeEventListener(e, onInteraction); - }); + ].forEach((e) => + document.removeEventListener(e, onInteraction) + ); }; onInteraction(); - ["scroll", "resize"].forEach(function (e) { - window.addEventListener(e, onInteraction); - }); + ["scroll", "resize"].forEach((e) => + window.addEventListener(e, onInteraction) + ); [ "click", "keypress", @@ -140,13 +138,11 @@ const inject = { "mousemove", "touchstart", "touchend", - ].forEach(function (e) { - document.addEventListener(e, onInteraction); - }); + ].forEach((e) => document.addEventListener(e, onInteraction)); } else { switch (cfgs[0].trigger) { case "default": - cfgs.forEach(function (cfg) { + cfgs.forEach((cfg) => { if (cfg.delay) { cfg.processDelay = cfg.delay; } @@ -209,20 +205,20 @@ const inject = { return $el; }, - destroy: function inject_destroy($el) { + destroy($el) { $el.off(".pat-inject"); $el.data("pat-inject", null); return $el; }, - onTrigger: function inject_onTrigger(ev) { + onTrigger(ev) { /* Injection has been triggered, either via form submission or a * link has been clicked. */ var cfgs = $(this).data("pat-inject"), $el = $(this); if ($el.is("form")) { - $(cfgs).each(function (i, v) { + $(cfgs).each((i, v) => { v.params = $.param($el.serializeArray()); }); } @@ -231,7 +227,7 @@ const inject = { inject.execute(cfgs, $el); }, - onFormActionSubmit: function inject_onFormActionSubmit(ev) { + onFormActionSubmit(ev) { ajax.onClickSubmit(ev); // make sure the submitting button is sent with the form var $button = $(ev.target), @@ -241,7 +237,7 @@ const inject = { $cfg_node = $button.closest("[data-pat-inject]"), cfgs = inject.extractConfig($cfg_node, opts); - $(cfgs).each(function (i, v) { + $(cfgs).each((i, v) => { v.params = $.param($form.serializeArray()); }); @@ -250,14 +246,14 @@ const inject = { inject.execute(cfgs, $form); }, - submitSubform: function inject_submitSubform($sub) { + submitSubform($sub) { /* This method is called from pat-subform */ var $el = $sub.parents("form"), cfgs = $sub.data("pat-inject"); // store the params of the subform in the config, to be used by history - $(cfgs).each(function (i, v) { + $(cfgs).each((i, v) => { v.params = $.param($sub.serializeArray()); }); @@ -269,11 +265,11 @@ const inject = { inject.execute(cfgs, $el); }, - extractConfig: function inject_extractConfig($el, opts) { + extractConfig($el, opts) { opts = $.extend({}, opts); var cfgs = parser.parse($el, opts, true); - cfgs.forEach(function inject_extractConfig_each(cfg) { + cfgs.forEach((cfg) => { // opts and cfg have priority, fallback to href/action cfg.url = opts.url || @@ -308,24 +304,24 @@ const inject = { return cfgs; }, - elementIsDirty: function (m) { + elementIsDirty(m) { /* Check whether the passed in form element contains a value. */ - var data = $.map(m.find(":input:not(select)"), function (i) { + var data = $.map(m.find(":input:not(select)"), (i) => { var val = $(i).val(); return Boolean(val) && val !== $(i).attr("placeholder"); }); return $.inArray(true, data) !== -1; }, - askForConfirmation: function inject_askForConfirmation(cfgs) { + askForConfirmation(cfgs) { /* If configured to do so, show a confirmation dialog to the user. * This is done before attempting to perform injection. */ var should_confirm = false, message; - _.each(cfgs, function (cfg) { + _.each(cfgs, (cfg) => { var _confirm = false; if (cfg.confirm == "always") { _confirm = true; @@ -349,7 +345,7 @@ const inject = { return true; }, - ensureTarget: function inject_ensureTarget(cfg, $el) { + ensureTarget(cfg, $el) { /* Make sure that a target element exists and that it's assigned to * cfg.$target. */ @@ -370,7 +366,7 @@ const inject = { return true; }, - verifySingleConfig: function inject_verifySingleonfig($el, url, cfg) { + verifySingleConfig($el, url, cfg) { /* Verify one of potentially multiple configs (i.e. argument lists). * * Extract modifiers such as ::element or ::after. @@ -396,7 +392,7 @@ const inject = { return true; }, - verifyConfig: function inject_verifyConfig(cfgs, $el) { + verifyConfig(cfgs, $el) { /* Verify and post-process all the configurations. * Each "config" is an arguments list separated by the && * combination operator. @@ -411,7 +407,7 @@ const inject = { ); }, - listenForFormReset: function (cfg) { + listenForFormReset(cfg) { /* if pat-inject is used to populate target in some form and when * Cancel button is pressed (this triggers reset event on the * form) you would expect to populate with initial placeholder @@ -425,13 +421,13 @@ const inject = { cfg.$target.data("initial-value") === undefined ) { cfg.$target.data("initial-value", cfg.$target.html()); - $form.on("reset", function () { + $form.on("reset", () => { cfg.$target.html(cfg.$target.data("initial-value")); }); } }, - extractModifiers: function inject_extractModifiers(cfg) { + extractModifiers(cfg) { /* The user can add modifiers to the source and target arguments. * Modifiers such as ::element, ::before and ::after. * We identifiy and extract these modifiers here. @@ -462,7 +458,7 @@ const inject = { return true; }, - createTarget: function inject_createTarget(selector) { + createTarget(selector) { /* create a target that matches the selector * * XXX: so far we only support #target and create a div with @@ -478,7 +474,7 @@ const inject = { return $target; }, - stopBubblingFromRemovedElement: function ($el, cfgs, ev) { + stopBubblingFromRemovedElement($el, cfgs, ev) { /* IE8 fix. Stop event from propagating IF $el will be removed * from the DOM. With pat-inject, often $el is the target that * will itself be replaced with injected content. @@ -498,7 +494,7 @@ const inject = { } }, - _performInjection: function ($el, $source, cfg, trigger, title) { + _performInjection($el, $source, cfg, trigger, title) { /* Called after the XHR has succeeded and we have a new $source * element to inject. */ @@ -549,7 +545,7 @@ const inject = { } }, - _afterInjection: function ($el, $injected, cfg) { + _afterInjection($el, $injected, cfg) { /* Set a class on the injected elements and fire the * patterns-injected event. */ @@ -666,9 +662,9 @@ const inject = { // Special case, we want to call something, but we don't want to inject anything data = ""; } - $.each(cfgs[0].hooks || [], function (idx, hook) { - $el.trigger("pat-inject-hook-" + hook); - }); + $.each(cfgs[0].hooks || [], (idx, hook) => + $el.trigger("pat-inject-hook-" + hook) + ); inject.stopBubblingFromRemovedElement($el, cfgs, ev); sources$ = await inject.callTypeHandler( cfgs[0].dataType, @@ -687,7 +683,7 @@ const inject = { ) { title = sources$[sources$.length - 1]; } - cfgs.forEach(function (cfg, idx) { + cfgs.forEach((cfg, idx) => { function perform_inject() { if (cfg.target != "none") cfg.$target.each(function () { @@ -701,9 +697,7 @@ const inject = { }); } if (cfg.processDelay) { - setTimeout(function () { - perform_inject(); - }, cfg.processDelay); + setTimeout(() => perform_inject(), cfg.processDelay); } else { perform_inject(); } @@ -718,7 +712,7 @@ const inject = { $el.off("pat-ajax-error.pat-inject"); }, - _onInjectError: function ($el, cfgs, event) { + _onInjectError($el, cfgs, event) { var explanation = ""; var timestamp = new Date(); if (event.jqxhr.status % 100 == 4) { @@ -741,27 +735,25 @@ const inject = { timestamp + ". You can click to close this."; $("body").attr("data-error-message", msg_attr); - $("body").on("click", function () { + $("body").on("click", () => { $("body").removeAttr("data-error-message"); window.location.href = window.location.href; }); - cfgs.forEach(function (cfg) { + cfgs.forEach((cfg) => { if ("$injected" in cfg) cfg.$injected.remove(); }); $el.off("pat-ajax-success.pat-inject"); $el.off("pat-ajax-error.pat-inject"); }, - execute: function inject_execute(cfgs, $el) { + execute(cfgs, $el) { /* Actually execute the injection. * * Either by making an ajax request or by spoofing an ajax * request when the content is readily available in the current page. */ // get a kinda deep copy, we scribble on it - cfgs = cfgs.map(function (cfg) { - return $.extend({}, cfg); - }); + cfgs = cfgs.map((cfg) => $.extend({}, cfg)); if (!inject.verifyConfig(cfgs, $el)) { return; } @@ -776,16 +768,14 @@ const inject = { // possibility for spinners on targets _.chain(cfgs) .filter(_.property("loadingClass")) - .each(function (cfg) { + .each((cfg) => { if (cfg.target != "none") cfg.$target.addClass(cfg.loadingClass); }); // Put the execute class on the elem that has pat inject on it _.chain(cfgs) .filter(_.property("loadingClass")) - .each(function (cfg) { - $el.addClass(cfg.executingClass); - }); + .each((cfg) => $el.addClass(cfg.executingClass)); $el.on( "pat-ajax-success.pat-inject", @@ -795,11 +785,8 @@ const inject = { "pat-ajax-error.pat-inject", this._onInjectError.bind(this, $el, cfgs) ); - $el.on( - "pat-ajax-success.pat-inject pat-ajax-error.pat-inject", - function () { - $el.removeData("pat-inject-triggered"); - } + $el.on("pat-ajax-success.pat-inject pat-ajax-error.pat-inject", () => + $el.removeData("pat-inject-triggered") ); if (cfgs[0].url.length) { @@ -818,7 +805,7 @@ const inject = { } }, - _inject: function inject_inject(trigger, $source, $target, cfg) { + _inject(trigger, $source, $target, cfg) { // action to jquery method mapping, except for "content" // and "element" var method = { @@ -860,9 +847,9 @@ const inject = { return true; }, - _sourcesFromHtml: function inject_sourcesFromHtml(html, url, sources) { + _sourcesFromHtml(html, url, sources) { var $html = inject._parseRawHtml(html, url); - return sources.map(function inject_sourcesFromHtml_map(source) { + return sources.map((source) => { if (source === "body") { source = "#__original_body"; } @@ -907,7 +894,7 @@ const inject = { VIDEO: "data-pat-inject-rebase-src", }, - _rebaseHTML: function inject_rebaseHTML(base, html) { + _rebaseHTML(base, html) { if (html === "") { // Special case, source is none return ""; @@ -956,7 +943,7 @@ const inject = { .trim(); }, - _parseRawHtml: function inject_parseRawHtml(html, url) { + _parseRawHtml(html, url) { url = url || ""; // remove script tags and head and replace body by a div @@ -985,7 +972,7 @@ const inject = { }, // XXX: hack - _initAutoloadVisible: function inject_initAutoloadVisible($el, cfgs) { + _initAutoloadVisible($el, cfgs) { if ($el.data("pat-inject-autoloaded")) { // ignore executed autoloads return false; @@ -1088,9 +1075,7 @@ const inject = { // https://github.com/w3c/IntersectionObserver/tree/master/polyfill if (IntersectionObserver) { var observer = new IntersectionObserver(checkVisibility); - $el.each(function (idx, el) { - observer.observe(el); - }); + $el.each((idx, el) => observer.observe(el)); } else { $(window).on( "resize.pat-autoload scroll.pat-autoload", @@ -1101,7 +1086,7 @@ const inject = { return false; }, - _initIdleTrigger: function inject_initIdleTrigger($el, delay) { + _initIdleTrigger($el, delay) { // XXX TODO: handle item removed from DOM var timeout = parseInt(delay, 10); var timer; @@ -1112,7 +1097,7 @@ const inject = { clearTimeout(timer); } - var onInteraction = utils.debounce(function onInteraction() { + var onInteraction = utils.debounce(() => { if (!document.body.contains($el[0])) { unsub(); return; @@ -1122,9 +1107,9 @@ const inject = { }, timeout); function unsub() { - ["scroll", "resize"].forEach(function (e) { - window.removeEventListener(e, onInteraction); - }); + ["scroll", "resize"].forEach((e) => + window.removeEventListener(e, onInteraction) + ); [ "click", "keypress", @@ -1132,16 +1117,14 @@ const inject = { "mousemove", "touchstart", "touchend", - ].forEach(function (e) { - document.removeEventListener(e, onInteraction); - }); + ].forEach((e) => document.removeEventListener(e, onInteraction)); } onInteraction(); - ["scroll", "resize"].forEach(function (e) { - window.addEventListener(e, onInteraction); - }); + ["scroll", "resize"].forEach((e) => + window.addEventListener(e, onInteraction) + ); [ "click", "keypress", @@ -1149,13 +1132,11 @@ const inject = { "mousemove", "touchstart", "touchend", - ].forEach(function (e) { - document.addEventListener(e, onInteraction); - }); + ].forEach((e) => document.addEventListener(e, onInteraction)); }, // XXX: simple so far to see what the team thinks of the idea - registerTypeHandler: function inject_registerTypeHandler(type, handler) { + registerTypeHandler(type, handler) { inject.handlers[type] = handler; }, @@ -1170,10 +1151,8 @@ const inject = { handlers: { html: { - sources: function (cfgs, data) { - var sources = cfgs.map(function (cfg) { - return cfg.source; - }); + sources(cfgs, data) { + var sources = cfgs.map((cfg) => cfg.source); sources.push("title"); return inject._sourcesFromHtml(data, cfgs[0].url, sources); }, @@ -1181,12 +1160,7 @@ const inject = { }, }; -$(document).on("patterns-injected.inject", function onInjected( - ev, - cfg, - trigger, - injected -) { +$(document).on("patterns-injected.inject", (ev, cfg, trigger, injected) => { /* Listen for the patterns-injected event. * * Remove the "loading-class" classes from all injection targets and @@ -1208,7 +1182,7 @@ $(document).on("patterns-injected.inject", function onInjected( } }); -$(window).on("popstate", function (event) { +$(window).on("popstate", (event) => { // popstate also triggers on traditional anchors if (!event.originalEvent.state && "replaceState" in history) { try { From 5f1f4ac2ae1f6ce4de44efbf4c02d49506c8ac73 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 10 Dec 2020 11:57:13 +0100 Subject: [PATCH 04/11] pat-inject: Modernize - avoid modified ``this`` --- src/pat/inject/inject.js | 183 +++++++++++++++++++-------------------- 1 file changed, 88 insertions(+), 95 deletions(-) diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index 99fee6c0f..1997a4eda 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -494,7 +494,7 @@ const inject = { } }, - _performInjection($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. */ @@ -508,20 +508,18 @@ const inject = { document.querySelector && !document.addEventListener ) { - $src = $source.map(function () { - return $(this.outerHTML)[0]; - }); + $src = $source.map((idx, el) => $(el.outerHTML)[0]); } else { $src = $source.safeClone(); } - var $target = $(this), - $injected = cfg.$injected || $src; - $src.findInclusive("img").on("load", function () { - $(this).trigger("pat-inject-content-loaded"); + $src.findInclusive("img").on("load", (e) => { + $(e.target).trigger("pat-inject-content-loaded"); }); + + const $injected = cfg.$injected || $src; // Now the injection actually happens. - if (inject._inject(trigger, $src, $target, cfg)) { + if (inject._inject(trigger, $src, $(target), cfg)) { inject._afterInjection($el, $injected, cfg); } // History support. if subform is submitted, append form params @@ -550,9 +548,9 @@ const inject = { * patterns-injected event. */ $injected - .filter(function () { + .filter((idx, el_) => { // setting data on textnode fails in IE8 - return this.nodeType !== TEXT_NODE; + return el_.nodeType !== TEXT_NODE; }) .data("pat-injected", { origin: cfg.url }); @@ -566,12 +564,12 @@ const inject = { .parent() .trigger("patterns-injected", [cfg, $el[0], $injected[0]]); } else { - $injected.each(function () { + $injected.each((idx, el_) => { // patterns-injected event will be triggered for each injected (non-text) element. - if (this.nodeType !== TEXT_NODE) { - $(this) + if (el_.nodeType !== TEXT_NODE) { + $(el_) .addClass(cfg["class"]) - .trigger("patterns-injected", [cfg, $el[0], this]); + .trigger("patterns-injected", [cfg, $el[0], el_]); } }); } @@ -683,17 +681,18 @@ const inject = { ) { title = sources$[sources$.length - 1]; } - cfgs.forEach((cfg, idx) => { + cfgs.forEach((cfg, idx1) => { function perform_inject() { if (cfg.target != "none") - cfg.$target.each(function () { - inject._performInjection.apply(this, [ + cfg.$target.each((idx2, target) => { + inject._performInjection( + target, $el, - sources$[idx], + sources$[idx1], cfg, ev.target, - title, - ]); + title + ); }); } if (cfg.processDelay) { @@ -864,8 +863,8 @@ const inject = { } } - $source.find('a[href^="#"]').each(function () { - var href = this.getAttribute("href"); + $source.find('a[href^="#"]').each((idx, el_) => { + var href = el_.getAttribute("href"); if (href.indexOf("#{1}") !== -1) { // We ignore hrefs containing #{1} because they're not // valid and only applicable in the context of @@ -876,9 +875,9 @@ const inject = { // this fragment. if (href.length === 1) { // Special case for top-of-page links - this.href = url; + el_.href = url; } else if (!$source.find(href).length) { - this.href = url + href; + el_.href = url + href; } }); return $source; @@ -912,10 +911,10 @@ const inject = { $page .find(Object.keys(inject._rebaseAttrs).join(",")) - .each(function () { - var $this = $(this), - attrName = inject._rebaseAttrs[this.tagName], - value = $this.attr(attrName); + .each((idx, el_) => { + var $el_ = $(el_), + attrName = inject._rebaseAttrs[el_.tagName], + value = $el_.attr(attrName); if ( value && @@ -925,13 +924,13 @@ const inject = { value.slice(0, 11) !== "javascript:" ) { value = utils.rebaseURL(base, value); - $this.attr(attrName, value); + $el_.attr(attrName, value); } }); // XXX: IE8 changes the order of attributes in html. The following // lines move data-pat-inject-rebase-src to src. - $page.find("[data-pat-inject-rebase-src]").each(function () { - var $el = $(this); + $page.find("[data-pat-inject-rebase-src]").each((id, el_) => { + var $el = $(el_); $el.attr("src", $el.attr("data-pat-inject-rebase-src")).removeAttr( "data-pat-inject-rebase-src" ); @@ -996,41 +995,38 @@ const inject = { if ($scrollable.length) { // if scrollable parent and visible -> trigger it // we only look at the closest scrollable parent, no nesting - checkVisibility = utils.debounce( - function inject_checkVisibility_scrollable() { - if ( - $el.data("patterns.autoload") || - !$.contains(document, $el[0]) - ) { - return false; - } - if (!$el.is(":visible")) { - return false; - } - // check if the target element still exists. Otherwise halt and catch fire - var target = ( - $el.data("pat-inject")[0].target || - cfgs[0].defaultSelector - ).replace(/::element/, ""); - if (target && target !== "self" && $(target).length === 0) { - return false; - } - var reltop = - $el.safeOffset().top - - $scrollable.safeOffset().top - - 1000, - doTrigger = reltop <= $scrollable.innerHeight(); - if (doTrigger) { - // checkVisibility was possibly installed as a scroll - // handler and has now served its purpose -> remove - $($scrollable[0]).off("scroll", checkVisibility); - $(window).off("resize.pat-autoload", checkVisibility); - return trigger(); - } + // Check visibility for scrollable + checkVisibility = utils.debounce(() => { + if ( + $el.data("patterns.autoload") || + !$.contains(document, $el[0]) + ) { return false; - }, - 100 - ); + } + if (!$el.is(":visible")) { + return false; + } + // check if the target element still exists. Otherwise halt and catch fire + var target = ( + $el.data("pat-inject")[0].target || cfgs[0].defaultSelector + ).replace(/::element/, ""); + if (target && target !== "self" && $(target).length === 0) { + return false; + } + var reltop = + $el.safeOffset().top - + $scrollable.safeOffset().top - + 1000, + doTrigger = reltop <= $scrollable.innerHeight(); + if (doTrigger) { + // checkVisibility was possibly installed as a scroll + // handler and has now served its purpose -> remove + $($scrollable[0]).off("scroll", checkVisibility); + $(window).off("resize.pat-autoload", checkVisibility); + return trigger(); + } + return false; + }, 100); if (checkVisibility()) { return true; } @@ -1039,36 +1035,33 @@ const inject = { $(window).on("resize.pat-autoload", checkVisibility); } else { // Use case 2: scrolling the entire page - checkVisibility = utils.debounce( - function inject_checkVisibility_not_scrollable() { - if ($el.parents(":scrollable").length) { - // Because of a resize the element has now a scrollable parent - // and we should reset the correct event - $(window).off(".pat-autoload", checkVisibility); - return inject._initAutoloadVisible($el); - } - if ($el.data("patterns.autoload")) { - return false; - } - if (!$el.is(":visible")) { - return false; - } - if (!utils.elementInViewport($el[0])) { - return false; - } - // check if the target element still exists. Otherwise halt and catch fire - var target = ( - $el.data("pat-inject")[0].target || - cfgs[0].defaultSelector - ).replace(/::element/, ""); - if (target && target !== "self" && $(target).length === 0) { - return false; - } + // Check visibility for non-scrollable + checkVisibility = utils.debounce(() => { + if ($el.parents(":scrollable").length) { + // Because of a resize the element has now a scrollable parent + // and we should reset the correct event $(window).off(".pat-autoload", checkVisibility); - return trigger(); - }, - 100 - ); + return inject._initAutoloadVisible($el); + } + if ($el.data("patterns.autoload")) { + return false; + } + if (!$el.is(":visible")) { + return false; + } + if (!utils.elementInViewport($el[0])) { + return false; + } + // check if the target element still exists. Otherwise halt and catch fire + var target = ( + $el.data("pat-inject")[0].target || cfgs[0].defaultSelector + ).replace(/::element/, ""); + if (target && target !== "self" && $(target).length === 0) { + return false; + } + $(window).off(".pat-autoload", checkVisibility); + return trigger(); + }, 100); if (checkVisibility()) { return true; } From bcec73b959e86d5b4f345bf8fb116d509d3aa59f Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 10 Dec 2020 12:43:41 +0100 Subject: [PATCH 05/11] pat-inject: Modernize - use ``const`` and ``let`` --- src/pat/inject/inject.js | 204 ++++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 110 deletions(-) diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index 1997a4eda..86aaaad65 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -53,7 +53,7 @@ const inject = { trigger: ".raptor-ui .ui-button.pat-inject, a.pat-inject, form.pat-inject, .pat-subform.pat-inject", init($el, opts) { - var cfgs = inject.extractConfig($el, opts); + const cfgs = inject.extractConfig($el, opts); if ( cfgs.some((e) => e.history === "record") && !("pushState" in history) @@ -90,22 +90,22 @@ const inject = { console.log("received push message: " + data); if (data == cfgs[0].pushMarker) { console.log("re-injecting " + data); - inject.onTrigger.apply($el[0], []); + inject.onTrigger({ target: $el[0] }); } }); } if (cfgs[0].idleTrigger) { // XXX TODO: handle item removed from DOM - var timeout = parseInt(cfgs[0].idleTrigger, 10); - var timer; + const timeout = parseInt(cfgs[0].idleTrigger, 10); + let timer; const onTimeout = () => { - inject.onTrigger.apply($el[0], []); + inject.onTrigger({ target: $el[0] }); unsub(); clearTimeout(timer); }; - var onInteraction = utils.debounce(() => { + const onInteraction = utils.debounce(() => { clearTimeout(timer); timer = setTimeout(onTimeout, cfgs[0].trigger); }, timeout); @@ -168,22 +168,22 @@ const inject = { break; case "autoload": if (!cfgs[0].delay) { - inject.onTrigger.apply($el[0], []); + inject.onTrigger({ target: $el[0] }); } else { // generate UID - var uid = Math.random().toString(36); + const uid = Math.random().toString(36); $el.attr("data-pat-inject-uid", uid); // function to trigger the autoload and mark as triggered - const delayed_trigger = (uid) => { + const delayed_trigger = (uid_) => { // Check if the element has been removed from the dom - var still_there = $( - "[data-pat-inject-uid='" + uid + "']" + const still_there = $( + "[data-pat-inject-uid='" + uid_ + "']" ); if (still_there.length == 0) return false; $el.data("pat-inject-autoloaded", true); - inject.onTrigger.apply($el[0], []); + inject.onTrigger({ target: $el[0] }); return true; }; window.setTimeout( @@ -211,37 +211,37 @@ const inject = { return $el; }, - onTrigger(ev) { + onTrigger(e) { /* Injection has been triggered, either via form submission or a * link has been clicked. */ - var cfgs = $(this).data("pat-inject"), - $el = $(this); + const $el = $(e.target); + const cfgs = $el.data("pat-inject"); if ($el.is("form")) { $(cfgs).each((i, v) => { v.params = $.param($el.serializeArray()); }); } - ev && ev.preventDefault(); + e.preventDefault && e.preventDefault(); $el.trigger("patterns-inject-triggered"); inject.execute(cfgs, $el); }, - onFormActionSubmit(ev) { - ajax.onClickSubmit(ev); // make sure the submitting button is sent with the form + onFormActionSubmit(e) { + ajax.onClickSubmit(e); // make sure the submitting button is sent with the form - var $button = $(ev.target), - formaction = $button.attr("formaction"), - $form = $button.parents(".pat-inject").first(), - opts = { url: formaction }, - $cfg_node = $button.closest("[data-pat-inject]"), - cfgs = inject.extractConfig($cfg_node, opts); + const $button = $(e.target); + const formaction = $button.attr("formaction"); + const $form = $button.parents(".pat-inject").first(); + const opts = { url: formaction }; + const $cfg_node = $button.closest("[data-pat-inject]"); + const cfgs = inject.extractConfig($cfg_node, opts); $(cfgs).each((i, v) => { v.params = $.param($form.serializeArray()); }); - ev.preventDefault(); + e.preventDefault(); $form.trigger("patterns-inject-triggered"); inject.execute(cfgs, $form); }, @@ -249,8 +249,8 @@ const inject = { submitSubform($sub) { /* This method is called from pat-subform */ - var $el = $sub.parents("form"), - cfgs = $sub.data("pat-inject"); + const $el = $sub.parents("form"); + const cfgs = $sub.data("pat-inject"); // store the params of the subform in the config, to be used by history $(cfgs).each((i, v) => { @@ -268,7 +268,7 @@ const inject = { extractConfig($el, opts) { opts = $.extend({}, opts); - var cfgs = parser.parse($el, opts, true); + const cfgs = parser.parse($el, opts, true); cfgs.forEach((cfg) => { // opts and cfg have priority, fallback to href/action cfg.url = @@ -280,17 +280,18 @@ const inject = { ""; // separate selector from url - var urlparts = cfg.url.split("#"); + const urlparts = cfg.url.split("#"); cfg.url = urlparts[0]; - // if no selector, check for selector as part of original url - var defaultSelector = (urlparts[1] && "#" + urlparts[1]) || "body"; - if (urlparts.length > 2) { log.warn("Ignoring additional source ids:", urlparts.slice(2)); } - cfg.defaultSelector = cfg.defaultSelector || defaultSelector; + if (!cfg.defaultSelector) { + // if no selector, check for selector as part of original url + cfg.defaultSelector = + (urlparts[1] && "#" + urlparts[1]) || "body"; + } if (cfg.delay) { try { cfg.delay = utils.parseTime(cfg.delay); @@ -307,8 +308,8 @@ const inject = { elementIsDirty(m) { /* Check whether the passed in form element contains a value. */ - var data = $.map(m.find(":input:not(select)"), (i) => { - var val = $(i).val(); + const data = $.map(m.find(":input:not(select)"), (i) => { + const val = $(i).val(); return Boolean(val) && val !== $(i).attr("placeholder"); }); return $.inArray(true, data) !== -1; @@ -318,11 +319,11 @@ const inject = { /* If configured to do so, show a confirmation dialog to the user. * This is done before attempting to perform injection. */ - var should_confirm = false, - message; + let should_confirm = false; + let message; _.each(cfgs, (cfg) => { - var _confirm = false; + let _confirm = false; if (cfg.confirm == "always") { _confirm = true; } else if (cfg.confirm === "form-data") { @@ -415,7 +416,7 @@ const inject = { if (cfg.target === "none") // Special case, we don't want to display any return value. return; - var $form = cfg.$target.parents("form"); + const $form = cfg.$target.parents("form"); if ( $form.length !== 0 && cfg.$target.data("initial-value") === undefined @@ -432,18 +433,16 @@ const inject = { * Modifiers such as ::element, ::before and ::after. * We identifiy and extract these modifiers here. */ - var source_re = /^(.*?)(::element)?$/, - target_re = /^(.*?)(::element)?(::after|::before)?$/, - source_match = source_re.exec(cfg.source), - target_match = target_re.exec(cfg.target), - targetMod, - targetPosition; + const source_re = /^(.*?)(::element)?$/; + const target_re = /^(.*?)(::element)?(::after|::before)?$/; + const source_match = source_re.exec(cfg.source); + const target_match = target_re.exec(cfg.target); cfg.source = source_match[1]; cfg.sourceMod = source_match[2] ? "element" : "content"; cfg.target = target_match[1]; - targetMod = target_match[2] ? "element" : "content"; - targetPosition = (target_match[3] || "::").slice(2); // position relative to target + const targetMod = target_match[2] ? "element" : "content"; + const targetPosition = (target_match[3] || "::").slice(2); // position relative to target if (cfg.loadingClass) { cfg.loadingClass += " " + cfg.loadingClass + "-" + targetMod; @@ -464,12 +463,11 @@ const inject = { * XXX: so far we only support #target and create a div with * that id appended to the body. */ - var $target; if (selector.slice(0, 1) !== "#") { log.error("only id supported for non-existing target"); return null; } - $target = $("
").attr({ id: selector.slice(1) }); + const $target = $("
").attr({ id: selector.slice(1) }); $("body").append($target); return $target; }, @@ -484,10 +482,9 @@ const inject = { * * See: http://stackoverflow.com/questions/7114368/why-is-jquery-remove-throwing-attr-exception-in-ie8 */ - var s; // jquery selector - for (var i = 0; i < cfgs.length; i++) { - s = cfgs[i].target; - if ($el.parents(s).addBack(s) && !ev.isPropagationStopped()) { + for (const cfg of cfgs) { + const sel = cfg.target; + if ($el.parents(sel).addBack(sel) && !ev.isPropagationStopped()) { ev.stopPropagation(); return; } @@ -501,7 +498,7 @@ const inject = { if (cfg.sourceMod === "content") { $source = $source.contents(); } - var $src; + let $src; // $source.clone() does not work with shived elements in IE8 if ( document.all && @@ -523,10 +520,9 @@ const inject = { inject._afterInjection($el, $injected, cfg); } // History support. if subform is submitted, append form params - var glue = "?"; + const glue = cfg.url.indexOf("?") > -1 ? "&" : "?"; if (cfg.history === "record" && "pushState" in history) { if (cfg.params) { - if (cfg.url.indexOf("?") > -1) glue = "&"; history.pushState( { url: cfg.url + glue + cfg.params }, "", @@ -575,7 +571,7 @@ const inject = { } if (cfg.scroll && cfg.scroll !== "none") { - var scroll_container = cfg.$target + const scroll_container = cfg.$target .parents() .addBack() .filter(":scrollable"); @@ -584,25 +580,23 @@ const inject = { : window; // default for scroll===top - var top = 0; - var left = 0; + let top = 0; + let left = 0; if (cfg.scroll !== "top") { - var scroll_target; - if (cfg.scroll === "target") { - scroll_target = cfg.$target[0]; - } else { - scroll_target = $injected.filter(cfg.scroll)[0]; - } + const scroll_target = + cfg.scroll === "target" + ? cfg.$target[0] + : $injected.filter(cfg.scroll)[0]; // Get the reference element to which against we calculate // the relative position of the target. // In case of a scroll container of window, we do not have // getBoundingClientRect method, so get the body instead. - var scroll_container_ref = scroll_container; - if (scroll_container_ref === window) { - scroll_container_ref = document.body; - } + const scroll_container_ref = + scroll_container === window + ? document.body + : scroll_container; // Calculate absolute [ยน] position difference between // scroll_container and scroll_target. @@ -650,8 +644,7 @@ const inject = { }, async _onInjectSuccess($el, cfgs, ev) { - var sources$, - data = ev && ev.jqxhr && ev.jqxhr.responseText; + let data = ev && ev.jqxhr && ev.jqxhr.responseText; if (!data) { log.warn("No response content, aborting", ev); return; @@ -664,7 +657,7 @@ const inject = { $el.trigger("pat-inject-hook-" + hook) ); inject.stopBubblingFromRemovedElement($el, cfgs, ev); - sources$ = await inject.callTypeHandler( + const sources$ = await inject.callTypeHandler( cfgs[0].dataType, "sources", $el, @@ -672,7 +665,7 @@ const inject = { ); /* pick the title source for dedicated handling later Title - if present - is always appended at the end. */ - var title; + let title; if ( sources$ && sources$[sources$.length - 1] && @@ -712,8 +705,8 @@ const inject = { }, _onInjectError($el, cfgs, event) { - var explanation = ""; - var timestamp = new Date(); + let explanation = ""; + const timestamp = new Date(); if (event.jqxhr.status % 100 == 4) { explanation = "Sorry! We couldn't find the page to load. Please make a screenshot and send it to support. Thank you!"; @@ -724,15 +717,7 @@ const inject = { explanation = "It seems, the server is down. Please make a screenshot and contact support. Thank you!"; } - var msg_attr = - explanation + - " Status is " + - event.jqxhr.status + - " " + - event.jqxhr.statusText + - ", time was " + - timestamp + - ". You can click to close this."; + const msg_attr = `${explanation} Status is ${event.jqxhr.status} ${event.jqxhr.statusText}, time was ${timestamp}. You can click to close this.`; $("body").attr("data-error-message", msg_attr); $("body").on("click", () => { $("body").removeAttr("data-error-message"); @@ -807,7 +792,7 @@ const inject = { _inject(trigger, $source, $target, cfg) { // action to jquery method mapping, except for "content" // and "element" - var method = { + const method = { contentbefore: "prepend", contentafter: "append", elementbefore: "before", @@ -847,7 +832,7 @@ const inject = { }, _sourcesFromHtml(html, url, sources) { - var $html = inject._parseRawHtml(html, url); + const $html = inject._parseRawHtml(html, url); return sources.map((source) => { if (source === "body") { source = "#__original_body"; @@ -855,7 +840,7 @@ const inject = { if (source === "none") { return $(""); } - var $source = $html.find(source); + const $source = $html.find(source); if ($source.length === 0) { if (source != "title") { @@ -864,7 +849,7 @@ const inject = { } $source.find('a[href^="#"]').each((idx, el_) => { - var href = el_.getAttribute("href"); + const href = el_.getAttribute("href"); if (href.indexOf("#{1}") !== -1) { // We ignore hrefs containing #{1} because they're not // valid and only applicable in the context of @@ -898,7 +883,7 @@ const inject = { // Special case, source is none return ""; } - var $page = $( + const $page = $( html .replace( /(\s)(src\s*)=/gi, @@ -912,9 +897,9 @@ const inject = { $page .find(Object.keys(inject._rebaseAttrs).join(",")) .each((idx, el_) => { - var $el_ = $(el_), - attrName = inject._rebaseAttrs[el_.tagName], - value = $el_.attr(attrName); + const $el_ = $(el_); + const attrName = inject._rebaseAttrs[el_.tagName]; + let value = $el_.attr(attrName); if ( value && @@ -930,7 +915,7 @@ const inject = { // XXX: IE8 changes the order of attributes in html. The following // lines move data-pat-inject-rebase-src to src. $page.find("[data-pat-inject-rebase-src]").each((id, el_) => { - var $el = $(el_); + const $el = $(el_); $el.attr("src", $el.attr("data-pat-inject-rebase-src")).removeAttr( "data-pat-inject-rebase-src" ); @@ -946,8 +931,8 @@ const inject = { url = url || ""; // remove script tags and head and replace body by a div - var title = html.match(/\(.*)\<\/title\>/); - var clean_html = html + const title = html.match(/\(.*)\<\/title\>/); + let clean_html = html .replace(/)<[^<]*)*<\/script>/gi, "") .replace(/)<[^<]*)*<\/head>/gi, "") .replace(/]*?)>/gi, '
') @@ -960,7 +945,7 @@ const inject = { } catch (e) { log.error("Error rebasing urls", e); } - var $html = $("
").html(clean_html); + const $html = $("
").html(clean_html); if ($html.children().length === 0) { log.warn( "Parsing html resulted in empty jquery object:", @@ -976,8 +961,7 @@ const inject = { // ignore executed autoloads return false; } - var $scrollable = $el.parents(":scrollable"), - checkVisibility; + const $scrollable = $el.parents(":scrollable"); // function to trigger the autoload and mark as triggered function trigger(event) { @@ -985,7 +969,7 @@ const inject = { return false; } $el.data("pat-inject-autoloaded", true); - inject.onTrigger.apply($el[0], []); + inject.onTrigger({ target: $el[0] }); event && event.preventDefault(); return true; } @@ -996,7 +980,7 @@ const inject = { // if scrollable parent and visible -> trigger it // we only look at the closest scrollable parent, no nesting // Check visibility for scrollable - checkVisibility = utils.debounce(() => { + const checkVisibility = utils.debounce(() => { if ( $el.data("patterns.autoload") || !$.contains(document, $el[0]) @@ -1007,13 +991,13 @@ const inject = { return false; } // check if the target element still exists. Otherwise halt and catch fire - var target = ( + const target = ( $el.data("pat-inject")[0].target || cfgs[0].defaultSelector ).replace(/::element/, ""); if (target && target !== "self" && $(target).length === 0) { return false; } - var reltop = + const reltop = $el.safeOffset().top - $scrollable.safeOffset().top - 1000, @@ -1036,7 +1020,7 @@ const inject = { } else { // Use case 2: scrolling the entire page // Check visibility for non-scrollable - checkVisibility = utils.debounce(() => { + const checkVisibility = utils.debounce(() => { if ($el.parents(":scrollable").length) { // Because of a resize the element has now a scrollable parent // and we should reset the correct event @@ -1053,7 +1037,7 @@ const inject = { return false; } // check if the target element still exists. Otherwise halt and catch fire - var target = ( + const target = ( $el.data("pat-inject")[0].target || cfgs[0].defaultSelector ).replace(/::element/, ""); if (target && target !== "self" && $(target).length === 0) { @@ -1067,7 +1051,7 @@ const inject = { } // https://github.com/w3c/IntersectionObserver/tree/master/polyfill if (IntersectionObserver) { - var observer = new IntersectionObserver(checkVisibility); + const observer = new IntersectionObserver(checkVisibility); $el.each((idx, el) => observer.observe(el)); } else { $(window).on( @@ -1081,16 +1065,16 @@ const inject = { _initIdleTrigger($el, delay) { // XXX TODO: handle item removed from DOM - var timeout = parseInt(delay, 10); - var timer; + const timeout = parseInt(delay, 10); + let timer; function onTimeout() { - inject.onTrigger.apply($el[0], []); + inject.onTrigger({ target: $el[0] }); unsub(); clearTimeout(timer); } - var onInteraction = utils.debounce(() => { + const onInteraction = utils.debounce(() => { if (!document.body.contains($el[0])) { unsub(); return; @@ -1145,7 +1129,7 @@ const inject = { handlers: { html: { sources(cfgs, data) { - var sources = cfgs.map((cfg) => cfg.source); + const sources = cfgs.map((cfg) => cfg.source); sources.push("title"); return inject._sourcesFromHtml(data, cfgs[0].url, sources); }, From 9a551f33be5b3c340b4f794c52f430c39cf078c6 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 10 Dec 2020 14:36:16 +0100 Subject: [PATCH 06/11] pat-inject: refactor to refer to this instance instead of object. --- src/pat/inject/inject.js | 90 ++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index 86aaaad65..bb142fdd9 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -1,12 +1,12 @@ +import "../../core/jquery-ext"; // for :scrollable for autoLoading-visible import "regenerator-runtime/runtime"; // needed for ``await`` support import $ from "jquery"; import _ from "underscore"; import ajax from "../ajax/ajax"; -import Parser from "../../core/parser"; import logging from "../../core/logging"; +import Parser from "../../core/parser"; import registry from "../../core/registry"; import utils from "../../core/utils"; -import "../../core/jquery-ext"; // for :scrollable for autoLoading-visible const log = logging.getLogger("pat.inject"); const parser = new Parser("inject"); @@ -53,7 +53,7 @@ const inject = { trigger: ".raptor-ui .ui-button.pat-inject, a.pat-inject, form.pat-inject, .pat-subform.pat-inject", init($el, opts) { - const cfgs = inject.extractConfig($el, opts); + const cfgs = this.extractConfig($el, opts); if ( cfgs.some((e) => e.history === "record") && !("pushState" in history) @@ -90,7 +90,7 @@ const inject = { console.log("received push message: " + data); if (data == cfgs[0].pushMarker) { console.log("re-injecting " + data); - inject.onTrigger({ target: $el[0] }); + this.onTrigger({ target: $el[0] }); } }); } @@ -100,7 +100,7 @@ const inject = { let timer; const onTimeout = () => { - inject.onTrigger({ target: $el[0] }); + this.onTrigger({ target: $el[0] }); unsub(); clearTimeout(timer); }; @@ -149,7 +149,7 @@ const inject = { }); // setup event handlers if ($el.is("form")) { - $el.on("submit.pat-inject", inject.onTrigger) + $el.on("submit.pat-inject", this.onTrigger.bind(this)) .on( "click.pat-inject", "[type=submit]", @@ -158,17 +158,17 @@ const inject = { .on( "click.pat-inject", "[type=submit][formaction], [type=image][formaction]", - inject.onFormActionSubmit + this.onFormActionSubmit.bind(this) ); } else if ($el.is(".pat-subform")) { log.debug("Initializing subform with injection"); } else { - $el.on("click.pat-inject", inject.onTrigger); + $el.on("click.pat-inject", this.onTrigger.bind(this)); } break; case "autoload": if (!cfgs[0].delay) { - inject.onTrigger({ target: $el[0] }); + this.onTrigger({ target: $el[0] }); } else { // generate UID const uid = Math.random().toString(36); @@ -183,7 +183,7 @@ const inject = { if (still_there.length == 0) return false; $el.data("pat-inject-autoloaded", true); - inject.onTrigger({ target: $el[0] }); + this.onTrigger({ target: $el[0] }); return true; }; window.setTimeout( @@ -193,10 +193,10 @@ const inject = { } break; case "autoload-visible": - inject._initAutoloadVisible($el, cfgs); + this._initAutoloadVisible($el, cfgs); break; case "idle": - inject._initIdleTrigger($el, cfgs[0].delay); + this._initIdleTrigger($el, cfgs[0].delay); break; } } @@ -224,7 +224,7 @@ const inject = { } e.preventDefault && e.preventDefault(); $el.trigger("patterns-inject-triggered"); - inject.execute(cfgs, $el); + this.execute(cfgs, $el); }, onFormActionSubmit(e) { @@ -235,7 +235,7 @@ const inject = { const $form = $button.parents(".pat-inject").first(); const opts = { url: formaction }; const $cfg_node = $button.closest("[data-pat-inject]"); - const cfgs = inject.extractConfig($cfg_node, opts); + const cfgs = this.extractConfig($cfg_node, opts); $(cfgs).each((i, v) => { v.params = $.param($form.serializeArray()); @@ -243,7 +243,7 @@ const inject = { e.preventDefault(); $form.trigger("patterns-inject-triggered"); - inject.execute(cfgs, $form); + this.execute(cfgs, $form); }, submitSubform($sub) { @@ -262,7 +262,7 @@ const inject = { } catch (e) { log.error("patterns-inject-triggered", e); } - inject.execute(cfgs, $el); + this.execute(cfgs, $el); }, extractConfig($el, opts) { @@ -328,7 +328,7 @@ const inject = { _confirm = true; } else if (cfg.confirm === "form-data") { if (cfg.target != "none") - _confirm = inject.elementIsDirty(cfg.$target); + _confirm = this.elementIsDirty(cfg.$target); } else if (cfg.confirm === "class") { if (cfg.target != "none") _confirm = cfg.$target.hasClass("is-dirty"); @@ -361,7 +361,7 @@ const inject = { log.error("Need target selector", cfg); return false; } - cfg.$target = inject.createTarget(cfg.target); + cfg.$target = this.createTarget(cfg.target); cfg.$injected = cfg.$target; } return true; @@ -383,13 +383,13 @@ const inject = { cfg.source = cfg.source || cfg.defaultSelector; cfg.target = cfg.target || cfg.defaultSelector; - if (!inject.extractModifiers(cfg)) { + if (!this.extractModifiers(cfg)) { return false; } - if (!inject.ensureTarget(cfg, $el)) { + if (!this.ensureTarget(cfg, $el)) { return false; } - inject.listenForFormReset(cfg); + this.listenForFormReset(cfg); return true; }, @@ -404,7 +404,7 @@ const inject = { * Verification for each cfg in the array needs to succeed. */ return cfgs.every( - _.partial(inject.verifySingleConfig, $el, cfgs[0].url) + _.partial(this.verifySingleConfig.bind(this), $el, cfgs[0].url) ); }, @@ -516,8 +516,8 @@ const inject = { const $injected = cfg.$injected || $src; // Now the injection actually happens. - if (inject._inject(trigger, $src, $(target), cfg)) { - inject._afterInjection($el, $injected, cfg); + if (this._inject(trigger, $src, $(target), cfg)) { + this._afterInjection($el, $injected, cfg); } // History support. if subform is submitted, append form params const glue = cfg.url.indexOf("?") > -1 ? "&" : "?"; @@ -533,7 +533,7 @@ const inject = { } // Also inject title element if we have one if (title) - inject._inject(trigger, title, $("title"), { + this._inject(trigger, title, $("title"), { action: "element", }); } @@ -571,7 +571,7 @@ const inject = { } if (cfg.scroll && cfg.scroll !== "none") { - const scroll_container = cfg.$target + let scroll_container = cfg.$target .parents() .addBack() .filter(":scrollable"); @@ -656,8 +656,8 @@ const inject = { $.each(cfgs[0].hooks || [], (idx, hook) => $el.trigger("pat-inject-hook-" + hook) ); - inject.stopBubblingFromRemovedElement($el, cfgs, ev); - const sources$ = await inject.callTypeHandler( + this.stopBubblingFromRemovedElement($el, cfgs, ev); + const sources$ = await this.callTypeHandler( cfgs[0].dataType, "sources", $el, @@ -678,7 +678,7 @@ const inject = { function perform_inject() { if (cfg.target != "none") cfg.$target.each((idx2, target) => { - inject._performInjection( + this._performInjection( target, $el, sources$[idx1], @@ -689,16 +689,16 @@ const inject = { }); } if (cfg.processDelay) { - setTimeout(() => perform_inject(), cfg.processDelay); + setTimeout(() => perform_inject.bind(this)(), cfg.processDelay); } else { - perform_inject(); + perform_inject.bind(this)(); } }); if (cfgs[0].nextHref && $el.is("a")) { // In case next-href is specified the anchor's href will // be set to it after the injection is triggered. $el.attr({ href: cfgs[0].nextHref.replace(/&/g, "&") }); - inject.destroy($el); + this.destroy($el); } $el.off("pat-ajax-success.pat-inject"); $el.off("pat-ajax-error.pat-inject"); @@ -738,10 +738,10 @@ const inject = { */ // get a kinda deep copy, we scribble on it cfgs = cfgs.map((cfg) => $.extend({}, cfg)); - if (!inject.verifyConfig(cfgs, $el)) { + if (!this.verifyConfig(cfgs, $el)) { return; } - if (!inject.askForConfirmation(cfgs)) { + if (!this.askForConfirmation(cfgs)) { return; } if ($el.data("pat-inject-triggered")) { @@ -832,7 +832,7 @@ const inject = { }, _sourcesFromHtml(html, url, sources) { - const $html = inject._parseRawHtml(html, url); + const $html = this._parseRawHtml(html, url); return sources.map((source) => { if (source === "body") { source = "#__original_body"; @@ -895,10 +895,10 @@ const inject = { .parent(); $page - .find(Object.keys(inject._rebaseAttrs).join(",")) + .find(Object.keys(this._rebaseAttrs).join(",")) .each((idx, el_) => { const $el_ = $(el_); - const attrName = inject._rebaseAttrs[el_.tagName]; + const attrName = this._rebaseAttrs[el_.tagName]; let value = $el_.attr(attrName); if ( @@ -941,7 +941,7 @@ const inject = { clean_html = title[0] + clean_html; } try { - clean_html = inject._rebaseHTML(url, clean_html); + clean_html = this._rebaseHTML(url, clean_html); } catch (e) { log.error("Error rebasing urls", e); } @@ -969,7 +969,7 @@ const inject = { return false; } $el.data("pat-inject-autoloaded", true); - inject.onTrigger({ target: $el[0] }); + this.onTrigger({ target: $el[0] }); event && event.preventDefault(); return true; } @@ -1025,7 +1025,7 @@ const inject = { // Because of a resize the element has now a scrollable parent // and we should reset the correct event $(window).off(".pat-autoload", checkVisibility); - return inject._initAutoloadVisible($el); + return this._initAutoloadVisible($el); } if ($el.data("patterns.autoload")) { return false; @@ -1069,7 +1069,7 @@ const inject = { let timer; function onTimeout() { - inject.onTrigger({ target: $el[0] }); + this.onTrigger({ target: $el[0] }); unsub(); clearTimeout(timer); } @@ -1114,13 +1114,13 @@ const inject = { // XXX: simple so far to see what the team thinks of the idea registerTypeHandler(type, handler) { - inject.handlers[type] = handler; + this.handlers[type] = handler; }, async callTypeHandler(type, fn, context, params) { type = type || "html"; - if (inject.handlers[type] && $.isFunction(inject.handlers[type][fn])) { - return await inject.handlers[type][fn].apply(context, params); + if (this.handlers[type] && $.isFunction(this.handlers[type][fn])) { + return await this.handlers[type][fn].bind(this)(...params); } else { return null; } @@ -1131,7 +1131,7 @@ const inject = { sources(cfgs, data) { const sources = cfgs.map((cfg) => cfg.source); sources.push("title"); - return inject._sourcesFromHtml(data, cfgs[0].url, sources); + return this._sourcesFromHtml(data, cfgs[0].url, sources); }, }, }, From 9e3ae07f9d5c6d36d0e3994619634519589f997c Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 15 Dec 2020 12:40:58 +0100 Subject: [PATCH 07/11] pat-inject: Fix this context. See: https://gist.github.com/thet/07145dfbfec9bf6c1213b7fc9f963450 --- src/pat/inject/inject.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index bb142fdd9..a49cad6e8 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -675,7 +675,7 @@ const inject = { title = sources$[sources$.length - 1]; } cfgs.forEach((cfg, idx1) => { - function perform_inject() { + const perform_inject = () => { if (cfg.target != "none") cfg.$target.each((idx2, target) => { this._performInjection( @@ -687,11 +687,11 @@ const inject = { title ); }); - } + }; if (cfg.processDelay) { - setTimeout(() => perform_inject.bind(this)(), cfg.processDelay); + setTimeout(() => perform_inject(), cfg.processDelay); } else { - perform_inject.bind(this)(); + perform_inject(); } }); if (cfgs[0].nextHref && $el.is("a")) { @@ -964,7 +964,7 @@ const inject = { const $scrollable = $el.parents(":scrollable"); // function to trigger the autoload and mark as triggered - function trigger(event) { + const trigger = (event) => { if ($el.data("pat-inject-autoloaded")) { return false; } @@ -972,7 +972,7 @@ const inject = { this.onTrigger({ target: $el[0] }); event && event.preventDefault(); return true; - } + }; $el.click(trigger); // Use case 1: a (heigh-constrained) scrollable parent @@ -1068,11 +1068,11 @@ const inject = { const timeout = parseInt(delay, 10); let timer; - function onTimeout() { + const onTimeout = () => { this.onTrigger({ target: $el[0] }); unsub(); clearTimeout(timer); - } + }; const onInteraction = utils.debounce(() => { if (!document.body.contains($el[0])) { @@ -1083,7 +1083,7 @@ const inject = { timer = setTimeout(onTimeout, timeout); }, timeout); - function unsub() { + const unsub = () => { ["scroll", "resize"].forEach((e) => window.removeEventListener(e, onInteraction) ); @@ -1095,7 +1095,7 @@ const inject = { "touchstart", "touchend", ].forEach((e) => document.removeEventListener(e, onInteraction)); - } + }; onInteraction(); From ae3295d8234fe453324e6c3e7f154f8b00d5301b Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 15 Dec 2020 12:46:19 +0100 Subject: [PATCH 08/11] pat-inject: Reuse function and remove code duplication. --- src/pat/inject/inject.js | 45 +--------------------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index a49cad6e8..443ee08ab 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -95,50 +95,7 @@ const inject = { }); } if (cfgs[0].idleTrigger) { - // XXX TODO: handle item removed from DOM - const timeout = parseInt(cfgs[0].idleTrigger, 10); - let timer; - - const onTimeout = () => { - this.onTrigger({ target: $el[0] }); - unsub(); - clearTimeout(timer); - }; - - const onInteraction = utils.debounce(() => { - clearTimeout(timer); - timer = setTimeout(onTimeout, cfgs[0].trigger); - }, timeout); - - const unsub = () => { - ["scroll", "resize"].forEach((e) => - window.removeEventListener(e, onInteraction) - ); - [ - "click", - "keypress", - "keyup", - "mousemove", - "touchstart", - "touchend", - ].forEach((e) => - document.removeEventListener(e, onInteraction) - ); - }; - - onInteraction(); - - ["scroll", "resize"].forEach((e) => - window.addEventListener(e, onInteraction) - ); - [ - "click", - "keypress", - "keyup", - "mousemove", - "touchstart", - "touchend", - ].forEach((e) => document.addEventListener(e, onInteraction)); + this._initIdleTrigger($el, cfgs[0].idleTrigger); } else { switch (cfgs[0].trigger) { case "default": From 83711839fc764e715ae581e7de54f664bcd1893e Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 16 Dec 2020 12:18:55 +0100 Subject: [PATCH 09/11] pat-inject: Change log messages to log.debug --- src/pat/inject/inject.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index 443ee08ab..3ce488211 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -87,9 +87,9 @@ const inject = { } if (cfgs[0].pushMarker) { $("body").on("push", (event, data) => { - console.log("received push message: " + data); + log.debug("received push message: " + data); if (data == cfgs[0].pushMarker) { - console.log("re-injecting " + data); + log.debug("re-injecting " + data); this.onTrigger({ target: $el[0] }); } }); From b4cc0c34eca3cdc4ad43c80e5789b2ccc5077f78 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 15 Dec 2020 14:34:17 +0100 Subject: [PATCH 10/11] pat inject: Allow configurable error pages. Fixes: https://github.com/Patternslib/Patterns/issues/735 --- CHANGES.md | 1 + src/pat/inject/documentation.md | 12 ++++++ src/pat/inject/index.html | 13 ++++++ src/pat/inject/inject.js | 47 +++++++++++++++----- src/pat/inject/inject.test.js | 76 +++++++++++++++++++++++++++++++++ src/pat/inject/test_404.html | 19 +++++++++ 6 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 src/pat/inject/test_404.html diff --git a/CHANGES.md b/CHANGES.md index 47b71f562..43b70a7e1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -54,6 +54,7 @@ - pat tabs: Refactor based on ``ResizeObserver`` and fix problems calculating the with with transitions. - pat tabs: When clicking on the ``extra-tabs`` element, toggle between ``open`` and ``closed`` classes to allow opening/closing an extra-tabs menu via CSS. - pat autofocus: Do not autofocus in iframes. Fixes: #761. +- pat inject: Allow configurable error pages. ### Technical diff --git a/src/pat/inject/documentation.md b/src/pat/inject/documentation.md index 7e8995087..060ed2792 100644 --- a/src/pat/inject/documentation.md +++ b/src/pat/inject/documentation.md @@ -323,6 +323,18 @@ After injection was triggered: +### Configurable error pages. + +In cases of AJAX errors, you can provide custom error pages by providing custom meta tags. +For example, if you add this meta tag to the html ````: + + + +Then, in case of a ``404`` error, it will try to retrieve the error page and replace the document body contents with the body contents of the error page. +The code looks for a meta tag with the name ``pat-inject-`` plus the HTTP status code. + +Another example: You can present the user with a login page in case the session has expired (``401`` error). + ### Options reference You can customise the behaviour of injection through options in the `data-pat-inject` attribute. diff --git a/src/pat/inject/index.html b/src/pat/inject/index.html index 02ebb413e..1ef25f6f0 100644 --- a/src/pat/inject/index.html +++ b/src/pat/inject/index.html @@ -8,6 +8,7 @@ + Injection pattern