can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
310 lines (309 loc) • 12.6 kB
JavaScript
/*!
* CanJS - 2.3.34
* http://canjs.com/
* Copyright (c) 2018 Bitovi
* Mon, 30 Apr 2018 20:56:51 GMT
* Licensed MIT
*/
/*can@2.3.34#route/route*/
define([
'can/util/library',
'can/map',
'can/list',
'can/util/string/deparam'
], function (can) {
var matcher = /\:([\w\.]+)/g, paramsMatcher = /^(?:&[^=]+=[^&]*)+/, makeProps = function (props) {
var tags = [];
can.each(props, function (val, name) {
tags.push((name === 'className' ? 'class' : name) + '="' + (name === 'href' ? val : can.esc(val)) + '"');
});
return tags.join(' ');
}, matchesData = function (route, data) {
var count = 0, i = 0, defaults = {};
for (var name in route.defaults) {
if (route.defaults[name] === data[name]) {
defaults[name] = 1;
count++;
}
}
for (; i < route.names.length; i++) {
if (!data.hasOwnProperty(route.names[i])) {
return -1;
}
if (!defaults[route.names[i]]) {
count++;
}
}
return count;
}, location = window.location, wrapQuote = function (str) {
return (str + '').replace(/([.?*+\^$\[\]\\(){}|\-])/g, '\\$1');
}, each = can.each, extend = can.extend, definedToString = function (obj) {
return obj.toString.toString() !== Object.prototype.toString.toString();
}, stringify = function (obj) {
if (obj && typeof obj === 'object' && !definedToString(obj)) {
if (obj instanceof can.Map) {
obj = obj;
} else {
obj = can.isFunction(obj.slice) ? obj.slice() : can.extend({}, obj);
}
can.each(obj, function (val, prop) {
obj[prop] = stringify(val);
});
} else if (obj !== undefined && obj !== null && can.isFunction(obj.toString)) {
obj = obj.toString();
}
return obj;
}, removeBackslash = function (str) {
return str.replace(/\\/g, '');
}, timer, curParams, lastHash, changingData, changedAttrs = [], onRouteDataChange = function (ev, attr, how, newval) {
changingData = 1;
changedAttrs.push(attr);
clearTimeout(timer);
timer = setTimeout(function () {
changingData = 0;
var serialized = can.route.data.serialize(), path = can.route.param(serialized, true);
can.route._call('setURL', path, changedAttrs);
can.batch.trigger(eventsObject, '__url', [
path,
lastHash
]);
lastHash = path;
changedAttrs = [];
}, 10);
}, eventsObject = can.extend({}, can.event), stringCoercingMapDecorator = function (map) {
var attrSuper = map.attr;
map.attr = function (prop, val) {
var serializable = this.define === undefined || this.define[prop] === undefined || !!this.define[prop].serialize, args;
if (serializable) {
args = stringify(Array.apply(null, arguments));
} else {
args = arguments;
}
return attrSuper.apply(this, args);
};
return map;
};
can.route = function (url, defaults) {
var root = can.route._call('root');
if (root.lastIndexOf('/') === root.length - 1 && url.indexOf('/') === 0) {
url = url.substr(1);
}
defaults = defaults || {};
var names = [], res, test = '', lastIndex = matcher.lastIndex = 0, next, querySeparator = can.route._call('querySeparator'), matchSlashes = can.route._call('matchSlashes');
while (res = matcher.exec(url)) {
names.push(res[1]);
test += removeBackslash(url.substring(lastIndex, matcher.lastIndex - res[0].length));
next = '\\' + (removeBackslash(url.substr(matcher.lastIndex, 1)) || querySeparator + (matchSlashes ? '' : '|/'));
test += '([^' + next + ']' + (defaults[res[1]] ? '*' : '+') + ')';
lastIndex = matcher.lastIndex;
}
test += url.substr(lastIndex).replace('\\', '');
can.route.routes[url] = {
test: new RegExp('^' + test + '($|' + wrapQuote(querySeparator) + ')'),
route: url,
names: names,
defaults: defaults,
length: url.split('/').length
};
return can.route;
};
extend(can.route, {
param: function (data, _setRoute) {
var route, matches = 0, matchCount, routeName = data.route, propCount = 0;
delete data.route;
each(data, function () {
propCount++;
});
each(can.route.routes, function (temp, name) {
matchCount = matchesData(temp, data);
if (matchCount > matches) {
route = temp;
matches = matchCount;
}
if (matchCount >= propCount) {
return false;
}
});
if (can.route.routes[routeName] && matchesData(can.route.routes[routeName], data) === matches) {
route = can.route.routes[routeName];
}
if (route) {
var cpy = extend({}, data), res = route.route.replace(matcher, function (whole, name) {
delete cpy[name];
return data[name] === route.defaults[name] ? '' : encodeURIComponent(data[name]);
}).replace('\\', ''), after;
each(route.defaults, function (val, name) {
if (cpy[name] === val) {
delete cpy[name];
}
});
after = can.param(cpy);
if (_setRoute) {
can.route.attr('route', route.route);
}
return res + (after ? can.route._call('querySeparator') + after : '');
}
return can.isEmptyObject(data) ? '' : can.route._call('querySeparator') + can.param(data);
},
deparam: function (url) {
var root = can.route._call('root');
if (root.lastIndexOf('/') === root.length - 1 && url.indexOf('/') === 0) {
url = url.substr(1);
}
var route = { length: -1 }, querySeparator = can.route._call('querySeparator'), paramsMatcher = can.route._call('paramsMatcher');
each(can.route.routes, function (temp, name) {
if (temp.test.test(url) && temp.length > route.length) {
route = temp;
}
});
if (route.length > -1) {
var parts = url.match(route.test), start = parts.shift(), remainder = url.substr(start.length - (parts[parts.length - 1] === querySeparator ? 1 : 0)), obj = remainder && paramsMatcher.test(remainder) ? can.deparam(remainder.slice(1)) : {};
obj = extend(true, {}, route.defaults, obj);
each(parts, function (part, i) {
if (part && part !== querySeparator) {
obj[route.names[i]] = decodeURIComponent(part);
}
});
obj.route = route.route;
return obj;
}
if (url.charAt(0) !== querySeparator) {
url = querySeparator + url;
}
return paramsMatcher.test(url) ? can.deparam(url.slice(1)) : {};
},
data: stringCoercingMapDecorator(new can.Map({})),
map: function (data) {
var appState;
if (data.prototype instanceof can.Map) {
appState = new data();
} else {
appState = data;
}
can.route.data = stringCoercingMapDecorator(appState);
},
routes: {},
ready: function (val) {
if (val !== true) {
can.route._setup();
if (can.isBrowserWindow || can.isWebWorker) {
can.route.setState();
}
}
return can.route;
},
url: function (options, merge) {
if (merge) {
can.__observe(eventsObject, '__url');
options = can.extend({}, can.route.deparam(can.route._call('matchingPartOfURL')), options);
}
return can.route._call('root') + can.route.param(options);
},
link: function (name, options, props, merge) {
return '<a ' + makeProps(extend({ href: can.route.url(options, merge) }, props)) + '>' + name + '</a>';
},
current: function (options) {
can.__observe(eventsObject, '__url');
return this._call('matchingPartOfURL') === can.route.param(options);
},
bindings: {
hashchange: {
paramsMatcher: paramsMatcher,
querySeparator: '&',
matchSlashes: false,
bind: function () {
can.bind.call(window, 'hashchange', setState);
},
unbind: function () {
can.unbind.call(window, 'hashchange', setState);
},
matchingPartOfURL: function () {
var loc = can.route.location || location;
return loc.href.split(/#!?/)[1] || '';
},
setURL: function (path) {
if (location.hash !== '#' + path) {
location.hash = '!' + path;
}
return path;
},
root: '#!'
}
},
defaultBinding: 'hashchange',
currentBinding: null,
_setup: function () {
if (!can.route.currentBinding) {
can.route._call('bind');
can.route.bind('change', onRouteDataChange);
can.route.currentBinding = can.route.defaultBinding;
}
},
_teardown: function () {
if (can.route.currentBinding) {
can.route._call('unbind');
can.route.unbind('change', onRouteDataChange);
can.route.currentBinding = null;
}
clearTimeout(timer);
changingData = 0;
},
_call: function () {
var args = can.makeArray(arguments), prop = args.shift(), binding = can.route.bindings[can.route.currentBinding || can.route.defaultBinding], method = binding[prop];
if (method.apply) {
return method.apply(binding, args);
} else {
return method;
}
}
});
each([
'bind',
'unbind',
'on',
'off',
'delegate',
'undelegate',
'removeAttr',
'compute',
'_get',
'___get',
'each'
], function (name) {
can.route[name] = function () {
if (!can.route.data[name]) {
return;
}
return can.route.data[name].apply(can.route.data, arguments);
};
});
can.route.attr = function () {
return can.route.data.attr.apply(can.route.data, arguments);
};
can.route.batch = can.batch;
var setState = can.route.setState = function () {
var hash = can.route._call('matchingPartOfURL');
var oldParams = curParams;
curParams = can.route.deparam(hash);
if (!changingData || hash !== lastHash) {
can.route.batch.start();
recursiveClean(oldParams, curParams, can.route.data);
can.route.attr(curParams);
can.route.batch.trigger(eventsObject, '__url', [
hash,
lastHash
]);
can.route.batch.stop();
}
};
var recursiveClean = function (old, cur, data) {
for (var attr in old) {
if (cur[attr] === undefined) {
data.removeAttr(attr);
} else if (Object.prototype.toString.call(old[attr]) === '[object Object]') {
recursiveClean(old[attr], cur[attr], data.attr(attr));
}
}
};
return can.route;
});