import { __, pure } from 'riot'; /** * Tokenize input string. */ function lexer(str) { var tokens = []; var i = 0; while (i < str.length) { var char = str[i]; if (char === "*" || char === "+" || char === "?") { tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); continue; } if (char === "\\") { tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); continue; } if (char === "{") { tokens.push({ type: "OPEN", index: i, value: str[i++] }); continue; } if (char === "}") { tokens.push({ type: "CLOSE", index: i, value: str[i++] }); continue; } if (char === ":") { var name = ""; var j = i + 1; while (j < str.length) { var code = str.charCodeAt(j); if ( // `0-9` code >= 48 && code <= 57 || // `A-Z` code >= 65 && code <= 90 || // `a-z` code >= 97 && code <= 122 || // `_` code === 95) { name += str[j++]; continue; } break; } if (!name) throw new TypeError("Missing parameter name at " + i); tokens.push({ type: "NAME", index: i, value: name }); i = j; continue; } if (char === "(") { var count = 1; var pattern = ""; var j = i + 1; if (str[j] === "?") { throw new TypeError("Pattern cannot start with \"?\" at " + j); } while (j < str.length) { if (str[j] === "\\") { pattern += str[j++] + str[j++]; continue; } if (str[j] === ")") { count--; if (count === 0) { j++; break; } } else if (str[j] === "(") { count++; if (str[j + 1] !== "?") { throw new TypeError("Capturing groups are not allowed at " + j); } } pattern += str[j++]; } if (count) throw new TypeError("Unbalanced pattern at " + i); if (!pattern) throw new TypeError("Missing pattern at " + i); tokens.push({ type: "PATTERN", index: i, value: pattern }); i = j; continue; } tokens.push({ type: "CHAR", index: i, value: str[i++] }); } tokens.push({ type: "END", index: i, value: "" }); return tokens; } /** * Parse a string for the raw tokens. */ function parse(str, options) { if (options === void 0) { options = {}; } var tokens = lexer(str); var _a = options.prefixes, prefixes = _a === void 0 ? "./" : _a; var defaultPattern = "[^" + escapeString(options.delimiter || "/#?") + "]+?"; var result = []; var key = 0; var i = 0; var path = ""; var tryConsume = function tryConsume(type) { if (i < tokens.length && tokens[i].type === type) return tokens[i++].value; }; var mustConsume = function mustConsume(type) { var value = tryConsume(type); if (value !== undefined) return value; var _a = tokens[i], nextType = _a.type, index = _a.index; throw new TypeError("Unexpected " + nextType + " at " + index + ", expected " + type); }; var consumeText = function consumeText() { var result = ""; var value; // tslint:disable-next-line while (value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR")) { result += value; } return result; }; while (i < tokens.length) { var char = tryConsume("CHAR"); var name = tryConsume("NAME"); var pattern = tryConsume("PATTERN"); if (name || pattern) { var prefix = char || ""; if (prefixes.indexOf(prefix) === -1) { path += prefix; prefix = ""; } if (path) { result.push(path); path = ""; } result.push({ name: name || key++, prefix: prefix, suffix: "", pattern: pattern || defaultPattern, modifier: tryConsume("MODIFIER") || "" }); continue; } var value = char || tryConsume("ESCAPED_CHAR"); if (value) { path += value; continue; } if (path) { result.push(path); path = ""; } var open = tryConsume("OPEN"); if (open) { var prefix = consumeText(); var name_1 = tryConsume("NAME") || ""; var pattern_1 = tryConsume("PATTERN") || ""; var suffix = consumeText(); mustConsume("CLOSE"); result.push({ name: name_1 || (pattern_1 ? key++ : ""), pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1, prefix: prefix, suffix: suffix, modifier: tryConsume("MODIFIER") || "" }); continue; } mustConsume("END"); } return result; } /** * Compile a string to a template function for the path. */ function compile(str, options) { return tokensToFunction(parse(str, options), options); } /** * Expose a method for transforming tokens into the path function. */ function tokensToFunction(tokens, options) { if (options === void 0) { options = {}; } var reFlags = flags(options); var _a = options.encode, encode = _a === void 0 ? function (x) { return x; } : _a, _b = options.validate, validate = _b === void 0 ? true : _b; // Compile all the tokens into regexps. var matches = tokens.map(function (token) { if (typeof token === "object") { return new RegExp("^(?:" + token.pattern + ")$", reFlags); } }); return function (data) { var path = ""; for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; if (typeof token === "string") { path += token; continue; } var value = data ? data[token.name] : undefined; var optional = token.modifier === "?" || token.modifier === "*"; var repeat = token.modifier === "*" || token.modifier === "+"; if (Array.isArray(value)) { if (!repeat) { throw new TypeError("Expected \"" + token.name + "\" to not repeat, but got an array"); } if (value.length === 0) { if (optional) continue; throw new TypeError("Expected \"" + token.name + "\" to not be empty"); } for (var j = 0; j < value.length; j++) { var segment = encode(value[j], token); if (validate && !matches[i].test(segment)) { throw new TypeError("Expected all \"" + token.name + "\" to match \"" + token.pattern + "\", but got \"" + segment + "\""); } path += token.prefix + segment + token.suffix; } continue; } if (typeof value === "string" || typeof value === "number") { var segment = encode(String(value), token); if (validate && !matches[i].test(segment)) { throw new TypeError("Expected \"" + token.name + "\" to match \"" + token.pattern + "\", but got \"" + segment + "\""); } path += token.prefix + segment + token.suffix; continue; } if (optional) continue; var typeOfMessage = repeat ? "an array" : "a string"; throw new TypeError("Expected \"" + token.name + "\" to be " + typeOfMessage); } return path; }; } /** * Escape a regular expression string. */ function escapeString(str) { return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); } /** * Get the flags for a regexp from the options. */ function flags(options) { return options && options.sensitive ? "" : "i"; } /** * Pull out keys from a regexp. */ function regexpToRegexp(path, keys) { if (!keys) return path; var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; var index = 0; var execResult = groupsRegex.exec(path.source); while (execResult) { keys.push({ // Use parenthesized substring match if available, index otherwise name: execResult[1] || index++, prefix: "", suffix: "", modifier: "", pattern: "" }); execResult = groupsRegex.exec(path.source); } return path; } /** * Transform an array into a regexp. */ function arrayToRegexp(paths, keys, options) { var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source; }); return new RegExp("(?:" + parts.join("|") + ")", flags(options)); } /** * Create a path regexp from string input. */ function stringToRegexp(path, keys, options) { return tokensToRegexp(parse(path, options), keys, options); } /** * Expose a function for taking tokens and returning a RegExp. */ function tokensToRegexp(tokens, keys, options) { if (options === void 0) { options = {}; } var _a = options.strict, strict = _a === void 0 ? false : _a, _b = options.start, start = _b === void 0 ? true : _b, _c = options.end, end = _c === void 0 ? true : _c, _d = options.encode, encode = _d === void 0 ? function (x) { return x; } : _d; var endsWith = "[" + escapeString(options.endsWith || "") + "]|$"; var delimiter = "[" + escapeString(options.delimiter || "/#?") + "]"; var route = start ? "^" : ""; // Iterate over the tokens and create our regexp string. for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { var token = tokens_1[_i]; if (typeof token === "string") { route += escapeString(encode(token)); } else { var prefix = escapeString(encode(token.prefix)); var suffix = escapeString(encode(token.suffix)); if (token.pattern) { if (keys) keys.push(token); if (prefix || suffix) { if (token.modifier === "+" || token.modifier === "*") { var mod = token.modifier === "*" ? "?" : ""; route += "(?:" + prefix + "((?:" + token.pattern + ")(?:" + suffix + prefix + "(?:" + token.pattern + "))*)" + suffix + ")" + mod; } else { route += "(?:" + prefix + "(" + token.pattern + ")" + suffix + ")" + token.modifier; } } else { route += "(" + token.pattern + ")" + token.modifier; } } else { route += "(?:" + prefix + suffix + ")" + token.modifier; } } } if (end) { if (!strict) route += delimiter + "?"; route += !options.endsWith ? "$" : "(?=" + endsWith + ")"; } else { var endToken = tokens[tokens.length - 1]; var isEndDelimited = typeof endToken === "string" ? delimiter.indexOf(endToken[endToken.length - 1]) > -1 : // tslint:disable-next-line endToken === undefined; if (!strict) { route += "(?:" + delimiter + "(?=" + endsWith + "))?"; } if (!isEndDelimited) { route += "(?=" + delimiter + "|" + endsWith + ")"; } } return new RegExp(route, flags(options)); } /** * Normalize the given path string, returning a regular expression. * * An empty array can be passed in for the keys, which will hold the * placeholder key descriptions. For example, using `/user/:id`, `keys` will * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. */ function pathToRegexp(path, keys, options) { if (path instanceof RegExp) return regexpToRegexp(path, keys); if (Array.isArray(path)) return arrayToRegexp(path, keys, options); return stringToRegexp(path, keys, options); } /** * Cancel token * @private * @type { Symbol } */ const CANCEL = Symbol(); /** * Helper that can be returned by ruit function to cancel the tasks chain * @returns { Symbol } internal private constant * @example * * ruit( * 100, * num => Math.random() * num * num => num > 50 ? ruit.cancel() : num * num => num - 2 * ).then(result => { * console.log(result) // here we will get only number lower than 50 * }) * */ ruit.cancel = () => CANCEL; /** * The same as ruit() but with the arguments inverted from right to left * @param { * } tasks - list of tasks to process sequentially * @returns { Promise } a promise containing the result of the whole chain * @example * * const curry = f => a => b => f(a, b) * const add = (a, b) => a + b * * const addOne = curry(add)(1) * * const squareAsync = (num) => { * return new Promise(r => { * setTimeout(r, 500, num * 2) * }) * } * * // a -> a + a -> a * 2 * // basically from right to left: 1 => 1 + 1 => 2 * 2 * ruit.compose(squareAsync, addOne, 1).then(result => console.log(result)) // 4 */ ruit.compose = function () { for (var _len = arguments.length, tasks = new Array(_len), _key = 0; _key < _len; _key++) { tasks[_key] = arguments[_key]; } return ruit(...tasks.reverse()); }; /** * Serialize a list of sync and async tasks from left to right * @param { * } tasks - list of tasks to process sequentially * @returns { Promise } a promise containing the result of the whole chain * @example * * const curry = f => a => b => f(a, b) * const add = (a, b) => a + b * * const addOne = curry(add)(1) * * const squareAsync = (num) => { * return new Promise(r => { * setTimeout(r, 500, num * 2) * }) * } * * // a -> a + a -> a * 2 * // basically from left to right: 1 => 1 + 1 => 2 * 2 * ruit(1, addOne, squareAsync).then(result => console.log(result)) // 4 */ function ruit() { for (var _len2 = arguments.length, tasks = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { tasks[_key2] = arguments[_key2]; } return new Promise((resolve, reject) => { return function run(queue, result) { if (!queue.length) return resolve(result); const [task, ...rest] = queue; const value = typeof task === 'function' ? task(result) : task; const done = v => run(rest, v); // check against nil values if (value != null) { if (value === CANCEL) return; if (value.then) return value.then(done, reject); } return Promise.resolve(done(value)); }(tasks); }); } const API_METHODS = new Set(); const UNSUBSCRIBE_SYMBOL = Symbol(); const UNSUBSCRIBE_METHOD = 'off'; const CANCEL_METHOD = 'cancel'; /** * Factory function to create the stream generator * @private * @param {Set} modifiers - stream input modifiers * @returns {Generator} the stream generator */ function createStream(modifiers) { const stream = function* stream() { while (true) { // get the initial stream value const input = yield; // run the input sequence yield ruit(input, ...modifiers); } }(); // start the stream stream.next(); return stream; } /** * Dispatch a value to several listeners * @private * @param {Set} callbacks - callbacks collection * @param {*} value - anything * @returns {Set} the callbacks received */ function dispatch(callbacks, value) { callbacks.forEach(f => { // unsubscribe the callback if erre.unsubscribe() will be returned if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f); }); return callbacks; } /** * Throw a panic error * @param {string} message - error message * @returns {Error} an error object */ function panic$2(message) { throw new Error(message); } /** * Install an erre plugin adding it to the API * @param {string} name - plugin name * @param {Function} fn - new erre API method * @returns {Function} return the erre function */ erre.install = function (name, fn) { if (!name || typeof name !== 'string') panic$2('Please provide a name (as string) for your erre plugin'); if (!fn || typeof fn !== 'function') panic$2('Please provide a function for your erre plugin'); if (API_METHODS.has(name)) { panic$2(`The ${name} is already part of the erre API, please provide a different name`); } else { erre[name] = fn; API_METHODS.add(name); } return erre; }; // alias for ruit canel to stop a stream chain erre.install(CANCEL_METHOD, ruit.cancel); // unsubscribe helper erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL); /** * Stream constuction function * @param {...Function} fns - stream modifiers * @returns {Object} erre instance */ function erre() { for (var _len = arguments.length, fns = new Array(_len), _key = 0; _key < _len; _key++) { fns[_key] = arguments[_key]; } const [success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)], generator = createStream(modifiers), stream = Object.create(generator), addToCollection = collection => fn => collection.add(fn) && stream, deleteFromCollection = collection => fn => collection.delete(fn) ? stream : panic$2('Couldn\'t remove handler passed by reference'); return Object.assign(stream, { on: Object.freeze({ value: addToCollection(success), error: addToCollection(error), end: addToCollection(end) }), off: Object.freeze({ value: deleteFromCollection(success), error: deleteFromCollection(error), end: deleteFromCollection(end) }), connect: addToCollection(modifiers), push(input) { const { value, done } = stream.next(input); // dispatch the stream events if (!done) { value.then(res => dispatch(success, res), err => dispatch(error, err)); } return stream; }, end() { // kill the stream generator.return(); // dispatch the end event dispatch(end) // clean up all the collections ; [success, error, end, modifiers].forEach(el => el.clear()); return stream; }, fork() { return erre(...modifiers); }, next(input) { // get the input and run eventually the promise const result = generator.next(input); // pause to the next iteration generator.next(); return result; } }); } const isNode = typeof process !== 'undefined'; const isString = str => typeof str === 'string'; // the url parsing function depends on the platform, on node we rely on the 'url' module /* istanbul ignore next */ const parseURL = function parseURL() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return isNode ? require('url').parse(...args) : new URL(...args); }; /** * Replace the base path from a path * @param {string} path - router path string * @returns {string} path cleaned up without the base */ const replaceBase = path => path.replace(defaults.base, ''); /** * Try to match the current path or skip it * @param {RegEx} pathRegExp - target path transformed by pathToRegexp * @returns {string|Symbol} if the path match we return it otherwise we cancel the stream */ const matchOrSkip = pathRegExp => path => match(path, pathRegExp) ? path : erre.cancel(); /** * Combine 2 streams connecting the events of dispatcherStream to the receiverStream * @param {Stream} dispatcherStream - main stream dispatching events * @param {Stream} receiverStream - sub stream receiving events from the dispatcher * @returns {Stream} receiverStream */ const joinStreams = (dispatcherStream, receiverStream) => { dispatcherStream.on.value(receiverStream.push); receiverStream.on.end(() => { dispatcherStream.off.value(receiverStream.push); }); return receiverStream; }; /** * Error handling function * @param {Error} error - error to catch * @returns {void} */ const panic$1 = error => { if (defaults.silentErrors) return; throw new Error(error); }; // make sure that the router will always receive strings params const filterStrings = str => isString(str) ? str : erre.cancel(); // create the streaming router const router = erre(filterStrings).on.error(panic$1); // cast the values of this stream always to string /* @type {object} general configuration object */ const defaults = { // custom option base: '', silentErrors: false, // pathToRegexp options sensitive: false, strict: false, end: true, start: true, delimiter: '/#?', encode: undefined, endsWith: undefined, prefixes: './' }; /** * Merge the user options with the defaults * @param {Object} options - custom user options * @returns {Object} options object merged with defaults */ const mergeOptions = options => Object.assign({}, defaults, options); /* {@link https://github.com/pillarjs/path-to-regexp#usage} */ const toRegexp = (path, keys, options) => pathToRegexp(path, keys, mergeOptions(options)); /** * Convert a router entry to a real path computing the url parameters * @param {string} path - router path string * @param {Object} params - named matched parameters * @param {Object} options - pathToRegexp options object * @returns {string} computed url string */ const toPath = (path, params, options) => compile(path, mergeOptions(options))(params); /** * Parse a string path generating an object containing * @param {string} path - target path * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @param {Object} options - object containing the base path * @returns {URL} url object enhanced with the `match` attribute */ const toURL = function toURL(path, pathRegExp, options) { if (options === void 0) { options = {}; } const { base } = mergeOptions(options); const [, ...params] = pathRegExp.exec(path); const url = parseURL(path, base); // extend the url object adding the matched params url.params = params.reduce((acc, param, index) => { const key = options.keys && options.keys[index]; if (key) acc[key.name] = param ? decodeURIComponent(param) : param; return acc; }, {}); return url; }; /** * Return true if a path will be matched * @param {string} path - target path * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @returns {boolean} true if the path matches the regexp */ const match = (path, pathRegExp) => pathRegExp.test(path); /** * Factory function to create an sequence of functions to pass to erre.js * This function will be used in the erre stream * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @param {Object} options - pathToRegexp options object * @returns {Array} a functions array that will be used as stream pipe for erre.js */ const createURLStreamPipe = (pathRegExp, options) => [decodeURI, replaceBase, matchOrSkip(pathRegExp), path => toURL(path, pathRegExp, options)]; /** * Create a fork of the main router stream * @param {string} path - route to match * @param {Object} options - pathToRegexp options object * @returns {Stream} new route stream */ function createRoute(path, options) { const keys = []; const pathRegExp = pathToRegexp(path, keys, options); const URLStream = erre(...createURLStreamPipe(pathRegExp, Object.assign({}, options, { keys }))); return joinStreams(router, URLStream).on.error(panic$1); } const getCurrentRoute = (currentRoute => { // listen the route changes events to store the current route router.on.value(r => currentRoute = r); return () => { return currentRoute; }; })(null); /** * Similar to compose but performs from left-to-right function composition.
* {@link https://30secondsofcode.org/function#composeright see also} * @param {...[function]} fns) - list of unary function * @returns {*} result of the computation */ /** * Performs right-to-left function composition.
* Use Array.prototype.reduce() to perform right-to-left function composition.
* The last (rightmost) function can accept one or more arguments; the remaining functions must be unary.
* {@link https://30secondsofcode.org/function#compose original source code} * @param {...[function]} fns) - list of unary function * @returns {*} result of the computation */ function compose() { for (var _len2 = arguments.length, fns = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { fns[_key2] = arguments[_key2]; } return fns.reduce((f, g) => function () { return f(g(...arguments)); }); } const getInitialRouteValue = (pathToRegexp, path, options) => { const route = compose( ...createURLStreamPipe(pathToRegexp, options).reverse() )(path); return route.params ? route : null }; var routeHoc = { css: null, exports: { onBeforeMount(props) { const currentRoute = getCurrentRoute(); const pathToRegexp = toRegexp(props.path, [], props); this.state = { pathToRegexp, route: currentRoute && match(currentRoute, pathToRegexp) ? getInitialRouteValue(pathToRegexp, currentRoute, props) : null }; router.on.value(this.onBeforeRoute); this.stream = createRoute(props.path, props).on.value(this.onRoute); }, onBeforeRoute(path, pathToRegexp) { if (this.state.route && !match(path, this.state.pathToRegexp)) { this.callLifecycleProperty('onBeforeUnmount', createRoute); this.update({ route: null }); this.callLifecycleProperty('onUnmounted', createRoute); } }, onRoute(route) { this.callLifecycleProperty('onBeforeMount', route); this.update({route}); this.callLifecycleProperty('onMounted', route); }, callLifecycleProperty(method, ...params) { if (this.props[method]) this.props[method](...params); }, onUnmounted() { router.off.value(this.onBeforeRoute); this.stream.end(); } }, template: ( template, expressionTypes, bindingTypes, getComponent ) => template( '', [ { type: bindingTypes.IF, evaluate: _scope => _scope.state.route, redundantAttribute: 'expr0', selector: '[expr0]', template: template( '', [ { type: bindingTypes.SLOT, attributes: [ { type: expressionTypes.ATTRIBUTE, name: 'route', evaluate: _scope => _scope.state.route } ], name: 'default', redundantAttribute: 'expr1', selector: '[expr1]' } ] ) } ] ), name: 'route-hoc' }; const getGlobal = () => getWindow() || global; const getWindow = () => typeof window === 'undefined' ? null : window; const getDocument = () => typeof document === 'undefined' ? null : document; const getHistory = () => typeof history === 'undefined' ? null : history; const getLocation = () => { const win = getWindow(); return win ? win.location : {}; }; const defer = (() => { const globalScope = getGlobal(); return globalScope.requestAnimationFrame || globalScope.setTimeout; })(); const cancelDefer = (() => { const globalScope = getGlobal(); return globalScope.cancelAnimationFrame || globalScope.clearTimeout; })(); const WINDOW_EVENTS = 'popstate'; const CLICK_EVENT = 'click'; const DOWNLOAD_LINK_ATTRIBUTE = 'download'; const HREF_LINK_ATTRIBUTE = 'href'; const TARGET_SELF_LINK_ATTRIBUTE = '_self'; const LINK_TAG_NAME = 'A'; const HASH = '#'; const SLASH = '/'; const RE_ORIGIN = /^.+?\/\/+[^/]+/; const normalizeInitialSlash = str => str[0] === SLASH ? str : `${SLASH}${str}`; const removeTrailingSlash = str => str[str.length - 1] === SLASH ? str.substr(0, str.length - 1) : str; const normalizeBase = base => { const win = getWindow(); const loc = win.location; const root = loc ? `${loc.protocol}//${loc.host}` : ''; const { pathname } = loc ? loc : {}; switch (true) { // pure root url + pathname case Boolean(base) === false: return removeTrailingSlash(`${root}${pathname || ''}`); // full path base case /(www|http(s)?:)/.test(base): return base; // hash navigation case base[0] === HASH: return `${root}${pathname && pathname !== SLASH ? pathname : ''}${base}`; // root url with trailing slash case base === SLASH: return removeTrailingSlash(root); // custom pathname default: return removeTrailingSlash(`${root}${normalizeInitialSlash(base)}`); } }; function setBase(base) { defaults.base = normalizeBase(base); } /** * Convert a string from camel case to dash-case * @param {string} string - probably a component tag name * @returns {string} component name normalized */ /** * Convert a string containing dashes to camel case * @param {string} string - input string * @returns {string} my-string -> myString */ function dashToCamelCase(string) { return string.replace(/-(\w)/g, (_, c) => c.toUpperCase()); } /** * Throw an error with a descriptive message * @param { string } message - error message * @returns { undefined } hoppla.. at this point the program should stop working */ function panic(message) { throw new Error(message); } /** * Converts any DOM node/s to a loopable array * @param { HTMLElement|NodeList } els - single html element or a node list * @returns { Array } always a loopable object */ function domToArray(els) { // can this object be already looped? if (!Array.isArray(els)) { // is it a node list? if (/^\[object (HTMLCollection|NodeList|Object)\]$/.test(Object.prototype.toString.call(els)) && typeof els.length === 'number') return Array.from(els);else // if it's a single node // it will be returned as "array" with one single entry return [els]; } // this object could be looped out of the box return els; } /** * Split a string into several items separed by spaces * @param { string } l - events list * @returns { Array } all the events detected * @private */ const split = l => l.split(/\s/); /** * Set a listener for all the events received separated by spaces * @param { HTMLElement|NodeList|Array } els - DOM node/s where the listeners will be bound * @param { string } evList - list of events we want to bind or unbind space separated * @param { Function } cb - listeners callback * @param { string } method - either 'addEventListener' or 'removeEventListener' * @param { Object } options - event options (capture, once and passive) * @returns { undefined } * @private */ function manageEvents(els, evList, cb, method, options) { els = domToArray(els); split(evList).forEach(e => { els.forEach(el => el[method](e, cb, options || false)); }); } /** * Set a listener for all the events received separated by spaces * @param { HTMLElement|Array } els - DOM node/s where the listeners will be bound * @param { string } evList - list of events we want to bind space separated * @param { Function } cb - listeners callback * @param { Object } options - event options (capture, once and passive) * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function */ function add(els, evList, cb, options) { manageEvents(els, evList, cb, 'addEventListener', options); return els; } /** * Remove all the listeners for the events received separated by spaces * @param { HTMLElement|Array } els - DOM node/s where the events will be unbind * @param { string } evList - list of events we want unbind space separated * @param { Function } cb - listeners callback * @param { Object } options - event options (capture, once and passive) * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function */ function remove(els, evList, cb, options) { manageEvents(els, evList, cb, 'removeEventListener', options); return els; } /** * Normalize the return values, in case of a single value we avoid to return an array * @param { Array } values - list of values we want to return * @returns { Array|string|boolean } either the whole list of values or the single one found * @private */ const normalize = values => values.length === 1 ? values[0] : values; /** * Parse all the nodes received to get/remove/check their attributes * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse * @param { string|Array } name - name or list of attributes * @param { string } method - method that will be used to parse the attributes * @returns { Array|string } result of the parsing in a list or a single value * @private */ function parseNodes(els, name, method) { const names = typeof name === 'string' ? [name] : name; return normalize(domToArray(els).map(el => { return normalize(names.map(n => el[method](n))); })); } /** * Set any attribute on a single or a list of DOM nodes * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse * @param { string|Array } name - name or list of attributes to detect * @returns { boolean|Array } true or false or an array of boolean values * @example * * import { has } from 'bianco.attr' * * has(img, 'width') // false * * // or also * has(img, ['width', 'height']) // => [false, false] * * // or also * has([img1, img2], ['width', 'height']) // => [[false, false], [false, false]] */ function has(els, name) { return parseNodes(els, name, 'hasAttribute'); } const onWindowEvent = () => router.push(normalizePath(String(getLocation().href))); const onRouterPush = path => { const url = path.includes(defaults.base) ? path : defaults.base + path; const loc = getLocation(); const hist = getHistory(); const doc = getDocument(); // update the browser history only if it's necessary if (hist && url !== loc.href) { hist.pushState(null, doc.title, url); } }; const getLinkElement = node => node && !isLinkNode(node) ? getLinkElement(node.parentNode) : node; const isLinkNode = node => node.nodeName === LINK_TAG_NAME; const isCrossOriginLink = path => path.indexOf(getLocation().href.match(RE_ORIGIN)[0]) === -1; const isTargetSelfLink = el => el.target && el.target !== TARGET_SELF_LINK_ATTRIBUTE; const isEventForbidden = event => event.which && event.which !== 1 // not left click || event.metaKey || event.ctrlKey || event.shiftKey // or meta keys || event.defaultPrevented; // or default prevented const isForbiddenLink = el => !el || !isLinkNode(el) // not A tag || has(el, DOWNLOAD_LINK_ATTRIBUTE) // has download attr || !has(el, HREF_LINK_ATTRIBUTE) // has no href attr || isTargetSelfLink(el) || isCrossOriginLink(el.href); const isHashLink = path => path.split(HASH).length > 1; const normalizePath = path => path.replace(defaults.base, ''); const isInBase = path => !defaults.base || path.includes(defaults.base); /** * Callback called anytime something will be clicked on the page * @param {HTMLEvent} event - click event * @returns {undefined} void method */ const onClick = event => { if (isEventForbidden(event)) return; const el = getLinkElement(event.target); if (isForbiddenLink(el) || isHashLink(el.href) || !isInBase(el.href)) return; const path = normalizePath(el.href); router.push(path); event.preventDefault(); }; /** * Link the rawth router to the DOM events * @param { HTMLElement } container - DOM node where the links are located * @returns {Function} teardown function */ function initDomListeners(container) { const win = getWindow(); const root = container || getDocument(); if (win) { add(win, WINDOW_EVENTS, onWindowEvent); add(root, CLICK_EVENT, onClick); } router.on.value(onRouterPush); return () => { if (win) { remove(win, WINDOW_EVENTS, onWindowEvent); remove(root, CLICK_EVENT, onClick); } router.off.value(onRouterPush); }; } const BASE_ATTRIBUTE_NAME = 'base'; const INITIAL_ROUTE = 'initialRoute'; const ON_STARTED_ATTRIBUTE_NAME = 'onStarted'; const {template, bindingTypes} = __.DOMBindings; let wasInitialized = false; var routerHoc = { css: null, exports: pure( ({slots, attributes, props}) => { if (wasInitialized) panic('Multiple components are not supported'); const getAttribute = name => attributes && attributes.find(a => dashToCamelCase(a.name) === name); return { slot: null, el: null, teardown: null, mount(el, context) { const initialRouteAttr = getAttribute(INITIAL_ROUTE); const initialRoute = initialRouteAttr ? initialRouteAttr.evaluate(context) : null; const currentRoute = getCurrentRoute(); const onFirstRoute = () => { this.createSlot(context); router.off.value(onFirstRoute); }; wasInitialized = true; this.el = el; this.teardown = initDomListeners(this.root); this.setBase(context); // mount the slots only if the current route was defined if (currentRoute && !initialRoute) { this.createSlot(context); } else { router.on.value(onFirstRoute); router.push(initialRoute || window.location.href); } }, createSlot(context) { if (!slots || !slots.length) return const onStartedAttr = getAttribute(ON_STARTED_ATTRIBUTE_NAME); this.slot = template(null, [{ type: bindingTypes.SLOT, name: 'default' }]); this.slot.mount(this.el, { slots }, context); if (onStartedAttr) { onStartedAttr.evaluate(context)(getCurrentRoute()); } }, update(context) { this.setBase(context); // defer the updates to avoid internal recursive update calls // see https://github.com/riot/route/issues/148 if (this.slot) { cancelDefer(this.deferred); this.deferred = defer(() => { this.slot.update({}, context); }); } }, unmount(...args) { this.teardown(); wasInitialized = false; if (this.slot) { this.slot.unmount(...args); } }, getBase(context) { const baseAttr = getAttribute(BASE_ATTRIBUTE_NAME); return baseAttr ? this.el.getAttribute(BASE_ATTRIBUTE_NAME) || baseAttr.evaluate(context) : '/' }, setBase(context) { setBase(props ? props.base : this.getBase(context)); } } } ), template: null, name: 'router-hoc' }; export { routeHoc as Route, routerHoc as Router, createURLStreamPipe, filterStrings, getCurrentRoute, initDomListeners, match, createRoute as route, router, setBase, toPath, toRegexp, toURL };