From c1c5e4cb2adf2354140fdedead39b25ee17adcf2 Mon Sep 17 00:00:00 2001 From: Tsutomu Kawamura Date: Fri, 23 Dec 2016 13:16:41 +0900 Subject: [PATCH 1/8] Adds the first implementation of riot-route-tag --- lib/tag.js | 7 +++++++ lib/tags/route.tag | 27 +++++++++++++++++++++++++++ lib/tags/router.tag | 21 +++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 lib/tag.js create mode 100644 lib/tags/route.tag create mode 100644 lib/tags/router.tag diff --git a/lib/tag.js b/lib/tag.js new file mode 100644 index 0000000..612821c --- /dev/null +++ b/lib/tag.js @@ -0,0 +1,7 @@ +'use strict' + +import route from 'riot-route' +import './tags/router.tag' +import './tags/route.tag' + +export default route diff --git a/lib/tags/route.tag b/lib/tags/route.tag new file mode 100644 index 0000000..7c6bd6a --- /dev/null +++ b/lib/tags/route.tag @@ -0,0 +1,27 @@ + + + + + + + diff --git a/lib/tags/router.tag b/lib/tags/router.tag new file mode 100644 index 0000000..6ef9a64 --- /dev/null +++ b/lib/tags/router.tag @@ -0,0 +1,21 @@ + + + + + + + From 6080b769de6ce4e70fc1c57e246df9e91f48489f Mon Sep 17 00:00:00 2001 From: Tsutomu Kawamura Date: Fri, 23 Dec 2016 13:19:16 +0900 Subject: [PATCH 2/8] Compiles route-tag --- dist/amd.route+tag.js | 514 +++++++++++++++++++++++++++++++++++++ dist/amd.route+tag.min.js | 1 + dist/route+tag.js | 515 ++++++++++++++++++++++++++++++++++++++ dist/route+tag.min.js | 1 + es.tag.js | 47 ++++ package.json | 9 +- rollup.js | 50 +++- tag.js | 51 ++++ 8 files changed, 1182 insertions(+), 6 deletions(-) create mode 100644 dist/amd.route+tag.js create mode 100644 dist/amd.route+tag.min.js create mode 100644 dist/route+tag.js create mode 100644 dist/route+tag.min.js create mode 100644 es.tag.js create mode 100644 tag.js diff --git a/dist/amd.route+tag.js b/dist/amd.route+tag.js new file mode 100644 index 0000000..b98d041 --- /dev/null +++ b/dist/amd.route+tag.js @@ -0,0 +1,514 @@ +define(['riot'], function (riot) { 'use strict'; + +riot = 'default' in riot ? riot['default'] : riot; + +var observable = function(el) { + + /** + * Extend the original object or create a new empty one + * @type { Object } + */ + + el = el || {}; + + /** + * Private variables + */ + var callbacks = {}, + slice = Array.prototype.slice; + + /** + * Public Api + */ + + // extend the el object adding the observable methods + Object.defineProperties(el, { + /** + * Listen to the given `event` ands + * execute the `callback` each time an event is triggered. + * @param { String } event - event id + * @param { Function } fn - callback function + * @returns { Object } el + */ + on: { + value: function(event, fn) { + if (typeof fn == 'function') + { (callbacks[event] = callbacks[event] || []).push(fn); } + return el + }, + enumerable: false, + writable: false, + configurable: false + }, + + /** + * Removes the given `event` listeners + * @param { String } event - event id + * @param { Function } fn - callback function + * @returns { Object } el + */ + off: { + value: function(event, fn) { + if (event == '*' && !fn) { callbacks = {}; } + else { + if (fn) { + var arr = callbacks[event]; + for (var i = 0, cb; cb = arr && arr[i]; ++i) { + if (cb == fn) { arr.splice(i--, 1); } + } + } else { delete callbacks[event]; } + } + return el + }, + enumerable: false, + writable: false, + configurable: false + }, + + /** + * Listen to the given `event` and + * execute the `callback` at most once + * @param { String } event - event id + * @param { Function } fn - callback function + * @returns { Object } el + */ + one: { + value: function(event, fn) { + function on() { + el.off(event, on); + fn.apply(el, arguments); + } + return el.on(event, on) + }, + enumerable: false, + writable: false, + configurable: false + }, + + /** + * Execute all callback functions that listen to + * the given `event` + * @param { String } event - event id + * @returns { Object } el + */ + trigger: { + value: function(event) { + var arguments$1 = arguments; + + + // getting the arguments + var arglen = arguments.length - 1, + args = new Array(arglen), + fns, + fn, + i; + + for (i = 0; i < arglen; i++) { + args[i] = arguments$1[i + 1]; // skip first argument + } + + fns = slice.call(callbacks[event] || [], 0); + + for (i = 0; fn = fns[i]; ++i) { + fn.apply(el, args); + } + + if (callbacks['*'] && event != '*') + { el.trigger.apply(el, ['*', event].concat(args)); } + + return el + }, + enumerable: false, + writable: false, + configurable: false + } + }); + + return el + +}; + +/** + * Simple client-side router + * @module riot-route + */ + +var RE_ORIGIN = /^.+?\/\/+[^\/]+/; +var EVENT_LISTENER = 'EventListener'; +var REMOVE_EVENT_LISTENER = 'remove' + EVENT_LISTENER; +var ADD_EVENT_LISTENER = 'add' + EVENT_LISTENER; +var HAS_ATTRIBUTE = 'hasAttribute'; +var POPSTATE = 'popstate'; +var HASHCHANGE = 'hashchange'; +var TRIGGER = 'trigger'; +var MAX_EMIT_STACK_LEVEL = 3; +var win = typeof window != 'undefined' && window; +var doc = typeof document != 'undefined' && document; +var hist = win && history; +var loc = win && (hist.location || win.location); +var prot = Router.prototype; +var clickEvent = doc && doc.ontouchstart ? 'touchstart' : 'click'; +var central = observable(); + +var started = false; +var routeFound = false; +var debouncedEmit; +var base; +var current; +var parser; +var secondParser; +var emitStack = []; +var emitStackLevel = 0; + +/** + * Default parser. You can replace it via router.parser method. + * @param {string} path - current path (normalized) + * @returns {array} array + */ +function DEFAULT_PARSER(path) { + return path.split(/[/?#]/) +} + +/** + * Default parser (second). You can replace it via router.parser method. + * @param {string} path - current path (normalized) + * @param {string} filter - filter string (normalized) + * @returns {array} array + */ +function DEFAULT_SECOND_PARSER(path, filter) { + var f = filter + .replace(/\?/g, '\\?') + .replace(/\*/g, '([^/?#]+?)') + .replace(/\.\./, '.*'); + var re = new RegExp(("^" + f + "$")); + var args = path.match(re); + + if (args) { return args.slice(1) } +} + +/** + * Simple/cheap debounce implementation + * @param {function} fn - callback + * @param {number} delay - delay in seconds + * @returns {function} debounced function + */ +function debounce(fn, delay) { + var t; + return function () { + clearTimeout(t); + t = setTimeout(fn, delay); + } +} + +/** + * Set the window listeners to trigger the routes + * @param {boolean} autoExec - see route.start + */ +function start(autoExec) { + debouncedEmit = debounce(emit, 1); + win[ADD_EVENT_LISTENER](POPSTATE, debouncedEmit); + win[ADD_EVENT_LISTENER](HASHCHANGE, debouncedEmit); + doc[ADD_EVENT_LISTENER](clickEvent, click); + if (autoExec) { emit(true); } +} + +/** + * Router class + */ +function Router() { + this.$ = []; + observable(this); // make it observable + central.on('stop', this.s.bind(this)); + central.on('emit', this.e.bind(this)); +} + +function normalize(path) { + return path.replace(/^\/|\/$/, '') +} + +function isString(str) { + return typeof str == 'string' +} + +/** + * Get the part after domain name + * @param {string} href - fullpath + * @returns {string} path from root + */ +function getPathFromRoot(href) { + return (href || loc.href).replace(RE_ORIGIN, '') +} + +/** + * Get the part after base + * @param {string} href - fullpath + * @returns {string} path from base + */ +function getPathFromBase(href) { + return base[0] === '#' + ? (href || loc.href || '').split(base)[1] || '' + : (loc ? getPathFromRoot(href) : href || '').replace(base, '') +} + +function emit(force) { + // the stack is needed for redirections + var isRoot = emitStackLevel === 0; + if (MAX_EMIT_STACK_LEVEL <= emitStackLevel) { return } + + emitStackLevel++; + emitStack.push(function() { + var path = getPathFromBase(); + if (force || path !== current) { + central[TRIGGER]('emit', path); + current = path; + } + }); + if (isRoot) { + var first; + while (first = emitStack.shift()) { first(); } // stack increses within this call + emitStackLevel = 0; + } +} + +function click(e) { + if ( + e.which !== 1 // not left click + || e.metaKey || e.ctrlKey || e.shiftKey // or meta keys + || e.defaultPrevented // or default prevented + ) { return } + + var el = e.target; + while (el && el.nodeName !== 'A') { el = el.parentNode; } + + if ( + !el || el.nodeName !== 'A' // not A tag + || el[HAS_ATTRIBUTE]('download') // has download attr + || !el[HAS_ATTRIBUTE]('href') // has no href attr + || el.target && el.target !== '_self' // another window or frame + || el.href.indexOf(loc.href.match(RE_ORIGIN)[0]) === -1 // cross origin + ) { return } + + if (el.href !== loc.href + && ( + el.href.split('#')[0] === loc.href.split('#')[0] // internal jump + || base[0] !== '#' && getPathFromRoot(el.href).indexOf(base) !== 0 // outside of base + || base[0] === '#' && el.href.split(base)[0] !== loc.href.split(base)[0] // outside of #base + || !go(getPathFromBase(el.href), el.title || doc.title) // route not found + )) { return } + + e.preventDefault(); +} + +/** + * Go to the path + * @param {string} path - destination path + * @param {string} title - page title + * @param {boolean} shouldReplace - use replaceState or pushState + * @returns {boolean} - route not found flag + */ +function go(path, title, shouldReplace) { + // Server-side usage: directly execute handlers for the path + if (!hist) { return central[TRIGGER]('emit', getPathFromBase(path)) } + + path = base + normalize(path); + title = title || doc.title; + // browsers ignores the second parameter `title` + shouldReplace + ? hist.replaceState(null, title, path) + : hist.pushState(null, title, path); + // so we need to set it manually + doc.title = title; + routeFound = false; + emit(); + return routeFound +} + +/** + * Go to path or set action + * a single string: go there + * two strings: go there with setting a title + * two strings and boolean: replace history with setting a title + * a single function: set an action on the default route + * a string/RegExp and a function: set an action on the route + * @param {(string|function)} first - path / action / filter + * @param {(string|RegExp|function)} second - title / action + * @param {boolean} third - replace flag + */ +prot.m = function(first, second, third) { + if (isString(first) && (!second || isString(second))) { go(first, second, third || false); } + else if (second) { this.r(first, second); } + else { this.r('@', first); } +}; + +/** + * Stop routing + */ +prot.s = function() { + this.off('*'); + this.$ = []; +}; + +/** + * Emit + * @param {string} path - path + */ +prot.e = function(path) { + this.$.concat('@').some(function(filter) { + var args = (filter === '@' ? parser : secondParser)(normalize(path), normalize(filter)); + if (typeof args != 'undefined') { + this[TRIGGER].apply(null, [filter].concat(args)); + return routeFound = true // exit from loop + } + }, this); +}; + +/** + * Register route + * @param {string} filter - filter for matching to url + * @param {function} action - action to register + */ +prot.r = function(filter, action) { + if (filter !== '@') { + filter = '/' + normalize(filter); + this.$.push(filter); + } + this.on(filter, action); +}; + +var mainRouter = new Router(); +var route$1 = mainRouter.m.bind(mainRouter); + +/** + * Create a sub router + * @returns {function} the method of a new Router object + */ +route$1.create = function() { + var newSubRouter = new Router(); + // assign sub-router's main method + var router = newSubRouter.m.bind(newSubRouter); + // stop only this sub-router + router.stop = newSubRouter.s.bind(newSubRouter); + return router +}; + +/** + * Set the base of url + * @param {(str|RegExp)} arg - a new base or '#' or '#!' + */ +route$1.base = function(arg) { + base = arg || '#'; + current = getPathFromBase(); // recalculate current path +}; + +/** Exec routing right now **/ +route$1.exec = function() { + emit(true); +}; + +/** + * Replace the default router to yours + * @param {function} fn - your parser function + * @param {function} fn2 - your secondParser function + */ +route$1.parser = function(fn, fn2) { + if (!fn && !fn2) { + // reset parser for testing... + parser = DEFAULT_PARSER; + secondParser = DEFAULT_SECOND_PARSER; + } + if (fn) { parser = fn; } + if (fn2) { secondParser = fn2; } +}; + +/** + * Helper function to get url query as an object + * @returns {object} parsed query + */ +route$1.query = function() { + var q = {}; + var href = loc.href || current; + href.replace(/[?&](.+?)=([^&]*)/g, function(_, k, v) { q[k] = v; }); + return q +}; + +/** Stop routing **/ +route$1.stop = function () { + if (started) { + if (win) { + win[REMOVE_EVENT_LISTENER](POPSTATE, debouncedEmit); + win[REMOVE_EVENT_LISTENER](HASHCHANGE, debouncedEmit); + doc[REMOVE_EVENT_LISTENER](clickEvent, click); + } + central[TRIGGER]('stop'); + started = false; + } +}; + +/** + * Start routing + * @param {boolean} autoExec - automatically exec after starting if true + */ +route$1.start = function (autoExec) { + if (!started) { + if (win) { + if (document.readyState === 'complete') { start(autoExec); } + // the timeout is needed to solve + // a weird safari bug https://github.com/riot/route/issues/33 + else { win[ADD_EVENT_LISTENER]('load', function() { + setTimeout(function() { start(autoExec); }, 1); + }); } + } + started = true; + } +}; + +/** Prepare the router **/ +route$1.base(); +route$1.parser(); + +riot.tag2('router', '', '', '', function(opts) { + var this$1 = this; + + + if (opts.base) { route$1.base(opts.base); } + this.route = route$1.create(); + this.select = function (target) { + [].concat(this$1.tags.route) + .forEach(function (r) { return r.show = (r === target); }); + }; + + this.on('mount', function () { + + window.setTimeout(function () { return route$1.start(true); }, 0); + }); +}); + +riot.tag2('route', '', '', '', function(opts) { + var this$1 = this; + + this.show = false; + this.parent.route(opts.path, function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + + this$1.one('updated', function () { + flatten(this$1.tags).forEach(function (tag) { + tag.trigger.apply(tag, [ 'route' ].concat( args )); + tag.update(); + }); + }); + this$1.parent.select(this$1); + this$1.parent.update(); + }); + + function flatten(tags) { + return Object.keys(tags) + .map(function (key) { return tags[key]; }) + .reduce(function (acc, tag) { return acc.concat(tag); }, []) + } +}); + +return route$1; + +}); diff --git a/dist/amd.route+tag.min.js b/dist/amd.route+tag.min.js new file mode 100644 index 0000000..0d9578a --- /dev/null +++ b/dist/amd.route+tag.min.js @@ -0,0 +1 @@ +define(["riot"],function(e){"use strict";e="default"in e?e["default"]:e;var t=function(e){e=e||{};var t={},r=Array.prototype.slice;Object.defineProperties(e,{on:{value:function(r,n){if(typeof n=="function"){(t[r]=t[r]||[]).push(n)}return e},enumerable:false,writable:false,configurable:false},off:{value:function(r,n){if(r=="*"&&!n){t={}}else{if(n){var i=t[r];for(var a=0,f;f=i&&i[a];++a){if(f==n){i.splice(a--,1)}}}else{delete t[r]}}return e},enumerable:false,writable:false,configurable:false},one:{value:function(t,r){function n(){e.off(t,n);r.apply(e,arguments)}return e.on(t,n)},enumerable:false,writable:false,configurable:false},trigger:{value:function(n){var i=arguments;var a=arguments.length-1,f=new Array(a),u,o,c;for(c=0;c","","",function(e){var t=this;if(e.base){C.base(e.base)}this.route=C.create();this.select=function(e){[].concat(t.tags.route).forEach(function(t){return t.show=t===e})};this.on("mount",function(){window.setTimeout(function(){return C.start(true)},0)})});e.tag2("route",'',"","",function(e){var t=this;this.show=false;this.parent.route(e.path,function(){var e=[],n=arguments.length;while(n--)e[n]=arguments[n];t.one("updated",function(){r(t.tags).forEach(function(t){t.trigger.apply(t,["route"].concat(e));t.update()})});t.parent.select(t);t.parent.update()});function r(e){return Object.keys(e).map(function(t){return e[t]}).reduce(function(e,t){return e.concat(t)},[])}});return C}); \ No newline at end of file diff --git a/dist/route+tag.js b/dist/route+tag.js new file mode 100644 index 0000000..f717779 --- /dev/null +++ b/dist/route+tag.js @@ -0,0 +1,515 @@ +var route = (function (riot) { +'use strict'; + +riot = 'default' in riot ? riot['default'] : riot; + +var observable = function(el) { + + /** + * Extend the original object or create a new empty one + * @type { Object } + */ + + el = el || {}; + + /** + * Private variables + */ + var callbacks = {}, + slice = Array.prototype.slice; + + /** + * Public Api + */ + + // extend the el object adding the observable methods + Object.defineProperties(el, { + /** + * Listen to the given `event` ands + * execute the `callback` each time an event is triggered. + * @param { String } event - event id + * @param { Function } fn - callback function + * @returns { Object } el + */ + on: { + value: function(event, fn) { + if (typeof fn == 'function') + { (callbacks[event] = callbacks[event] || []).push(fn); } + return el + }, + enumerable: false, + writable: false, + configurable: false + }, + + /** + * Removes the given `event` listeners + * @param { String } event - event id + * @param { Function } fn - callback function + * @returns { Object } el + */ + off: { + value: function(event, fn) { + if (event == '*' && !fn) { callbacks = {}; } + else { + if (fn) { + var arr = callbacks[event]; + for (var i = 0, cb; cb = arr && arr[i]; ++i) { + if (cb == fn) { arr.splice(i--, 1); } + } + } else { delete callbacks[event]; } + } + return el + }, + enumerable: false, + writable: false, + configurable: false + }, + + /** + * Listen to the given `event` and + * execute the `callback` at most once + * @param { String } event - event id + * @param { Function } fn - callback function + * @returns { Object } el + */ + one: { + value: function(event, fn) { + function on() { + el.off(event, on); + fn.apply(el, arguments); + } + return el.on(event, on) + }, + enumerable: false, + writable: false, + configurable: false + }, + + /** + * Execute all callback functions that listen to + * the given `event` + * @param { String } event - event id + * @returns { Object } el + */ + trigger: { + value: function(event) { + var arguments$1 = arguments; + + + // getting the arguments + var arglen = arguments.length - 1, + args = new Array(arglen), + fns, + fn, + i; + + for (i = 0; i < arglen; i++) { + args[i] = arguments$1[i + 1]; // skip first argument + } + + fns = slice.call(callbacks[event] || [], 0); + + for (i = 0; fn = fns[i]; ++i) { + fn.apply(el, args); + } + + if (callbacks['*'] && event != '*') + { el.trigger.apply(el, ['*', event].concat(args)); } + + return el + }, + enumerable: false, + writable: false, + configurable: false + } + }); + + return el + +}; + +/** + * Simple client-side router + * @module riot-route + */ + +var RE_ORIGIN = /^.+?\/\/+[^\/]+/; +var EVENT_LISTENER = 'EventListener'; +var REMOVE_EVENT_LISTENER = 'remove' + EVENT_LISTENER; +var ADD_EVENT_LISTENER = 'add' + EVENT_LISTENER; +var HAS_ATTRIBUTE = 'hasAttribute'; +var POPSTATE = 'popstate'; +var HASHCHANGE = 'hashchange'; +var TRIGGER = 'trigger'; +var MAX_EMIT_STACK_LEVEL = 3; +var win = typeof window != 'undefined' && window; +var doc = typeof document != 'undefined' && document; +var hist = win && history; +var loc = win && (hist.location || win.location); +var prot = Router.prototype; +var clickEvent = doc && doc.ontouchstart ? 'touchstart' : 'click'; +var central = observable(); + +var started = false; +var routeFound = false; +var debouncedEmit; +var base; +var current; +var parser; +var secondParser; +var emitStack = []; +var emitStackLevel = 0; + +/** + * Default parser. You can replace it via router.parser method. + * @param {string} path - current path (normalized) + * @returns {array} array + */ +function DEFAULT_PARSER(path) { + return path.split(/[/?#]/) +} + +/** + * Default parser (second). You can replace it via router.parser method. + * @param {string} path - current path (normalized) + * @param {string} filter - filter string (normalized) + * @returns {array} array + */ +function DEFAULT_SECOND_PARSER(path, filter) { + var f = filter + .replace(/\?/g, '\\?') + .replace(/\*/g, '([^/?#]+?)') + .replace(/\.\./, '.*'); + var re = new RegExp(("^" + f + "$")); + var args = path.match(re); + + if (args) { return args.slice(1) } +} + +/** + * Simple/cheap debounce implementation + * @param {function} fn - callback + * @param {number} delay - delay in seconds + * @returns {function} debounced function + */ +function debounce(fn, delay) { + var t; + return function () { + clearTimeout(t); + t = setTimeout(fn, delay); + } +} + +/** + * Set the window listeners to trigger the routes + * @param {boolean} autoExec - see route.start + */ +function start(autoExec) { + debouncedEmit = debounce(emit, 1); + win[ADD_EVENT_LISTENER](POPSTATE, debouncedEmit); + win[ADD_EVENT_LISTENER](HASHCHANGE, debouncedEmit); + doc[ADD_EVENT_LISTENER](clickEvent, click); + if (autoExec) { emit(true); } +} + +/** + * Router class + */ +function Router() { + this.$ = []; + observable(this); // make it observable + central.on('stop', this.s.bind(this)); + central.on('emit', this.e.bind(this)); +} + +function normalize(path) { + return path.replace(/^\/|\/$/, '') +} + +function isString(str) { + return typeof str == 'string' +} + +/** + * Get the part after domain name + * @param {string} href - fullpath + * @returns {string} path from root + */ +function getPathFromRoot(href) { + return (href || loc.href).replace(RE_ORIGIN, '') +} + +/** + * Get the part after base + * @param {string} href - fullpath + * @returns {string} path from base + */ +function getPathFromBase(href) { + return base[0] === '#' + ? (href || loc.href || '').split(base)[1] || '' + : (loc ? getPathFromRoot(href) : href || '').replace(base, '') +} + +function emit(force) { + // the stack is needed for redirections + var isRoot = emitStackLevel === 0; + if (MAX_EMIT_STACK_LEVEL <= emitStackLevel) { return } + + emitStackLevel++; + emitStack.push(function() { + var path = getPathFromBase(); + if (force || path !== current) { + central[TRIGGER]('emit', path); + current = path; + } + }); + if (isRoot) { + var first; + while (first = emitStack.shift()) { first(); } // stack increses within this call + emitStackLevel = 0; + } +} + +function click(e) { + if ( + e.which !== 1 // not left click + || e.metaKey || e.ctrlKey || e.shiftKey // or meta keys + || e.defaultPrevented // or default prevented + ) { return } + + var el = e.target; + while (el && el.nodeName !== 'A') { el = el.parentNode; } + + if ( + !el || el.nodeName !== 'A' // not A tag + || el[HAS_ATTRIBUTE]('download') // has download attr + || !el[HAS_ATTRIBUTE]('href') // has no href attr + || el.target && el.target !== '_self' // another window or frame + || el.href.indexOf(loc.href.match(RE_ORIGIN)[0]) === -1 // cross origin + ) { return } + + if (el.href !== loc.href + && ( + el.href.split('#')[0] === loc.href.split('#')[0] // internal jump + || base[0] !== '#' && getPathFromRoot(el.href).indexOf(base) !== 0 // outside of base + || base[0] === '#' && el.href.split(base)[0] !== loc.href.split(base)[0] // outside of #base + || !go(getPathFromBase(el.href), el.title || doc.title) // route not found + )) { return } + + e.preventDefault(); +} + +/** + * Go to the path + * @param {string} path - destination path + * @param {string} title - page title + * @param {boolean} shouldReplace - use replaceState or pushState + * @returns {boolean} - route not found flag + */ +function go(path, title, shouldReplace) { + // Server-side usage: directly execute handlers for the path + if (!hist) { return central[TRIGGER]('emit', getPathFromBase(path)) } + + path = base + normalize(path); + title = title || doc.title; + // browsers ignores the second parameter `title` + shouldReplace + ? hist.replaceState(null, title, path) + : hist.pushState(null, title, path); + // so we need to set it manually + doc.title = title; + routeFound = false; + emit(); + return routeFound +} + +/** + * Go to path or set action + * a single string: go there + * two strings: go there with setting a title + * two strings and boolean: replace history with setting a title + * a single function: set an action on the default route + * a string/RegExp and a function: set an action on the route + * @param {(string|function)} first - path / action / filter + * @param {(string|RegExp|function)} second - title / action + * @param {boolean} third - replace flag + */ +prot.m = function(first, second, third) { + if (isString(first) && (!second || isString(second))) { go(first, second, third || false); } + else if (second) { this.r(first, second); } + else { this.r('@', first); } +}; + +/** + * Stop routing + */ +prot.s = function() { + this.off('*'); + this.$ = []; +}; + +/** + * Emit + * @param {string} path - path + */ +prot.e = function(path) { + this.$.concat('@').some(function(filter) { + var args = (filter === '@' ? parser : secondParser)(normalize(path), normalize(filter)); + if (typeof args != 'undefined') { + this[TRIGGER].apply(null, [filter].concat(args)); + return routeFound = true // exit from loop + } + }, this); +}; + +/** + * Register route + * @param {string} filter - filter for matching to url + * @param {function} action - action to register + */ +prot.r = function(filter, action) { + if (filter !== '@') { + filter = '/' + normalize(filter); + this.$.push(filter); + } + this.on(filter, action); +}; + +var mainRouter = new Router(); +var route$1 = mainRouter.m.bind(mainRouter); + +/** + * Create a sub router + * @returns {function} the method of a new Router object + */ +route$1.create = function() { + var newSubRouter = new Router(); + // assign sub-router's main method + var router = newSubRouter.m.bind(newSubRouter); + // stop only this sub-router + router.stop = newSubRouter.s.bind(newSubRouter); + return router +}; + +/** + * Set the base of url + * @param {(str|RegExp)} arg - a new base or '#' or '#!' + */ +route$1.base = function(arg) { + base = arg || '#'; + current = getPathFromBase(); // recalculate current path +}; + +/** Exec routing right now **/ +route$1.exec = function() { + emit(true); +}; + +/** + * Replace the default router to yours + * @param {function} fn - your parser function + * @param {function} fn2 - your secondParser function + */ +route$1.parser = function(fn, fn2) { + if (!fn && !fn2) { + // reset parser for testing... + parser = DEFAULT_PARSER; + secondParser = DEFAULT_SECOND_PARSER; + } + if (fn) { parser = fn; } + if (fn2) { secondParser = fn2; } +}; + +/** + * Helper function to get url query as an object + * @returns {object} parsed query + */ +route$1.query = function() { + var q = {}; + var href = loc.href || current; + href.replace(/[?&](.+?)=([^&]*)/g, function(_, k, v) { q[k] = v; }); + return q +}; + +/** Stop routing **/ +route$1.stop = function () { + if (started) { + if (win) { + win[REMOVE_EVENT_LISTENER](POPSTATE, debouncedEmit); + win[REMOVE_EVENT_LISTENER](HASHCHANGE, debouncedEmit); + doc[REMOVE_EVENT_LISTENER](clickEvent, click); + } + central[TRIGGER]('stop'); + started = false; + } +}; + +/** + * Start routing + * @param {boolean} autoExec - automatically exec after starting if true + */ +route$1.start = function (autoExec) { + if (!started) { + if (win) { + if (document.readyState === 'complete') { start(autoExec); } + // the timeout is needed to solve + // a weird safari bug https://github.com/riot/route/issues/33 + else { win[ADD_EVENT_LISTENER]('load', function() { + setTimeout(function() { start(autoExec); }, 1); + }); } + } + started = true; + } +}; + +/** Prepare the router **/ +route$1.base(); +route$1.parser(); + +riot.tag2('router', '', '', '', function(opts) { + var this$1 = this; + + + if (opts.base) { route$1.base(opts.base); } + this.route = route$1.create(); + this.select = function (target) { + [].concat(this$1.tags.route) + .forEach(function (r) { return r.show = (r === target); }); + }; + + this.on('mount', function () { + + window.setTimeout(function () { return route$1.start(true); }, 0); + }); +}); + +riot.tag2('route', '', '', '', function(opts) { + var this$1 = this; + + this.show = false; + this.parent.route(opts.path, function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + + this$1.one('updated', function () { + flatten(this$1.tags).forEach(function (tag) { + tag.trigger.apply(tag, [ 'route' ].concat( args )); + tag.update(); + }); + }); + this$1.parent.select(this$1); + this$1.parent.update(); + }); + + function flatten(tags) { + return Object.keys(tags) + .map(function (key) { return tags[key]; }) + .reduce(function (acc, tag) { return acc.concat(tag); }, []) + } +}); + +return route$1; + +}(riot)); diff --git a/dist/route+tag.min.js b/dist/route+tag.min.js new file mode 100644 index 0000000..d2a70e0 --- /dev/null +++ b/dist/route+tag.min.js @@ -0,0 +1 @@ +var route=function(e){"use strict";e="default"in e?e["default"]:e;var t=function(e){e=e||{};var t={},r=Array.prototype.slice;Object.defineProperties(e,{on:{value:function(r,n){if(typeof n=="function"){(t[r]=t[r]||[]).push(n)}return e},enumerable:false,writable:false,configurable:false},off:{value:function(r,n){if(r=="*"&&!n){t={}}else{if(n){var i=t[r];for(var a=0,f;f=i&&i[a];++a){if(f==n){i.splice(a--,1)}}}else{delete t[r]}}return e},enumerable:false,writable:false,configurable:false},one:{value:function(t,r){function n(){e.off(t,n);r.apply(e,arguments)}return e.on(t,n)},enumerable:false,writable:false,configurable:false},trigger:{value:function(n){var i=arguments;var a=arguments.length-1,f=new Array(a),u,o,c;for(c=0;c","","",function(e){var t=this;if(e.base){C.base(e.base)}this.route=C.create();this.select=function(e){[].concat(t.tags.route).forEach(function(t){return t.show=t===e})};this.on("mount",function(){window.setTimeout(function(){return C.start(true)},0)})});e.tag2("route",'',"","",function(e){var t=this;this.show=false;this.parent.route(e.path,function(){var e=[],n=arguments.length;while(n--)e[n]=arguments[n];t.one("updated",function(){r(t.tags).forEach(function(t){t.trigger.apply(t,["route"].concat(e));t.update()})});t.parent.select(t);t.parent.update()});function r(e){return Object.keys(e).map(function(t){return e[t]}).reduce(function(e,t){return e.concat(t)},[])}});return C}(riot); \ No newline at end of file diff --git a/es.tag.js b/es.tag.js new file mode 100644 index 0000000..565e938 --- /dev/null +++ b/es.tag.js @@ -0,0 +1,47 @@ +import route from 'riot-route'; +import riot from 'riot'; + +riot.tag2('router', '', '', '', function(opts) { + var this$1 = this; + + + if (opts.base) { route.base(opts.base); } + this.route = route.create(); + this.select = function (target) { + [].concat(this$1.tags.route) + .forEach(function (r) { return r.show = (r === target); }); + }; + + this.on('mount', function () { + + window.setTimeout(function () { return route.start(true); }, 0); + }); +}); + +riot.tag2('route', '', '', '', function(opts) { + var this$1 = this; + + this.show = false; + this.parent.route(opts.path, function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + + this$1.one('updated', function () { + flatten(this$1.tags).forEach(function (tag) { + tag.trigger.apply(tag, [ 'route' ].concat( args )); + tag.update(); + }); + }); + this$1.parent.select(this$1); + this$1.parent.update(); + }); + + function flatten(tags) { + return Object.keys(tags) + .map(function (key) { return tags[key]; }) + .reduce(function (acc, tag) { return acc.concat(tag); }, []) + } +}); + +export default route; diff --git a/package.json b/package.json index a79e12b..e318e00 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "pretest": "npm run build", "test": "npm run eslint && npm run karma && npm run mocha", "prepublish": "npm run build", - "build": "npm run rollup && npm run uglify && npm run uglify-amd", + "build": "npm run rollup && npm run uglify && npm run uglify-amd && npm run uglify-tag && npm run uglify-tag-amd", "rollup": "node rollup.js", "uglify": "uglifyjs dist/route.js --comments --mangle -o dist/route.min.js", "uglify-amd": "uglifyjs dist/amd.route.js --comments --mangle -o dist/amd.route.min.js", + "uglify-tag": "uglifyjs dist/route+tag.js --comments --mangle -o dist/route+tag.min.js", + "uglify-tag-amd": "uglifyjs dist/amd.route+tag.js --comments --mangle -o dist/amd.route+tag.min.js", "watch": "chokidar lib/* lib/**/* -c 'npm run build'", "eslint": "eslint lib test/specs", "karma": "karma start test/karma.conf.js", @@ -48,8 +50,9 @@ "karma-mocha-reporter": "^2.2.1", "mocha": "^3.1.2", "rollup": "^0.36.3", - "rollup-plugin-buble": "^0.14.0", - "rollup-plugin-node-resolve": "^2.0.0", + "rollup-plugin-alias": "^1.2.0", + "rollup-plugin-buble": "^0.15.0", + "rollup-plugin-riot": "^1.1.0", "uglify-js": "^2.7.4" }, "author": "Muut, Inc. and other contributors", diff --git a/rollup.js b/rollup.js index b719463..9acf084 100644 --- a/rollup.js +++ b/rollup.js @@ -1,15 +1,19 @@ const rollup = require('rollup'), buble = require('rollup-plugin-buble'), - nodeResolve = require('rollup-plugin-node-resolve') + alias = require('rollup-plugin-alias'), + riot = require('rollup-plugin-riot') rollup .rollup({ entry: 'lib/index.js', - plugins: [nodeResolve({ jsnext: true }), buble()] + plugins: [ + alias({ 'riot-observable': 'node_modules/riot-observable/dist/es6.observable.js' }), + buble() + ] }) .then(bundle => { - bundle.write({ format: 'iife', moduleName: 'route', dest: 'dist/route.js'}) + bundle.write({ format: 'iife', moduleName: 'route', dest: 'dist/route.js' }) bundle.write({ format: 'amd', dest: 'dist/amd.route.js' }) }) .catch(error => { @@ -28,3 +32,43 @@ rollup .catch(error => { console.error(error) }) + +rollup + .rollup({ + entry: 'lib/tag.js', + external: ['riot'], + plugins: [ + riot(), + alias({ + 'riot-observable': 'node_modules/riot-observable/dist/es6.observable.js', + 'riot-route': 'lib/index.js' + }), + buble() + ] + }) + .then(bundle => { + bundle.write({ + format: 'iife', + moduleName: 'route', + globals: { riot: 'riot' }, + dest: 'dist/route+tag.js' + }) + bundle.write({ format: 'amd', dest: 'dist/amd.route+tag.js' }) + }) + .catch(error => { + console.error(error) + }) + +rollup + .rollup({ + entry: 'lib/tag.js', + external: ['riot', 'riot-observable', 'riot-route'], + plugins: [riot(), buble()] + }) + .then(bundle => { + bundle.write({ format: 'es', dest: 'es.tag.js' }) + bundle.write({ format: 'cjs', dest: 'tag.js' }) + }) + .catch(error => { + console.error(error) + }) diff --git a/tag.js b/tag.js new file mode 100644 index 0000000..4a75871 --- /dev/null +++ b/tag.js @@ -0,0 +1,51 @@ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var route = _interopDefault(require('riot-route')); +var riot = _interopDefault(require('riot')); + +riot.tag2('router', '', '', '', function(opts) { + var this$1 = this; + + + if (opts.base) { route.base(opts.base); } + this.route = route.create(); + this.select = function (target) { + [].concat(this$1.tags.route) + .forEach(function (r) { return r.show = (r === target); }); + }; + + this.on('mount', function () { + + window.setTimeout(function () { return route.start(true); }, 0); + }); +}); + +riot.tag2('route', '', '', '', function(opts) { + var this$1 = this; + + this.show = false; + this.parent.route(opts.path, function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + + this$1.one('updated', function () { + flatten(this$1.tags).forEach(function (tag) { + tag.trigger.apply(tag, [ 'route' ].concat( args )); + tag.update(); + }); + }); + this$1.parent.select(this$1); + this$1.parent.update(); + }); + + function flatten(tags) { + return Object.keys(tags) + .map(function (key) { return tags[key]; }) + .reduce(function (acc, tag) { return acc.concat(tag); }, []) + } +}); + +module.exports = route; From 81e234a7e43b6530f43d9f7244d4618384e8315f Mon Sep 17 00:00:00 2001 From: Tsutomu Kawamura Date: Fri, 23 Dec 2016 23:47:22 +0900 Subject: [PATCH 3/8] Adds some tests for route-tag --- package.json | 6 ++++-- test/karma.conf.js | 15 ++++++++++----- test/specs/tag.specs.js | 37 +++++++++++++++++++++++++++++++++++++ test/tags/no-param.tag | 7 +++++++ test/tags/param.tag | 12 ++++++++++++ 5 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 test/specs/tag.specs.js create mode 100644 test/tags/no-param.tag create mode 100644 test/tags/param.tag diff --git a/package.json b/package.json index e318e00..e923ffb 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "devDependencies": { "chokidar-cli": "^1.2.0", "coveralls": "^2.11.15", - "electron-prebuilt": "^1.3.9", + "electron": "^1.3.9", "eslint": "^3.10.2", "expect.js": "^0.3.1", "karma": "^1.3.0", @@ -48,8 +48,10 @@ "karma-electron": "^5.1.1", "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.1", + "karma-riot": "^2.0.0", "mocha": "^3.1.2", - "rollup": "^0.36.3", + "riot": "^3.0.5", + "rollup": "^0.38.0", "rollup-plugin-alias": "^1.2.0", "rollup-plugin-buble": "^0.15.0", "rollup-plugin-riot": "^1.1.0", diff --git a/test/karma.conf.js b/test/karma.conf.js index 73da488..5ea6db2 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -12,24 +12,29 @@ module.exports = function(config) { config.set({ basePath: '', - frameworks: ['mocha'], + frameworks: ['mocha', 'riot'], plugins: [ 'karma-mocha', 'karma-mocha-reporter', 'karma-coverage', 'karma-browserstack-launcher', - 'karma-electron' + 'karma-electron', + 'karma-riot' ], files: [ '../node_modules/expect.js/index.js', - '../dist/route.js', - 'specs/core.specs.js' + '../node_modules/riot/riot.js', + '../dist/route+tag.js', + 'tags/*.tag', + 'specs/core.specs.js', + 'specs/tag.specs.js' ], browsers: browsers, customLaunchers: customLaunchers, reporters: ['mocha', 'coverage'], preprocessors: { - '../dist/route.js': ['coverage'] + 'tags/*.tag': ['riot'], + '../dist/route-tag.js': ['coverage'] }, coverageReporter: { dir: '../coverage/', diff --git a/test/specs/tag.specs.js b/test/specs/tag.specs.js new file mode 100644 index 0000000..93e3748 --- /dev/null +++ b/test/specs/tag.specs.js @@ -0,0 +1,37 @@ +/* global riot */ + +describe('Tag specs', function() { + + before(function() { + route.stop() + // create mounting points + var html = document.createElement('app') + document.body.appendChild(html) + }) + + after(function() { + if (window.history && window.history.replaceState) { + window.history.replaceState(null, '', window.location.pathname) + } + }) + + afterEach(function() { + route.stop() // detouch all routings + route.base() // reset base + route.parser() // reset parser + }) + + it('mounts no-param tag', function() { + riot.mount('app', 'no-param') + route('apple') + expect(document.querySelector('router p').textContent) + .to.be('Apple') + }) + + it('mounts param tag', function() { + riot.mount('app', 'param') + route('abc') + expect(document.querySelector('router p').textContent) + .to.be('abc') + }) +}) diff --git a/test/tags/no-param.tag b/test/tags/no-param.tag new file mode 100644 index 0000000..59394ca --- /dev/null +++ b/test/tags/no-param.tag @@ -0,0 +1,7 @@ + + +

Apple

+

Banana

+

Coffee

+
+
diff --git a/test/tags/param.tag b/test/tags/param.tag new file mode 100644 index 0000000..a3a0754 --- /dev/null +++ b/test/tags/param.tag @@ -0,0 +1,12 @@ + + + + + + + +

{ name || '(undefined)' }

+ +
From 61e946b27f338cbbfb25ad85545829123ae161ba Mon Sep 17 00:00:00 2001 From: Tsutomu Kawamura Date: Sat, 24 Dec 2016 00:38:14 +0900 Subject: [PATCH 4/8] Makes `lib` as output dir for ES6: - es.tag.js --> lib/tag.js - Moves source code into `src` dir --- es.tag.js | 47 ----- lib/index.js | 237 ++++++++++++------------ lib/tag.js | 50 ++++- package.json | 4 +- rollup.js | 13 +- src/index.js | 344 +++++++++++++++++++++++++++++++++++ src/tag.js | 7 + {lib => src}/tags/route.tag | 0 {lib => src}/tags/router.tag | 0 9 files changed, 523 insertions(+), 179 deletions(-) delete mode 100644 es.tag.js create mode 100644 src/index.js create mode 100644 src/tag.js rename {lib => src}/tags/route.tag (100%) rename {lib => src}/tags/router.tag (100%) diff --git a/es.tag.js b/es.tag.js deleted file mode 100644 index 565e938..0000000 --- a/es.tag.js +++ /dev/null @@ -1,47 +0,0 @@ -import route from 'riot-route'; -import riot from 'riot'; - -riot.tag2('router', '', '', '', function(opts) { - var this$1 = this; - - - if (opts.base) { route.base(opts.base); } - this.route = route.create(); - this.select = function (target) { - [].concat(this$1.tags.route) - .forEach(function (r) { return r.show = (r === target); }); - }; - - this.on('mount', function () { - - window.setTimeout(function () { return route.start(true); }, 0); - }); -}); - -riot.tag2('route', '', '', '', function(opts) { - var this$1 = this; - - this.show = false; - this.parent.route(opts.path, function () { - var args = [], len = arguments.length; - while ( len-- ) args[ len ] = arguments[ len ]; - - - this$1.one('updated', function () { - flatten(this$1.tags).forEach(function (tag) { - tag.trigger.apply(tag, [ 'route' ].concat( args )); - tag.update(); - }); - }); - this$1.parent.select(this$1); - this$1.parent.update(); - }); - - function flatten(tags) { - return Object.keys(tags) - .map(function (key) { return tags[key]; }) - .reduce(function (acc, tag) { return acc.concat(tag); }, []) - } -}); - -export default route; diff --git a/lib/index.js b/lib/index.js index f8c8932..23d121b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,39 +1,36 @@ -'use strict' +import observable from 'riot-observable'; /** * Simple client-side router * @module riot-route */ -import observable from 'riot-observable' - -const RE_ORIGIN = /^.+?\/\/+[^\/]+/, - EVENT_LISTENER = 'EventListener', - REMOVE_EVENT_LISTENER = 'remove' + EVENT_LISTENER, - ADD_EVENT_LISTENER = 'add' + EVENT_LISTENER, - HAS_ATTRIBUTE = 'hasAttribute', - POPSTATE = 'popstate', - HASHCHANGE = 'hashchange', - TRIGGER = 'trigger', - MAX_EMIT_STACK_LEVEL = 3, - win = typeof window != 'undefined' && window, - doc = typeof document != 'undefined' && document, - hist = win && history, - loc = win && (hist.location || win.location), // see html5-history-api - prot = Router.prototype, // to minify more - clickEvent = doc && doc.ontouchstart ? 'touchstart' : 'click', - central = observable() - -let - started = false, - routeFound = false, - debouncedEmit, - base, - current, - parser, - secondParser, - emitStack = [], - emitStackLevel = 0 +var RE_ORIGIN = /^.+?\/\/+[^\/]+/; +var EVENT_LISTENER = 'EventListener'; +var REMOVE_EVENT_LISTENER = 'remove' + EVENT_LISTENER; +var ADD_EVENT_LISTENER = 'add' + EVENT_LISTENER; +var HAS_ATTRIBUTE = 'hasAttribute'; +var POPSTATE = 'popstate'; +var HASHCHANGE = 'hashchange'; +var TRIGGER = 'trigger'; +var MAX_EMIT_STACK_LEVEL = 3; +var win = typeof window != 'undefined' && window; +var doc = typeof document != 'undefined' && document; +var hist = win && history; +var loc = win && (hist.location || win.location); +var prot = Router.prototype; +var clickEvent = doc && doc.ontouchstart ? 'touchstart' : 'click'; +var central = observable(); + +var started = false; +var routeFound = false; +var debouncedEmit; +var base; +var current; +var parser; +var secondParser; +var emitStack = []; +var emitStackLevel = 0; /** * Default parser. You can replace it via router.parser method. @@ -51,14 +48,14 @@ function DEFAULT_PARSER(path) { * @returns {array} array */ function DEFAULT_SECOND_PARSER(path, filter) { - const f = filter + var f = filter .replace(/\?/g, '\\?') .replace(/\*/g, '([^/?#]+?)') - .replace(/\.\./, '.*') - const re = new RegExp(`^${f}$`) - const args = path.match(re) + .replace(/\.\./, '.*'); + var re = new RegExp(("^" + f + "$")); + var args = path.match(re); - if (args) return args.slice(1) + if (args) { return args.slice(1) } } /** @@ -68,10 +65,10 @@ function DEFAULT_SECOND_PARSER(path, filter) { * @returns {function} debounced function */ function debounce(fn, delay) { - let t + var t; return function () { - clearTimeout(t) - t = setTimeout(fn, delay) + clearTimeout(t); + t = setTimeout(fn, delay); } } @@ -80,21 +77,21 @@ function debounce(fn, delay) { * @param {boolean} autoExec - see route.start */ function start(autoExec) { - debouncedEmit = debounce(emit, 1) - win[ADD_EVENT_LISTENER](POPSTATE, debouncedEmit) - win[ADD_EVENT_LISTENER](HASHCHANGE, debouncedEmit) - doc[ADD_EVENT_LISTENER](clickEvent, click) - if (autoExec) emit(true) + debouncedEmit = debounce(emit, 1); + win[ADD_EVENT_LISTENER](POPSTATE, debouncedEmit); + win[ADD_EVENT_LISTENER](HASHCHANGE, debouncedEmit); + doc[ADD_EVENT_LISTENER](clickEvent, click); + if (autoExec) { emit(true); } } /** * Router class */ function Router() { - this.$ = [] - observable(this) // make it observable - central.on('stop', this.s.bind(this)) - central.on('emit', this.e.bind(this)) + this.$ = []; + observable(this); // make it observable + central.on('stop', this.s.bind(this)); + central.on('emit', this.e.bind(this)); } function normalize(path) { @@ -127,21 +124,21 @@ function getPathFromBase(href) { function emit(force) { // the stack is needed for redirections - const isRoot = emitStackLevel === 0 - if (MAX_EMIT_STACK_LEVEL <= emitStackLevel) return + var isRoot = emitStackLevel === 0; + if (MAX_EMIT_STACK_LEVEL <= emitStackLevel) { return } - emitStackLevel++ + emitStackLevel++; emitStack.push(function() { - const path = getPathFromBase() + var path = getPathFromBase(); if (force || path !== current) { - central[TRIGGER]('emit', path) - current = path + central[TRIGGER]('emit', path); + current = path; } - }) + }); if (isRoot) { - let first - while (first = emitStack.shift()) first() // stack increses within this call - emitStackLevel = 0 + var first; + while (first = emitStack.shift()) { first(); } // stack increses within this call + emitStackLevel = 0; } } @@ -150,10 +147,10 @@ function click(e) { e.which !== 1 // not left click || e.metaKey || e.ctrlKey || e.shiftKey // or meta keys || e.defaultPrevented // or default prevented - ) return + ) { return } - let el = e.target - while (el && el.nodeName !== 'A') el = el.parentNode + var el = e.target; + while (el && el.nodeName !== 'A') { el = el.parentNode; } if ( !el || el.nodeName !== 'A' // not A tag @@ -161,7 +158,7 @@ function click(e) { || !el[HAS_ATTRIBUTE]('href') // has no href attr || el.target && el.target !== '_self' // another window or frame || el.href.indexOf(loc.href.match(RE_ORIGIN)[0]) === -1 // cross origin - ) return + ) { return } if (el.href !== loc.href && ( @@ -169,9 +166,9 @@ function click(e) { || base[0] !== '#' && getPathFromRoot(el.href).indexOf(base) !== 0 // outside of base || base[0] === '#' && el.href.split(base)[0] !== loc.href.split(base)[0] // outside of #base || !go(getPathFromBase(el.href), el.title || doc.title) // route not found - )) return + )) { return } - e.preventDefault() + e.preventDefault(); } /** @@ -183,18 +180,18 @@ function click(e) { */ function go(path, title, shouldReplace) { // Server-side usage: directly execute handlers for the path - if (!hist) return central[TRIGGER]('emit', getPathFromBase(path)) + if (!hist) { return central[TRIGGER]('emit', getPathFromBase(path)) } - path = base + normalize(path) - title = title || doc.title + path = base + normalize(path); + title = title || doc.title; // browsers ignores the second parameter `title` shouldReplace ? hist.replaceState(null, title, path) - : hist.pushState(null, title, path) + : hist.pushState(null, title, path); // so we need to set it manually - doc.title = title - routeFound = false - emit() + doc.title = title; + routeFound = false; + emit(); return routeFound } @@ -210,18 +207,18 @@ function go(path, title, shouldReplace) { * @param {boolean} third - replace flag */ prot.m = function(first, second, third) { - if (isString(first) && (!second || isString(second))) go(first, second, third || false) - else if (second) this.r(first, second) - else this.r('@', first) -} + if (isString(first) && (!second || isString(second))) { go(first, second, third || false); } + else if (second) { this.r(first, second); } + else { this.r('@', first); } +}; /** * Stop routing */ prot.s = function() { - this.off('*') - this.$ = [] -} + this.off('*'); + this.$ = []; +}; /** * Emit @@ -229,13 +226,13 @@ prot.s = function() { */ prot.e = function(path) { this.$.concat('@').some(function(filter) { - const args = (filter === '@' ? parser : secondParser)(normalize(path), normalize(filter)) + var args = (filter === '@' ? parser : secondParser)(normalize(path), normalize(filter)); if (typeof args != 'undefined') { - this[TRIGGER].apply(null, [filter].concat(args)) + this[TRIGGER].apply(null, [filter].concat(args)); return routeFound = true // exit from loop } - }, this) -} + }, this); +}; /** * Register route @@ -244,41 +241,41 @@ prot.e = function(path) { */ prot.r = function(filter, action) { if (filter !== '@') { - filter = '/' + normalize(filter) - this.$.push(filter) + filter = '/' + normalize(filter); + this.$.push(filter); } - this.on(filter, action) -} + this.on(filter, action); +}; -const mainRouter = new Router() -const route = mainRouter.m.bind(mainRouter) +var mainRouter = new Router(); +var route = mainRouter.m.bind(mainRouter); /** * Create a sub router * @returns {function} the method of a new Router object */ route.create = function() { - const newSubRouter = new Router() + var newSubRouter = new Router(); // assign sub-router's main method - const router = newSubRouter.m.bind(newSubRouter) + var router = newSubRouter.m.bind(newSubRouter); // stop only this sub-router - router.stop = newSubRouter.s.bind(newSubRouter) + router.stop = newSubRouter.s.bind(newSubRouter); return router -} +}; /** * Set the base of url * @param {(str|RegExp)} arg - a new base or '#' or '#!' */ route.base = function(arg) { - base = arg || '#' - current = getPathFromBase() // recalculate current path -} + base = arg || '#'; + current = getPathFromBase(); // recalculate current path +}; /** Exec routing right now **/ route.exec = function() { - emit(true) -} + emit(true); +}; /** * Replace the default router to yours @@ -288,36 +285,36 @@ route.exec = function() { route.parser = function(fn, fn2) { if (!fn && !fn2) { // reset parser for testing... - parser = DEFAULT_PARSER - secondParser = DEFAULT_SECOND_PARSER + parser = DEFAULT_PARSER; + secondParser = DEFAULT_SECOND_PARSER; } - if (fn) parser = fn - if (fn2) secondParser = fn2 -} + if (fn) { parser = fn; } + if (fn2) { secondParser = fn2; } +}; /** * Helper function to get url query as an object * @returns {object} parsed query */ route.query = function() { - const q = {} - const href = loc.href || current - href.replace(/[?&](.+?)=([^&]*)/g, function(_, k, v) { q[k] = v }) + var q = {}; + var href = loc.href || current; + href.replace(/[?&](.+?)=([^&]*)/g, function(_, k, v) { q[k] = v; }); return q -} +}; /** Stop routing **/ route.stop = function () { if (started) { if (win) { - win[REMOVE_EVENT_LISTENER](POPSTATE, debouncedEmit) - win[REMOVE_EVENT_LISTENER](HASHCHANGE, debouncedEmit) - doc[REMOVE_EVENT_LISTENER](clickEvent, click) + win[REMOVE_EVENT_LISTENER](POPSTATE, debouncedEmit); + win[REMOVE_EVENT_LISTENER](HASHCHANGE, debouncedEmit); + doc[REMOVE_EVENT_LISTENER](clickEvent, click); } - central[TRIGGER]('stop') - started = false + central[TRIGGER]('stop'); + started = false; } -} +}; /** * Start routing @@ -326,19 +323,19 @@ route.stop = function () { route.start = function (autoExec) { if (!started) { if (win) { - if (document.readyState === 'complete') start(autoExec) + if (document.readyState === 'complete') { start(autoExec); } // the timeout is needed to solve // a weird safari bug https://github.com/riot/route/issues/33 - else win[ADD_EVENT_LISTENER]('load', function() { - setTimeout(function() { start(autoExec) }, 1) - }) + else { win[ADD_EVENT_LISTENER]('load', function() { + setTimeout(function() { start(autoExec); }, 1); + }); } } - started = true + started = true; } -} +}; /** Prepare the router **/ -route.base() -route.parser() +route.base(); +route.parser(); -export default route +export default route; diff --git a/lib/tag.js b/lib/tag.js index 612821c..565e938 100644 --- a/lib/tag.js +++ b/lib/tag.js @@ -1,7 +1,47 @@ -'use strict' +import route from 'riot-route'; +import riot from 'riot'; -import route from 'riot-route' -import './tags/router.tag' -import './tags/route.tag' +riot.tag2('router', '', '', '', function(opts) { + var this$1 = this; -export default route + + if (opts.base) { route.base(opts.base); } + this.route = route.create(); + this.select = function (target) { + [].concat(this$1.tags.route) + .forEach(function (r) { return r.show = (r === target); }); + }; + + this.on('mount', function () { + + window.setTimeout(function () { return route.start(true); }, 0); + }); +}); + +riot.tag2('route', '', '', '', function(opts) { + var this$1 = this; + + this.show = false; + this.parent.route(opts.path, function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + + this$1.one('updated', function () { + flatten(this$1.tags).forEach(function (tag) { + tag.trigger.apply(tag, [ 'route' ].concat( args )); + tag.update(); + }); + }); + this$1.parent.select(this$1); + this$1.parent.update(); + }); + + function flatten(tags) { + return Object.keys(tags) + .map(function (key) { return tags[key]; }) + .reduce(function (acc, tag) { return acc.concat(tag); }, []) + } +}); + +export default route; diff --git a/package.json b/package.json index e923ffb..15c45b3 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "description": "Simple isomorphic router", "main": "dist/cjs.route.js", "jsnext:main": "lib/index.js", + "module": "lib/index.js", + "modules.root": "lib", "directories": { "lib": "lib", "doc": "doc" @@ -19,7 +21,7 @@ "uglify-tag": "uglifyjs dist/route+tag.js --comments --mangle -o dist/route+tag.min.js", "uglify-tag-amd": "uglifyjs dist/amd.route+tag.js --comments --mangle -o dist/amd.route+tag.min.js", "watch": "chokidar lib/* lib/**/* -c 'npm run build'", - "eslint": "eslint lib test/specs", + "eslint": "eslint src test/specs", "karma": "karma start test/karma.conf.js", "karma-bs": "BROWSERSTACK=1 karma start test/karma.conf.js", "mocha": "mocha test/specs/server.specs.js", diff --git a/rollup.js b/rollup.js index 9acf084..019c07c 100644 --- a/rollup.js +++ b/rollup.js @@ -6,7 +6,7 @@ const rollup .rollup({ - entry: 'lib/index.js', + entry: 'src/index.js', plugins: [ alias({ 'riot-observable': 'node_modules/riot-observable/dist/es6.observable.js' }), buble() @@ -22,11 +22,12 @@ rollup rollup .rollup({ - entry: 'lib/index.js', + entry: 'src/index.js', external: ['riot-observable'], plugins: [buble()] }) .then(bundle => { + bundle.write({ format: 'es', dest: 'lib/index.js' }) bundle.write({ format: 'cjs', dest: 'dist/cjs.route.js' }) }) .catch(error => { @@ -35,13 +36,13 @@ rollup rollup .rollup({ - entry: 'lib/tag.js', + entry: 'src/tag.js', external: ['riot'], plugins: [ riot(), alias({ 'riot-observable': 'node_modules/riot-observable/dist/es6.observable.js', - 'riot-route': 'lib/index.js' + 'riot-route': 'src/index.js' }), buble() ] @@ -61,12 +62,12 @@ rollup rollup .rollup({ - entry: 'lib/tag.js', + entry: 'src/tag.js', external: ['riot', 'riot-observable', 'riot-route'], plugins: [riot(), buble()] }) .then(bundle => { - bundle.write({ format: 'es', dest: 'es.tag.js' }) + bundle.write({ format: 'es', dest: 'lib/tag.js' }) bundle.write({ format: 'cjs', dest: 'tag.js' }) }) .catch(error => { diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f8c8932 --- /dev/null +++ b/src/index.js @@ -0,0 +1,344 @@ +'use strict' + +/** + * Simple client-side router + * @module riot-route + */ + +import observable from 'riot-observable' + +const RE_ORIGIN = /^.+?\/\/+[^\/]+/, + EVENT_LISTENER = 'EventListener', + REMOVE_EVENT_LISTENER = 'remove' + EVENT_LISTENER, + ADD_EVENT_LISTENER = 'add' + EVENT_LISTENER, + HAS_ATTRIBUTE = 'hasAttribute', + POPSTATE = 'popstate', + HASHCHANGE = 'hashchange', + TRIGGER = 'trigger', + MAX_EMIT_STACK_LEVEL = 3, + win = typeof window != 'undefined' && window, + doc = typeof document != 'undefined' && document, + hist = win && history, + loc = win && (hist.location || win.location), // see html5-history-api + prot = Router.prototype, // to minify more + clickEvent = doc && doc.ontouchstart ? 'touchstart' : 'click', + central = observable() + +let + started = false, + routeFound = false, + debouncedEmit, + base, + current, + parser, + secondParser, + emitStack = [], + emitStackLevel = 0 + +/** + * Default parser. You can replace it via router.parser method. + * @param {string} path - current path (normalized) + * @returns {array} array + */ +function DEFAULT_PARSER(path) { + return path.split(/[/?#]/) +} + +/** + * Default parser (second). You can replace it via router.parser method. + * @param {string} path - current path (normalized) + * @param {string} filter - filter string (normalized) + * @returns {array} array + */ +function DEFAULT_SECOND_PARSER(path, filter) { + const f = filter + .replace(/\?/g, '\\?') + .replace(/\*/g, '([^/?#]+?)') + .replace(/\.\./, '.*') + const re = new RegExp(`^${f}$`) + const args = path.match(re) + + if (args) return args.slice(1) +} + +/** + * Simple/cheap debounce implementation + * @param {function} fn - callback + * @param {number} delay - delay in seconds + * @returns {function} debounced function + */ +function debounce(fn, delay) { + let t + return function () { + clearTimeout(t) + t = setTimeout(fn, delay) + } +} + +/** + * Set the window listeners to trigger the routes + * @param {boolean} autoExec - see route.start + */ +function start(autoExec) { + debouncedEmit = debounce(emit, 1) + win[ADD_EVENT_LISTENER](POPSTATE, debouncedEmit) + win[ADD_EVENT_LISTENER](HASHCHANGE, debouncedEmit) + doc[ADD_EVENT_LISTENER](clickEvent, click) + if (autoExec) emit(true) +} + +/** + * Router class + */ +function Router() { + this.$ = [] + observable(this) // make it observable + central.on('stop', this.s.bind(this)) + central.on('emit', this.e.bind(this)) +} + +function normalize(path) { + return path.replace(/^\/|\/$/, '') +} + +function isString(str) { + return typeof str == 'string' +} + +/** + * Get the part after domain name + * @param {string} href - fullpath + * @returns {string} path from root + */ +function getPathFromRoot(href) { + return (href || loc.href).replace(RE_ORIGIN, '') +} + +/** + * Get the part after base + * @param {string} href - fullpath + * @returns {string} path from base + */ +function getPathFromBase(href) { + return base[0] === '#' + ? (href || loc.href || '').split(base)[1] || '' + : (loc ? getPathFromRoot(href) : href || '').replace(base, '') +} + +function emit(force) { + // the stack is needed for redirections + const isRoot = emitStackLevel === 0 + if (MAX_EMIT_STACK_LEVEL <= emitStackLevel) return + + emitStackLevel++ + emitStack.push(function() { + const path = getPathFromBase() + if (force || path !== current) { + central[TRIGGER]('emit', path) + current = path + } + }) + if (isRoot) { + let first + while (first = emitStack.shift()) first() // stack increses within this call + emitStackLevel = 0 + } +} + +function click(e) { + if ( + e.which !== 1 // not left click + || e.metaKey || e.ctrlKey || e.shiftKey // or meta keys + || e.defaultPrevented // or default prevented + ) return + + let el = e.target + while (el && el.nodeName !== 'A') el = el.parentNode + + if ( + !el || el.nodeName !== 'A' // not A tag + || el[HAS_ATTRIBUTE]('download') // has download attr + || !el[HAS_ATTRIBUTE]('href') // has no href attr + || el.target && el.target !== '_self' // another window or frame + || el.href.indexOf(loc.href.match(RE_ORIGIN)[0]) === -1 // cross origin + ) return + + if (el.href !== loc.href + && ( + el.href.split('#')[0] === loc.href.split('#')[0] // internal jump + || base[0] !== '#' && getPathFromRoot(el.href).indexOf(base) !== 0 // outside of base + || base[0] === '#' && el.href.split(base)[0] !== loc.href.split(base)[0] // outside of #base + || !go(getPathFromBase(el.href), el.title || doc.title) // route not found + )) return + + e.preventDefault() +} + +/** + * Go to the path + * @param {string} path - destination path + * @param {string} title - page title + * @param {boolean} shouldReplace - use replaceState or pushState + * @returns {boolean} - route not found flag + */ +function go(path, title, shouldReplace) { + // Server-side usage: directly execute handlers for the path + if (!hist) return central[TRIGGER]('emit', getPathFromBase(path)) + + path = base + normalize(path) + title = title || doc.title + // browsers ignores the second parameter `title` + shouldReplace + ? hist.replaceState(null, title, path) + : hist.pushState(null, title, path) + // so we need to set it manually + doc.title = title + routeFound = false + emit() + return routeFound +} + +/** + * Go to path or set action + * a single string: go there + * two strings: go there with setting a title + * two strings and boolean: replace history with setting a title + * a single function: set an action on the default route + * a string/RegExp and a function: set an action on the route + * @param {(string|function)} first - path / action / filter + * @param {(string|RegExp|function)} second - title / action + * @param {boolean} third - replace flag + */ +prot.m = function(first, second, third) { + if (isString(first) && (!second || isString(second))) go(first, second, third || false) + else if (second) this.r(first, second) + else this.r('@', first) +} + +/** + * Stop routing + */ +prot.s = function() { + this.off('*') + this.$ = [] +} + +/** + * Emit + * @param {string} path - path + */ +prot.e = function(path) { + this.$.concat('@').some(function(filter) { + const args = (filter === '@' ? parser : secondParser)(normalize(path), normalize(filter)) + if (typeof args != 'undefined') { + this[TRIGGER].apply(null, [filter].concat(args)) + return routeFound = true // exit from loop + } + }, this) +} + +/** + * Register route + * @param {string} filter - filter for matching to url + * @param {function} action - action to register + */ +prot.r = function(filter, action) { + if (filter !== '@') { + filter = '/' + normalize(filter) + this.$.push(filter) + } + this.on(filter, action) +} + +const mainRouter = new Router() +const route = mainRouter.m.bind(mainRouter) + +/** + * Create a sub router + * @returns {function} the method of a new Router object + */ +route.create = function() { + const newSubRouter = new Router() + // assign sub-router's main method + const router = newSubRouter.m.bind(newSubRouter) + // stop only this sub-router + router.stop = newSubRouter.s.bind(newSubRouter) + return router +} + +/** + * Set the base of url + * @param {(str|RegExp)} arg - a new base or '#' or '#!' + */ +route.base = function(arg) { + base = arg || '#' + current = getPathFromBase() // recalculate current path +} + +/** Exec routing right now **/ +route.exec = function() { + emit(true) +} + +/** + * Replace the default router to yours + * @param {function} fn - your parser function + * @param {function} fn2 - your secondParser function + */ +route.parser = function(fn, fn2) { + if (!fn && !fn2) { + // reset parser for testing... + parser = DEFAULT_PARSER + secondParser = DEFAULT_SECOND_PARSER + } + if (fn) parser = fn + if (fn2) secondParser = fn2 +} + +/** + * Helper function to get url query as an object + * @returns {object} parsed query + */ +route.query = function() { + const q = {} + const href = loc.href || current + href.replace(/[?&](.+?)=([^&]*)/g, function(_, k, v) { q[k] = v }) + return q +} + +/** Stop routing **/ +route.stop = function () { + if (started) { + if (win) { + win[REMOVE_EVENT_LISTENER](POPSTATE, debouncedEmit) + win[REMOVE_EVENT_LISTENER](HASHCHANGE, debouncedEmit) + doc[REMOVE_EVENT_LISTENER](clickEvent, click) + } + central[TRIGGER]('stop') + started = false + } +} + +/** + * Start routing + * @param {boolean} autoExec - automatically exec after starting if true + */ +route.start = function (autoExec) { + if (!started) { + if (win) { + if (document.readyState === 'complete') start(autoExec) + // the timeout is needed to solve + // a weird safari bug https://github.com/riot/route/issues/33 + else win[ADD_EVENT_LISTENER]('load', function() { + setTimeout(function() { start(autoExec) }, 1) + }) + } + started = true + } +} + +/** Prepare the router **/ +route.base() +route.parser() + +export default route diff --git a/src/tag.js b/src/tag.js new file mode 100644 index 0000000..612821c --- /dev/null +++ b/src/tag.js @@ -0,0 +1,7 @@ +'use strict' + +import route from 'riot-route' +import './tags/router.tag' +import './tags/route.tag' + +export default route diff --git a/lib/tags/route.tag b/src/tags/route.tag similarity index 100% rename from lib/tags/route.tag rename to src/tags/route.tag diff --git a/lib/tags/router.tag b/src/tags/router.tag similarity index 100% rename from lib/tags/router.tag rename to src/tags/router.tag From 489e02ade0b84efaa951fb44cd877ca3959e8cf6 Mon Sep 17 00:00:00 2001 From: Tsutomu Kawamura Date: Sat, 24 Dec 2016 00:50:48 +0900 Subject: [PATCH 5/8] dist/cjs.route.js --> index.js --- dist/cjs.route.js => index.js | 0 package.json | 2 +- rollup.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename dist/cjs.route.js => index.js (100%) diff --git a/dist/cjs.route.js b/index.js similarity index 100% rename from dist/cjs.route.js rename to index.js diff --git a/package.json b/package.json index 15c45b3..bb56b00 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "riot-route", "version": "3.0.2", "description": "Simple isomorphic router", - "main": "dist/cjs.route.js", + "main": "index.js", "jsnext:main": "lib/index.js", "module": "lib/index.js", "modules.root": "lib", diff --git a/rollup.js b/rollup.js index 019c07c..37d2112 100644 --- a/rollup.js +++ b/rollup.js @@ -28,7 +28,7 @@ rollup }) .then(bundle => { bundle.write({ format: 'es', dest: 'lib/index.js' }) - bundle.write({ format: 'cjs', dest: 'dist/cjs.route.js' }) + bundle.write({ format: 'cjs', dest: 'index.js' }) }) .catch(error => { console.error(error) From ee28f8b14884c6a0f36a91b596df3388cf0ac7ba Mon Sep 17 00:00:00 2001 From: Tsutomu Kawamura Date: Sat, 24 Dec 2016 00:53:13 +0900 Subject: [PATCH 6/8] dist/cjs.route.js --> index.js --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb9b6cd..a4ff957 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ edition | target | file | via :-- | :-- | :-- | :-- **Standalone** | ` +``` + +Or for ES6: + +```javascript +import route from 'riot-route/lib/tag' // note that the path is bit different to cjs one +``` + +Or for CommonJS: + +```javascript +const route = require('riot-route/tag') +``` + +### Available tags + +- `` + - it can contains multiple routes + - equivalent to `const r = route.create()` so it creates a sub router +- `` + - it has `path` attribute + - `` is equivalent to `r('fruit/apple', () => { ... })` + - when the route has selected, it triggers **route** event on its children and passes some arguments to them (see details below) + +### Capturing wildcard arguments + +Remember that we could use wildcards `*` in routing. Of cause we can also do the same in *tag-based routing*: + +```html + + +

Apple

+ +
+
+ + +

{ name } is not found

+ +
+``` + +See the example above. If it gets `fruit/pineapple`, the `route` event will fire in `` and pass one argument `'pineapple'`. + +### Real world example + +Usually we would call external API to get some data during routing process. It's handy to hook `route` event for such a purpose. For example: + +```html + + + + + + + +

{ message }!

+ +
+``` + +### Some notes + +- The router automatically starts after first `` tag has been mounted. You don't have to call `router.start(true)` by yourself. +- to change `base` for routing, use `route.base('/path/to/base/')`