diff --git a/CHANGES.md b/CHANGES.md index 341b927bc..620694f82 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -55,6 +55,8 @@ - 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. Can be disabled by adding ``pat-inject-errorhandler.off`` to the URL's query string. +- core utils: Add ``jqToNode`` to return a DOM node if a jQuery node was passed. +- pat inject: Rebase URLs in pattern configuration attributes. This avoids URLs in pattern configuration to point to unreachable paths in the context where the result is injected into. ### Technical @@ -82,7 +84,8 @@ Configure ``style_loader`` to insert CSS at the TOP of the html ```` Provide a webpack-helpers module with a ``top_head_insert`` function which can be reused in depending projects. - Build infra: Switch the CI system to GitHub Actions and drop Travis CI. - +- core base: Add the parser instance to pattern attributes if available. + We can then reuse the parser from registered patterns. This is used in the ``_rebaseHTML`` method of pat-inject to URL-rebase the pattern configuration. ### Fixes diff --git a/src/core/base.js b/src/core/base.js index 648d82840..52a0f1047 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -17,15 +17,15 @@ import Registry from "./registry"; import logging from "./logging"; import mockupParser from "./mockup-parser"; -var log = logging.getLogger("Patternslib Base"); +const log = logging.getLogger("Patternslib Base"); -var initBasePattern = function ($el, options, trigger) { +const initBasePattern = function ($el, options, trigger) { if (!$el.jquery) { $el = $($el); } - var name = this.prototype.name; - var log = logging.getLogger("pat." + name); - var pattern = $el.data("pattern-" + name); + const name = this.prototype.name; + const plog = logging.getLogger(`pat.${name}`); + let pattern = $el.data(`pattern-${name}`); if (pattern === undefined && Registry.patterns[name]) { try { options = @@ -34,14 +34,14 @@ var initBasePattern = function ($el, options, trigger) { : options; pattern = new Registry.patterns[name]($el, options, trigger); } catch (e) { - log.error("Failed while initializing '" + name + "' pattern.", e); + plog.error(`Failed while initializing ${name} pattern.`, e); } - $el.data("pattern-" + name, pattern); + $el.data(`pattern-${name}`, pattern); } return pattern; }; -var Base = function ($el, options, trigger) { +const Base = function ($el, options, trigger) { if (!$el.jquery) { $el = $($el); } @@ -54,23 +54,23 @@ var Base = function ($el, options, trigger) { Base.prototype = { constructor: Base, - on: function (eventName, eventCallback) { - this.$el.on(eventName + "." + this.name + ".patterns", eventCallback); + on(eventName, eventCallback) { + this.$el.on(`${eventName}.${this.name}.patterns`, eventCallback); }, - emit: function (eventName, args) { + emit(eventName, args) { // args should be a list if (args === undefined) { args = []; } - this.$el.trigger(eventName + "." + this.name + ".patterns", args); + this.$el.trigger(`${eventName}.${this.name}.patterns`, args); }, }; Base.extend = function (patternProps) { /* Helper function to correctly set up the prototype chain for new patterns. */ - var parent = this; - var child; + const parent = this; + let child; // Check that the required configuration properties are given. if (!patternProps) { @@ -97,6 +97,7 @@ Base.extend = function (patternProps) { child.init = initBasePattern; child.jquery_plugin = true; child.trigger = patternProps.trigger; + child.parser = patternProps?.parser || null; // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function. @@ -120,10 +121,7 @@ Base.extend = function (patternProps) { ); } else if (!patternProps.trigger) { log.warn( - "The pattern '" + - patternProps.name + - "' does not " + - "have a trigger attribute, it will not be registered." + `The pattern ${patternProps.name} does not have a trigger attribute, it will not be registered.` ); } else { Registry.register(child, patternProps.name); diff --git a/src/core/dom.js b/src/core/dom.js index 66f0c9052..6c8b2b2c4 100644 --- a/src/core/dom.js +++ b/src/core/dom.js @@ -57,7 +57,8 @@ const find_parents = (el, selector) => { // This matches against all parents but not the element itself. // The order of elements is from the search starting point up to higher // DOM levels. - let parent = el.parentNode?.closest(selector) || null; + let parent = + (el?.parentNode?.closest && el.parentNode.closest(selector)) || null; const ret = []; while (parent) { ret.push(parent); diff --git a/src/core/dom.test.js b/src/core/dom.test.js index b0f178262..70e09f37b 100644 --- a/src/core/dom.test.js +++ b/src/core/dom.test.js @@ -173,6 +173,24 @@ describe("core.dom tests", () => { expect(res[0]).toEqual(document.querySelector(".level3")); // inner dom levels first // prettier-ignore expect(res[1]).toEqual(document.querySelector(".level1")); + done(); + }); + it("don't break with no element.", (done) => { + const res = dom.find_parents(null, ".findme"); + expect(res.length).toEqual(0); + + done(); + }); + it("don't break with DocumentFragment without a parent.", (done) => { + const el = new DocumentFragment(); + el.innerHTML = `
`; + console.log(el.parentNode); + const res = dom.find_parents( + el.querySelector(".starthere"), + ".findme" + ); + expect(res.length).toEqual(0); + done(); }); }); diff --git a/src/core/mockup-parser.js b/src/core/mockup-parser.js index d09eb8746..65de6478a 100644 --- a/src/core/mockup-parser.js +++ b/src/core/mockup-parser.js @@ -1,7 +1,7 @@ import $ from "jquery"; var parser = { - getOptions: function getOptions($el, patternName, options) { + getOptions($el, patternName, options) { /* This is the Mockup parser. An alternative parser for Patternslib * patterns. * @@ -13,23 +13,23 @@ var parser = { options = options || {}; // get options from parent element first, stop if element tag name is 'body' if ($el.length !== 0 && !$.nodeName($el[0], "body")) { - options = getOptions($el.parent(), patternName, options); + options = this.getOptions($el.parent(), patternName, options); } // collect all options from element - var elOptions = {}; + let elOptions = {}; if ($el.length !== 0) { elOptions = $el.data("pat-" + patternName); if (elOptions) { // parse options if string if (typeof elOptions === "string") { - var tmpOptions = {}; + const tmpOptions = {}; $.each(elOptions.split(";"), function (i, item) { item = item.split(":"); item.reverse(); - var key = item.pop(); + let key = item.pop(); key = key.replace(/^\s+|\s+$/g, ""); // trim item.reverse(); - var value = item.join(":"); + let value = item.join(":"); value = value.replace(/^\s+|\s+$/g, ""); // trim tmpOptions[key] = value; }); diff --git a/src/core/parser.js b/src/core/parser.js index ae760d6ca..d38f77451 100644 --- a/src/core/parser.js +++ b/src/core/parser.js @@ -1,39 +1,30 @@ -/** - * Patterns parser - Argument parser - * - * Copyright 2012-2013 Florian Friesdorf - * Copyright 2012-2013 Simplon B.V. - Wichert Akkerman - */ - +// Patterns argument parser import $ from "jquery"; -import _ from "underscore"; import utils from "./utils.js"; import logging from "./logging"; -function ArgumentParser(name) { - this.order = []; - this.parameters = {}; - this.attribute = "data-pat-" + name; - this.enum_values = {}; - this.enum_conflicts = []; - this.groups = {}; - this.possible_groups = {}; - this.log = logging.getLogger(name + ".parser"); -} +class ArgumentParser { + constructor(name) { + this.order = []; + this.parameters = {}; + this.attribute = "data-pat-" + name; + this.enum_values = {}; + this.enum_conflicts = []; + this.groups = {}; + this.possible_groups = {}; + this.log = logging.getLogger(name + ".parser"); -ArgumentParser.prototype = { - group_pattern: /([a-z][a-z0-9]*)-([A-Z][a-z0-0\-]*)/i, - json_param_pattern: /^\s*{/i, - named_param_pattern: /^\s*([a-z][a-z0-9\-]*)\s*:(.*)/i, - token_pattern: /((["']).*?(?!\\)\2)|\s*(\S+)\s*/g, + this.group_pattern = /([a-z][a-z0-9]*)-([A-Z][a-z0-0\-]*)/i; + this.json_param_pattern = /^\s*{/i; + this.named_param_pattern = /^\s*([a-z][a-z0-9\-]*)\s*:(.*)/i; + this.token_pattern = /((["']).*?(?!\\)\2)|\s*(\S+)\s*/g; + } - _camelCase: function (str) { - return str.replace(/\-([a-z])/g, function (_, p1) { - return p1.toUpperCase(); - }); - }, + _camelCase(str) { + return str.replace(/\-([a-z])/g, (__, p1) => p1.toUpperCase()); + } - addAlias: function argParserAddAlias(alias, original) { + addAlias(alias, original) { /* Add an alias for a previously added parser argument. * * Useful when you want to support both US and UK english argument @@ -50,21 +41,21 @@ ArgumentParser.prototype = { '".' ); } - }, + } - addGroupToSpec: function argParserAddGroupToSpec(spec) { + addGroupToSpec(spec) { /* Determine wether an argument being parsed can be grouped and * update its specifications object accordingly. * * Internal method used by addArgument and addJSONArgument */ - var m = spec.name.match(this.group_pattern); + const m = spec.name.match(this.group_pattern); if (m) { - var group = m[1], - field = m[2]; + const group = m[1]; + const field = m[2]; if (group in this.possible_groups) { - var first_spec = this.possible_groups[group], - first_name = first_spec.name.match(this.group_pattern)[2]; + const first_spec = this.possible_groups[group]; + const first_name = first_spec.name.match(this.group_pattern)[2]; first_spec.group = group; first_spec.dest = first_name; this.groups[group] = new ArgumentParser(); @@ -91,9 +82,9 @@ ArgumentParser.prototype = { } } return spec; - }, + } - addJSONArgument: function argParserAddJSONArgument(name, default_value) { + addJSONArgument(name, default_value) { /* Add an argument where the value is provided in JSON format. * * This is a different usecase than specifying all arguments to @@ -109,15 +100,10 @@ ArgumentParser.prototype = { group: null, type: "json", }); - }, + } - addArgument: function ArgParserAddArgument( - name, - default_value, - choices, - multiple - ) { - var spec = { + addArgument(name, default_value, choices, multiple) { + const spec = { name: name, value: multiple && !Array.isArray(default_value) @@ -130,14 +116,14 @@ ArgumentParser.prototype = { if (choices && Array.isArray(choices) && choices.length) { spec.choices = choices; spec.type = this._typeof(choices[0]); - for (var i = 0; i < choices.length; i++) { - if (this.enum_conflicts.indexOf(choices[i]) !== -1) { + for (const choice of choices) { + if (this.enum_conflicts.indexOf(choice) !== -1) { continue; - } else if (choices[i] in this.enum_values) { - this.enum_conflicts.push(choices[i]); - delete this.enum_values[choices[i]]; + } else if (choice in this.enum_values) { + this.enum_conflicts.push(choice); + delete this.enum_values[choice]; } else { - this.enum_values[choices[i]] = name; + this.enum_values[choice] = name; } } } else if ( @@ -151,16 +137,17 @@ ArgumentParser.prototype = { } this.order.push(name); this.parameters[name] = this.addGroupToSpec(spec); - }, + } - _typeof: function argParserTypeof(obj) { - var type = typeof obj; - if (obj === null) return "null"; - return type; - }, + _typeof(obj) { + if (obj === null) { + return "null"; + } + return typeof obj; + } - _coerce: function argParserCoerce(name, value) { - var spec = this.parameters[name]; + _coerce(name, value) { + const spec = this.parameters[name]; if (typeof value !== spec.type) try { switch (spec.type) { @@ -170,7 +157,7 @@ ArgumentParser.prototype = { case "boolean": if (typeof value === "string") { value = value.toLowerCase(); - var num = parseInt(value, 10); + const num = parseInt(value, 10); if (!isNaN(num)) value = !!num; else value = @@ -178,31 +165,35 @@ ArgumentParser.prototype = { value === "y" || value === "yes" || value === "y"; - } else if (typeof value === "number") value = !!value; - else + } else if (typeof value === "number") { + value = !!value; + } else { throw ( "Cannot convert value for " + name + " to boolean" ); + } break; case "number": if (typeof value === "string") { value = parseInt(value, 10); - if (isNaN(value)) + if (isNaN(value)) { throw ( "Cannot convert value for " + name + " to number" ); - } else if (typeof value === "boolean") + } + } else if (typeof value === "boolean") { value = value + 0; - else + } else { throw ( "Cannot convert value for " + name + " to number" ); + } break; case "string": value = value.toString(); @@ -228,17 +219,15 @@ ArgumentParser.prototype = { return null; } return value; - }, + } - _set: function argParserSet(opts, name, value) { + _set(opts, name, value) { if (!(name in this.parameters)) { this.log.debug("Ignoring value for unknown argument " + name); return; } - var spec = this.parameters[name], - parts, - i, - v; + const spec = this.parameters[name]; + let parts; if (spec.multiple) { if (typeof value === "string") { parts = value.split(/,+/); @@ -246,88 +235,84 @@ ArgumentParser.prototype = { parts = value; } value = []; - for (i = 0; i < parts.length; i++) { - v = this._coerce(name, parts[i].trim()); - if (v !== null) value.push(v); + for (const part of parts) { + const v = this._coerce(name, part.trim()); + if (v !== null) { + value.push(v); + } } } else { value = this._coerce(name, value); - if (value === null) return; + if (value === null) { + return; + } } opts[name] = value; - }, + } - _split: function argParserSplit(text) { - var tokens = []; - text.replace(this.token_pattern, function (match, quoted, _, simple) { - if (quoted) tokens.push(quoted); - else if (simple) tokens.push(simple); + _split(text) { + const tokens = []; + text.replace(this.token_pattern, (match, quoted, __, simple) => { + if (quoted) { + tokens.push(quoted); + } else if (simple) { + tokens.push(simple); + } }); return tokens; - }, + } - _parseExtendedNotation: function argParserParseExtendedNotation(argstring) { - var opts = {}; - var parts = argstring + _parseExtendedNotation(argstring) { + const opts = {}; + const parts = argstring .replace(/;;/g, "\0x1f") .replace(/&/g, "&\0x1f") .split(/;/) - .map(function (el) { - return el.replace(new RegExp("\0x1f", "g"), ";"); - }); - _.each( - parts, - function (part) { - if (!part) { - return; - } - var matches = part.match(this.named_param_pattern); - if (!matches) { - this.log.warn( - "Invalid parameter: " + part + ": " + argstring - ); - return; - } - var name = matches[1], - value = matches[2].trim(), - arg = _.chain(this.parameters) - .where({ alias: name }) - .value(), - is_alias = arg.length === 1; + .map((el) => el.replace(new RegExp("\0x1f", "g"), ";")); + for (const part of parts) { + if (!part) { + continue; + } + const matches = part.match(this.named_param_pattern); + if (!matches) { + this.log.warn("Invalid parameter: " + part + ": " + argstring); + continue; + } + const name = matches[1]; + const value = matches[2].trim(); + const arg = Object.values(this.parameters).filter( + (it) => it.alias === name + ); - if (is_alias) { - this._set(opts, arg[0].name, value); - } else if (name in this.parameters) { - this._set(opts, name, value); - } else if (name in this.groups) { - var subopt = this.groups[name]._parseShorthandNotation( - value - ); - for (var field in subopt) { - this._set(opts, name + "-" + field, subopt[field]); - } - } else { - this.log.warn("Unknown named parameter " + matches[1]); - return; + const is_alias = arg.length === 1; + + if (is_alias) { + this._set(opts, arg[0].name, value); + } else if (name in this.parameters) { + this._set(opts, name, value); + } else if (name in this.groups) { + const subopt = this.groups[name]._parseShorthandNotation(value); + for (const field in subopt) { + this._set(opts, name + "-" + field, subopt[field]); } - }.bind(this) - ); + } else { + this.log.warn("Unknown named parameter " + matches[1]); + continue; + } + } return opts; - }, + } - _parseShorthandNotation: function argParserParseShorthandNotation( - parameter - ) { - var parts = this._split(parameter), - opts = {}, - positional = true, - i = 0, - part, - flag, - sense; + _parseShorthandNotation(parameter) { + const parts = this._split(parameter); + const opts = {}; + let i = 0; while (parts.length) { - part = parts.shift().trim(); + const part = parts.shift().trim(); + let sense; + let flag; + let positional = true; if (part.slice(0, 3) === "no-") { sense = false; flag = part.slice(3); @@ -357,10 +342,9 @@ ArgumentParser.prototype = { if (parts.length) this.log.warn("Ignore extra arguments: " + parts.join(" ")); return opts; - }, + } - _parse: function argParser_parse(parameter) { - var opts, extended, sep; + _parse(parameter) { if (!parameter) { return {}; } @@ -374,19 +358,21 @@ ArgumentParser.prototype = { if (parameter.match(this.named_param_pattern)) { return this._parseExtendedNotation(parameter); } - sep = parameter.indexOf(";"); + const sep = parameter.indexOf(";"); if (sep === -1) { return this._parseShorthandNotation(parameter); } - opts = this._parseShorthandNotation(parameter.slice(0, sep)); - extended = this._parseExtendedNotation(parameter.slice(sep + 1)); - for (var name in extended) opts[name] = extended[name]; + const opts = this._parseShorthandNotation(parameter.slice(0, sep)); + const extended = this._parseExtendedNotation(parameter.slice(sep + 1)); + for (const name in extended) { + opts[name] = extended[name]; + } return opts; - }, + } - _defaults: function argParserDefaults($el) { - var result = {}; - for (var name in this.parameters) + _defaults($el) { + const result = {}; + for (const name in this.parameters) if (typeof this.parameters[name].value === "function") try { result[name] = this.parameters[name].value($el, name); @@ -396,19 +382,12 @@ ArgumentParser.prototype = { } else result[name] = this.parameters[name].value; return result; - }, - - _cleanupOptions: function argParserCleanupOptions(options) { - var keys = Object.keys(options), - i, - spec, - name, - target; + } + _cleanupOptions(options) { // Resolve references - for (i = 0; i < keys.length; i++) { - name = keys[i]; - spec = this.parameters[name]; + for (const name of Object.keys(options)) { + const spec = this.parameters[name]; if (spec === undefined) continue; if ( @@ -419,10 +398,9 @@ ArgumentParser.prototype = { options[name] = options[spec.value.slice(1)]; } // Move options into groups and do renames - keys = Object.keys(options); - for (i = 0; i < keys.length; i++) { - name = keys[i]; - spec = this.parameters[name]; + for (const name of Object.keys(options)) { + const spec = this.parameters[name]; + let target; if (spec === undefined) continue; if (spec.group) { @@ -439,20 +417,21 @@ ArgumentParser.prototype = { } } return options; - }, + } - parse: function argParserParse($el, options, multiple, inherit) { + parse($el, options, multiple, inherit) { if (!$el.jquery) { $el = $($el); } if (typeof options === "boolean" && multiple === undefined) { + // Fix argument order: ``multiple`` passed instead of ``options``. multiple = options; options = {}; } inherit = inherit !== false; - var stack = inherit ? [[this._defaults($el)]] : [[{}]]; - var $possible_config_providers; - var final_length = 1; + const stack = inherit ? [[this._defaults($el)]] : [[{}]]; + let $possible_config_providers; + let final_length = 1; /* * XXX this is a workaround for: * - https://github.com/Patternslib/Patterns/issues/393 @@ -475,22 +454,21 @@ ArgumentParser.prototype = { .addBack(); } - _.each( - $possible_config_providers, - function (provider) { - var data, frame, _parse; - data = $(provider).attr(this.attribute); - if (!data) { - return; - } - _parse = this._parse.bind(this); - if (data.match(/&&/)) - frame = data.split(/\s*&&\s*/).map(_parse); - else frame = [_parse(data)]; - final_length = Math.max(frame.length, final_length); - stack.push(frame); - }.bind(this) - ); + for (const provider of $possible_config_providers) { + let frame; + const data = $(provider).attr(this.attribute); + if (!data) { + continue; + } + const _parse = this._parse.bind(this); + if (data.match(/&&/)) { + frame = data.split(/\s*&&\s*/).map(_parse); + } else { + frame = [_parse(data)]; + } + final_length = Math.max(frame.length, final_length); + stack.push(frame); + } if (typeof options === "object") { if (Array.isArray(options)) { @@ -501,16 +479,12 @@ ArgumentParser.prototype = { if (!multiple) { final_length = 1; } - var results = _.map( - _.compose( - utils.removeDuplicateObjects, - _.partial(utils.mergeStack, _, final_length) - )(stack), - this._cleanupOptions.bind(this) - ); + const results = utils + .removeDuplicateObjects(utils.mergeStack(stack, final_length)) + .map(this._cleanupOptions.bind(this)); return multiple ? results : results[0]; - }, -}; + } +} // BBB ArgumentParser.prototype.add_argument = ArgumentParser.prototype.addArgument; diff --git a/src/core/registry.js b/src/core/registry.js index 994d3e950..e584af682 100644 --- a/src/core/registry.js +++ b/src/core/registry.js @@ -17,19 +17,16 @@ * - set pattern.jquery_plugin if you want it */ import $ from "jquery"; -import _ from "underscore"; +import dom from "./dom"; import logging from "./logging"; import utils from "./utils"; -// below here modules that are only loaded -import "./jquery-ext"; - -var log = logging.getLogger("registry"), - disable_re = /patterns-disable=([^&]+)/g, - dont_catch_re = /patterns-dont-catch/g, - dont_catch = false, - disabled = {}, - match; +const log = logging.getLogger("registry"); +const disable_re = /patterns-disable=([^&]+)/g; +const dont_catch_re = /patterns-dont-catch/g; +const disabled = {}; +let dont_catch = false; +let match; while ((match = disable_re.exec(window.location.search)) !== null) { disabled[match[1]] = true; @@ -41,14 +38,14 @@ while ((match = dont_catch_re.exec(window.location.search)) !== null) { log.info("I will not catch init exceptions"); } -var registry = { +const registry = { patterns: {}, // as long as the registry is not initialized, pattern // registration just registers a pattern. Once init is called, // the DOM is scanned. After that registering a new pattern // results in rescanning the DOM only for this pattern. initialized: false, - init: function registry_init() { + init() { $(document).ready(function () { log.info( "loaded: " + Object.keys(registry.patterns).sort().join(", ") @@ -59,13 +56,13 @@ var registry = { }); }, - clear: function clearRegistry() { + clear() { // Removes all patterns from the registry. Currently only being // used in tests. this.patterns = {}; }, - transformPattern: function (name, content) { + transformPattern(name, content) { /* Call the transform method on the pattern with the given name, if * it exists. */ @@ -88,14 +85,14 @@ var registry = { } }, - initPattern: function (name, el, trigger) { + initPattern(name, el, trigger) { /* Initialize the pattern with the provided name and in the context * of the passed in DOM element. */ - var $el = $(el); - var pattern = registry.patterns[name]; + const $el = $(el); + const pattern = registry.patterns[name]; if (pattern.init) { - var plog = logging.getLogger("pat." + name); + const plog = logging.getLogger("pat." + name); if ($el.is(pattern.trigger)) { plog.debug("Initialising:", $el); try { @@ -111,54 +108,61 @@ var registry = { } }, - orderPatterns: function (patterns) { + orderPatterns(patterns) { // XXX: Bit of a hack. We need the validation pattern to be // parsed and initiated before the inject pattern. So we make // sure here, that it appears first. Not sure what would be // the best solution. Perhaps some kind of way to register // patterns "before" or "after" other patterns. - if ( - _.contains(patterns, "validation") && - _.contains(patterns, "inject") - ) { + if (patterns.includes("validation") && patterns.includes("inject")) { patterns.splice(patterns.indexOf("validation"), 1); patterns.unshift("validation"); } return patterns; }, - scan: function registryScan(content, patterns, trigger) { - var selectors = [], - $match; + scan(content, patterns, trigger) { + if (typeof content === "string") { + content = document.querySelector(content); + } else if (content.jquery) { + content = content[0]; + } + + const selectors = []; patterns = this.orderPatterns( patterns || Object.keys(registry.patterns) ); - patterns.forEach(_.partial(this.transformPattern, _, content)); - patterns = _.each(patterns, function (name) { - var pattern = registry.patterns[name]; + for (const name of patterns) { + this.transformPattern(name, content); + const pattern = registry.patterns[name]; if (pattern.trigger) { selectors.unshift(pattern.trigger); } - }); - $match = $(content).findInclusive(selectors.join(",")); // Find all DOM elements belonging to a pattern - $match = $match.filter(function () { + } + + let matches = dom.querySelectorAllAndMe( + content, + selectors.map((it) => it.trim().replace(/,$/, "")).join(",") + ); + matches = matches.filter((el) => { // Filter out code examples wrapped in
 elements.
-            return $(this).parents("pre").length === 0;
+            // Also filter special class ``.cant-touch-this``
+            return (
+                dom.find_parents(el, "pre").length === 0 &&
+                !el.matches(".cant-touch-this")
+            );
         });
-        $match = $match.filter(":not(.cant-touch-this)");
 
         // walk list backwards and initialize patterns inside-out.
-        $match.toArray().reduceRight(
-            function registryInitPattern(acc, el) {
-                patterns.forEach(_.partial(this.initPattern, _, el, trigger));
-            }.bind(this),
-            null
-        );
-        $("body").addClass("patterns-loaded");
+        for (const el of matches.reverse()) {
+            for (const name of patterns) {
+                this.initPattern(name, el, trigger);
+            }
+        }
+        document.body.classList.add("patterns-loaded");
     },
 
-    register: function registry_register(pattern, name) {
-        var plugin_name;
+    register(pattern, name) {
         name = name || pattern.name;
         if (!name) {
             log.error("Pattern lacks a name:", pattern);
@@ -174,12 +178,12 @@ var registry = {
 
         // register pattern as jquery plugin
         if (pattern.jquery_plugin) {
-            plugin_name = ("pat-" + name).replace(/-([a-zA-Z])/g, function (
-                match,
-                p1
-            ) {
-                return p1.toUpperCase();
-            });
+            const plugin_name = ("pat-" + name).replace(
+                /-([a-zA-Z])/g,
+                function (match, p1) {
+                    return p1.toUpperCase();
+                }
+            );
             $.fn[plugin_name] = utils.jqueryPlugin(pattern);
             // BBB 2012-12-10 and also for Mockup patterns.
             $.fn[plugin_name.replace(/^pat/, "pattern")] = $.fn[plugin_name];
diff --git a/src/core/utils.js b/src/core/utils.js
index 02c0c238c..d152bec48 100644
--- a/src/core/utils.js
+++ b/src/core/utils.js
@@ -548,6 +548,14 @@ const isIE = () => {
     return /*@cc_on!@*/ false || !!document.documentMode;
 };
 
+const jqToNode = (el) => {
+    // Return a DOM node if a jQuery node was passed.
+    if (el.jquery) {
+        el = el[0];
+    }
+    return el;
+};
+
 var utils = {
     // pattern pimping - own module?
     jqueryPlugin: jqueryPlugin,
@@ -572,6 +580,7 @@ var utils = {
     timeout: timeout,
     debounce: debounce,
     isIE: isIE,
+    jqToNode: jqToNode,
 };
 
 export default utils;
diff --git a/src/core/utils.test.js b/src/core/utils.test.js
index fe1e1bd4d..d8367cd40 100644
--- a/src/core/utils.test.js
+++ b/src/core/utils.test.js
@@ -601,3 +601,17 @@ describe("getCSSValue", function () {
         expect(utils.getCSSValue(el2, "margin-bottom", true)).toBe(24.0);
     });
 });
+
+describe("core.utils tests", () => {
+    describe("jqToNode tests", () => {
+        it("always returns a bare DOM node no matter if a jQuery or bare DOM node was passed.", (done) => {
+            const el = document.createElement("div");
+            const $el = $(el);
+
+            expect(utils.jqToNode($el)).toBe(el);
+            expect(utils.jqToNode(el)).toBe(el);
+
+            done();
+        });
+    });
+});
diff --git a/src/pat/auto-submit/auto-submit.js b/src/pat/auto-submit/auto-submit.js
index 99d58976b..393e4870d 100644
--- a/src/pat/auto-submit/auto-submit.js
+++ b/src/pat/auto-submit/auto-submit.js
@@ -1,8 +1,9 @@
+import "../../core/jquery-ext";
 import $ from "jquery";
-import logging from "../../core/logging";
 import Base from "../../core/base";
-import Parser from "../../core/parser";
 import input_change_events from "../../lib/input-change-events";
+import logging from "../../core/logging";
+import Parser from "../../core/parser";
 import utils from "../../core/utils";
 
 const log = logging.getLogger("autosubmit");
diff --git a/src/pat/auto-suggest/auto-suggest.js b/src/pat/auto-suggest/auto-suggest.js
index 28444d356..d76f02906 100644
--- a/src/pat/auto-suggest/auto-suggest.js
+++ b/src/pat/auto-suggest/auto-suggest.js
@@ -1,4 +1,5 @@
 import "regenerator-runtime/runtime"; // needed for ``await`` support
+import "../../core/jquery-ext";
 import $ from "jquery";
 import Base from "../../core/base";
 import logging from "../../core/logging";
diff --git a/src/pat/autofocus/autofocus.js b/src/pat/autofocus/autofocus.js
index a82a6118d..5c7d2c795 100644
--- a/src/pat/autofocus/autofocus.js
+++ b/src/pat/autofocus/autofocus.js
@@ -6,7 +6,16 @@ let registered_event_handler = false;
 
 export default Base.extend({
     name: "autofocus",
-    trigger: ":input.pat-autofocus,:input[autofocus]",
+    trigger: `
+        input.pat-autofocus,
+        input[autofocus],
+        select.pat-autofocus,
+        select[autofocus],
+        textarea.pat-autofocus,
+        textarea[autofocus],
+        button.pat-autofocus,
+        button[autofocus]
+    `,
 
     init() {
         if (window.self !== window.top) {
diff --git a/src/pat/bumper/bumper.js b/src/pat/bumper/bumper.js
index 9d9dfd7ea..8c4b0317f 100644
--- a/src/pat/bumper/bumper.js
+++ b/src/pat/bumper/bumper.js
@@ -6,10 +6,11 @@
  * Copyright 2013-2014 Simplon B.V. - Wichert Akkerman
  */
 
+import "../../core/jquery-ext";
 import $ from "jquery";
 import _ from "underscore";
-import Parser from "../../core/parser";
 import Base from "../../core/base";
+import Parser from "../../core/parser";
 import utils from "../../core/utils";
 
 const parser = new Parser("bumper");
diff --git a/src/pat/calendar/calendar.js b/src/pat/calendar/calendar.js
index 156064293..851c7e048 100644
--- a/src/pat/calendar/calendar.js
+++ b/src/pat/calendar/calendar.js
@@ -85,6 +85,7 @@ export default Base.extend({
     },
     dayNames: ["su", "mo", "tu", "we", "th", "fr", "sa"],
     active_categories: null,
+    parser: parser,
 
     async init($el, opts) {
         const el = this.el;
diff --git a/src/pat/calendar/index.html b/src/pat/calendar/index.html
index 1cc22e67d..1bbbe7351 100644
--- a/src/pat/calendar/index.html
+++ b/src/pat/calendar/index.html
@@ -15,9 +15,9 @@
 
         
- -
Click me - I explicitly start out closed and will fade in
+ data-pat-collapsible="load-content: ./collapsible-sources.html#panel1; close-trigger: #close; open-trigger: #open">

Click me! - I will load my content when opened

diff --git a/src/pat/date-picker/date-picker.js b/src/pat/date-picker/date-picker.js index cb06bb298..5e71f91ec 100644 --- a/src/pat/date-picker/date-picker.js +++ b/src/pat/date-picker/date-picker.js @@ -29,6 +29,7 @@ parser.addAlias("behaviour", "behavior"); export default Base.extend({ name: "date-picker", trigger: ".pat-date-picker", + parser: parser, async init() { const el = this.el; diff --git a/src/pat/date-picker/index.html b/src/pat/date-picker/index.html index a513a66b1..2c54f1213 100644 --- a/src/pat/date-picker/index.html +++ b/src/pat/date-picker/index.html @@ -76,7 +76,7 @@ >Multilingual support with German translations diff --git a/src/pat/datetime-picker/datetime-picker.js b/src/pat/datetime-picker/datetime-picker.js index 6ed08104c..e5dd5161f 100644 --- a/src/pat/datetime-picker/datetime-picker.js +++ b/src/pat/datetime-picker/datetime-picker.js @@ -20,6 +20,7 @@ parser.addArgument("first-day", 0); export default Base.extend({ name: "datetime-picker", trigger: ".pat-datetime-picker", + parser: parser, async init() { const el = this.el; diff --git a/src/pat/inject/index.html b/src/pat/inject/index.html index 1ef25f6f0..7e37f9f01 100644 --- a/src/pat/inject/index.html +++ b/src/pat/inject/index.html @@ -413,6 +413,18 @@

Shows a fallback message on error

+
+
+

Inject and fix URLs of options

+ + injection happens here! + +
+
+ @@ -667,6 +679,14 @@

transform: scale(1); } } + + #rebase-url-demo a { + cursor: pointer; + } + #rebase-url-demo div div { + margin-left: 2em; + border: 1px solid black; + } diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index 10b1c79a7..d650cbf86 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -3,6 +3,7 @@ import "regenerator-runtime/runtime"; // needed for ``await`` support import $ from "jquery"; import _ from "underscore"; import ajax from "../ajax/ajax"; +import dom from "../../core/dom"; import logging from "../../core/logging"; import Parser from "../../core/parser"; import registry from "../../core/registry"; @@ -52,6 +53,8 @@ const inject = { name: "inject", trigger: ".raptor-ui .ui-button.pat-inject, a.pat-inject, form.pat-inject, .pat-subform.pat-inject", + parser: parser, + init($el, opts) { const cfgs = this.extractConfig($el, opts); if ( @@ -867,6 +870,14 @@ const inject = { VIDEO: "data-pat-inject-rebase-src", }, + _rebaseOptions: { + "calendar": ["url", "event-sources"], + "collapsible": ["load-content"], + "date-picker": ["i18n"], + "datetime-picker": ["i18n"], + "inject": ["url"], + }, + _rebaseHTML(base, html) { if (html === "") { // Special case, source is none @@ -901,6 +912,47 @@ const inject = { $el_.attr(attrName, value); } }); + + for (const [pattern_name, opts] of Object.entries( + this._rebaseOptions + )) { + for (const el_ of dom.querySelectorAllAndMe( + $page[0], + `[data-pat-${pattern_name}]` + )) { + const val = el_.getAttribute(`data-pat-${pattern_name}`, false); + if (val) { + const pattern = registry.patterns[pattern_name]; + const pattern_parser = pattern?.parser; + if (!pattern_parser) { + continue; + } + let options = pattern_parser._parse(val); + let changed = false; + for (const opt of opts) { + const val = options[opt]; + if (typeof val === "undefined") { + continue; + } + changed = true; + if (Array.isArray(val)) { + options[opt] = val.map((it) => + utils.rebaseURL(base, it) + ); + } else { + options[opt] = utils.rebaseURL(base, val); + } + } + if (changed) { + el_.setAttribute( + `data-pat-${pattern_name}`, + JSON.stringify(options) + ); + } + } + } + } + // 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_) => { diff --git a/src/pat/inject/inject.test.js b/src/pat/inject/inject.test.js index e8720b5c9..f35d73c74 100644 --- a/src/pat/inject/inject.test.js +++ b/src/pat/inject/inject.test.js @@ -308,6 +308,45 @@ describe("pat-inject", function () { "

This string has src = \"foo\" , src= bar , and SrC='foo'

" ); }); + + it("rebase pattern configuration", async (done) => { + await import("../calendar/calendar"); + await import("../collapsible/collapsible"); + await import("../date-picker/date-picker"); + await import("../datetime-picker/datetime-picker"); + + const res = pattern._rebaseHTML( + "https://example.com/test/", + `
+
+
` + ); + console.log(res); + + const el = document.createElement("div"); + el.innerHTML = res; + + const test1_config = JSON.parse(el.querySelector(".test1").getAttribute("data-pat-inject")); // prettier-ignore + expect(test1_config.url).toEqual("https://example.com/test/./index.html"); // prettier-ignore + + const test2_config = JSON.parse(el.querySelector(".test2").getAttribute("data-pat-calendar")); // prettier-ignore + expect(test2_config.url).toEqual("https://example.com/test/./calendar.html"); // prettier-ignore + expect(test2_config["event-sources"][0]).toEqual("https://example.com/test/../calendar2.json"); // prettier-ignore + expect(test2_config["event-sources"][1]).toEqual("https://example.com/test/./test/calendar3.json"); // prettier-ignore + + const test3a_config = JSON.parse(el.querySelector(".test3").getAttribute("data-pat-date-picker")); // prettier-ignore + expect(test3a_config.i18n).toEqual("https://example.com/test/./i18n"); // prettier-ignore + const test3b_config = JSON.parse(el.querySelector(".test3").getAttribute("data-pat-datetime-picker")); // prettier-ignore + expect(test3b_config.i18n).toEqual("https://example.com/test/./path/to/i18n"); // prettier-ignore + const test3c_config = JSON.parse(el.querySelector(".test3").getAttribute("data-pat-collapsible")); // prettier-ignore + expect(test3c_config["load-content"]).toEqual("https://example.com/test/../load/content/from/here"); // prettier-ignore + + done(); + }); }); describe("parseRawHtml", function () { diff --git a/src/pat/legend/legend.js b/src/pat/legend/legend.js index ed6304900..db8600d19 100644 --- a/src/pat/legend/legend.js +++ b/src/pat/legend/legend.js @@ -1,5 +1,7 @@ import $ from "jquery"; import registry from "../../core/registry"; +import utils from "../../core/utils"; +import dom from "../../core/dom"; var legend = { name: "legend", @@ -21,9 +23,14 @@ var legend = { }, transform: function ($root) { - $root.findInclusive("legend:not(.cant-touch-this)").each(function () { - $(this).replaceWith("

" + $(this).html() + "

"); - }); + const root = utils.jqToNode($root); + const all = dom.querySelectorAllAndMe( + root, + "legend:not(.cant-touch-this)" + ); + for (const el of all) { + $(el).replaceWith("

" + $(el).html() + "

"); + } }, }; registry.register(legend); diff --git a/src/pat/scroll/scroll.js b/src/pat/scroll/scroll.js index d9fd032a1..3c2da5583 100644 --- a/src/pat/scroll/scroll.js +++ b/src/pat/scroll/scroll.js @@ -1,9 +1,10 @@ import "regenerator-runtime/runtime"; // needed for ``await`` support +import "../../core/jquery-ext"; import $ from "jquery"; +import _ from "underscore"; import Base from "../../core/base"; -import utils from "../../core/utils"; import Parser from "../../core/parser"; -import _ from "underscore"; +import utils from "../../core/utils"; // Lazy loading modules. let ImagesLoaded; diff --git a/src/pat/slides/slides.js b/src/pat/slides/slides.js index 91bc3dab3..bf46c3129 100644 --- a/src/pat/slides/slides.js +++ b/src/pat/slides/slides.js @@ -12,13 +12,17 @@ import "../../core/remove"; var slides = { name: "slides", - trigger: ".pat-slides:has(.slide)", + trigger: ".pat-slides", setup: function () { $(document).on("patterns-injected", utils.debounce(slides._reset, 100)); }, async init($el) { + if (!this.el.querySelector(".slide")) { + // no slides, nothing to do. + return; + } await import("slides/src/slides"); // loads ``Presentation`` globally. var parameters = url.parameters();