Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/pat/inject/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,5 +387,6 @@ pat-inject fires several JavaScript events which bubble up the DOM tree:
| `pat-inject-content-loaded` | jQuery | images within injected content | true | Triggered on images within the injected content when those images are loaded. |
| `pat-inject-missingSource` | jQuery | trigger which caused the injection | true | Triggered when no to-be-injected source could be found. |
| `pat-inject-missingTarget` | jQuery | trigger which caused the injection | true | Triggered when no target could be found. |
| `pat-inject-before-history-update` | JavaScript | document | true | Trigger just before the history is update, if `history: record` is set. |

Please note: `jQuery.trigger` events can be catched with jQuery only while JavaScript `dispatchEvent` events can be catched with bare JavaScript `addEventListener` and `jQuery.on`.
113 changes: 81 additions & 32 deletions src/pat/inject/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ const inject = {
return $target;
},

_performInjection(target, $el, $sources, cfg, trigger, $title) {
_performInjection(target, $el, $sources, cfg, trigger, ev) {
/* Called after the XHR has succeeded and we have a new $sources
* element to inject.
*/
Expand All @@ -472,7 +472,9 @@ const inject = {
const method = cfg.sourceMod === "content" ? "innerHTML" : "outerHTML";
// There might be multiple sources, so we need to loop over them.
// Access them with "innerHTML" or "outerHTML" depending on the sourceMod.
const sources_string = [...$sources].map(source => source[method]).join("\n");
const sources_string = [...$sources]
.map((source) => source[method])
.join("\n");
wrapper.innerHTML = sources_string;

for (const img of wrapper.content.querySelectorAll("img")) {
Expand All @@ -494,32 +496,92 @@ 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, ev);
// Post-injection
this._afterInjection($el, cfg.$created_target || $(source_nodes), cfg);
}
},

_update_history(cfg, trigger, $title) {
_update_history(cfg, ev) {
// History support. if subform is submitted, append form params
if (cfg.history !== "record" || !history?.pushState) {
return;
}

// We have a URL changing injection. We also need to update other data:
// - title
// - canonical link
// - base url

const html = ev?.jqxhr?.responseText;

// Update the document's title.
const source_title = html?.match(/<title\b[^>]*>(.*?)<\/title>/i)?.[0];
const target_title = document.querySelector("head title");
// Title: update/add but never remove
this._update_head(source_title, target_title, false);

// Update the <link rel="canonical"> element.
const source_canonical = html?.match(
/<link\b[^>]*\brel\s*=\s*["']canonical["'][^>]*>/i
)?.[0];
const target_canonical = document.querySelector("head link[rel=canonical]");
// Canonical: update/add/remove as needed
this._update_head(source_canonical, target_canonical, true);

// Update the base tag.
const source_base = html?.match(/<base\b[^>]*>/i)?.[0];
const target_base = document.querySelector("head base");
// Base: update/add/remove as needed
this._update_head(source_base, target_base, true);

document.dispatchEvent(
new Event("pat-inject-before-history-update", { detail: { ajax_event: ev } })
);

// At last position - other patterns can react on already changed title,
// canonical or base.
let url = cfg.url;
if (cfg.params) {
const glue = url.indexOf("?") > -1 ? "&" : "?";
url = `${url}${glue}${cfg.params}`;
}
history.pushState({ url: url }, "", url);
// Also inject title element if we have one
if ($title?.length) {
const title_el = document.querySelector("title");
if (title_el) {
this._inject(trigger, $title, title_el, {
action: "element",
});
},

_update_head(source_string, target_el, delete_when_empty = true) {
if (!source_string) {
// No source element found in the HTML
if (target_el && delete_when_empty) {
// Source doesn't have the target element equivalent. Remove the
// target. This prevents incorrect canonical and base URLs after a
// navigation URL change.
target_el.remove();
}
// (If delete_when_empty is false, keep the target - used for title)
// (If no target_el exists, nothing to do)
return;
}

const parser = new DOMParser();
const parsed = parser.parseFromString(source_string, "text/html");
// Head elements (title, link, base) are placed in parsed.head.
const source_el = parsed.head.children[0];

if (source_el) {
// Source is present - update or add
if (target_el) {
// Replace existing target element
target_el.replaceWith(source_el);
} else {
// Add new element to head
document.head.prepend(source_el);
}
} else if (target_el && delete_when_empty) {
// Source string exists but doesn't parse to an element. Remove target.
target_el.remove();
}
// (If source doesn't parse to element AND no target, do nothing)
},

_afterInjection($el, $injected, cfg) {
Expand Down Expand Up @@ -592,17 +654,6 @@ const inject = {
data,
ev,
]);
/* pick the title source for dedicated handling later
Title - if present - is always appended at the end. */
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];
}

for (const [idx1, cfg] of cfgs.entries()) {
const perform_inject = () => {
Expand All @@ -614,7 +665,7 @@ const inject = {
sources$[idx1],
cfg,
ev.target,
$title
ev
);
}
}
Expand Down Expand Up @@ -834,19 +885,20 @@ const inject = {

_sourcesFromHtml(html, url, sources) {
const $html = this._parseRawHtml(html, url);
return sources.map((source) => {
const $sources = sources.map((source) => {
// Special case for body
if (source === "body") {
source = "#__original_body";
}

// Special case for "none";
if (source === "none") {
return $("<!-- -->");
}
const $source = $html.find(source);

if ($source.length === 0) {
if (source != "title") {
log.warn("No source elements for selector:", source, $html);
}
log.warn("No source elements for selector:", source, $html);
}

$source.find('a[href^="#"]').each((idx, el_) => {
Expand All @@ -868,6 +920,7 @@ const inject = {
});
return $source;
});
return $sources;
},

_rebaseAttrs: {
Expand Down Expand Up @@ -973,7 +1026,6 @@ const inject = {

_parseRawHtml(html, url = "") {
// remove script tags and head and replace body by a div
const title = html.match(/\<title\>(.*)\<\/title\>/);
let clean_html = html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
.replace(/<head\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/head>/gi, "")
Expand All @@ -982,14 +1034,12 @@ const inject = {
.replace(/<body([^>]*?)>/gi, '<div id="__original_body">')
.replace(/<\/body([^>]*?)>/gi, "</div>");

if (title && title.length == 2) {
clean_html = title[0] + clean_html;
}
try {
clean_html = this._rebaseHTML(url, clean_html);
} catch (e) {
log.error("Error rebasing urls", e);
}

const $html = $("<div/>").html(clean_html);
if ($html.children().length === 0) {
log.warn("Parsing html resulted in empty jquery object:", clean_html);
Expand Down Expand Up @@ -1121,7 +1171,6 @@ const inject = {
html: {
sources(cfgs, data) {
const sources = cfgs.map((cfg) => cfg.source);
sources.push("title");
const result = this._sourcesFromHtml(data, cfgs[0].url, sources);
return result;
},
Expand Down
Loading
Loading