1383 lines
38 KiB
JavaScript
1383 lines
38 KiB
JavaScript
|
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.<br/>
|
||
|
* {@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.<br/>
|
||
|
* Use Array.prototype.reduce() to perform right-to-left function composition.<br/>
|
||
|
* The last (rightmost) function can accept one or more arguments; the remaining functions must be unary.<br/>
|
||
|
* {@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(
|
||
|
'<template expr0="expr0"></template>',
|
||
|
[
|
||
|
{
|
||
|
type: bindingTypes.IF,
|
||
|
evaluate: _scope => _scope.state.route,
|
||
|
redundantAttribute: 'expr0',
|
||
|
selector: '[expr0]',
|
||
|
|
||
|
template: template(
|
||
|
'<slot expr1="expr1"></slot>',
|
||
|
[
|
||
|
{
|
||
|
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 <router> 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 };
|