decojs
Version:
Scalable frontend architecture
1,504 lines (1,254 loc) • 41.6 kB
JavaScript
define('deco/utils',[], function(){
return {
toArray: function(obj){
var array = [];
// iterate backwards ensuring that length is an UInt32
for (var i = obj.length >>> 0; i--;) {
array[i] = obj[i];
}
return array;
},
extend: function(dst, src){
src = src || {};
dst = dst || {};
for(var i in src){
dst[i] = src[i];
}
return dst;
},
inheritsFrom: function(o){
function F() {}
F.prototype = o.prototype;
return new F();
},
arrayToObject: function(array, func){
return array.reduce(function(collection, item){
func(item, collection);
return collection;
}, {});
},
trim: function(word, character){
for(var f=0; f<word.length; f++){
if(word.charAt(f) !== character) break;
}
for(var t=word.length; t>0; t--){
if(word.charAt(t-1) !== character) break;
}
return word.substring(f, t);
},
after: function(word, character){
var index = word.indexOf(character);
if(index < 0){
return "";
}else{
return word.substr(index+1);
}
},
popTail: function(array){
return array.slice(0, -1);
},
startsWith: function(word, character){
return word.charAt(0) === character;
},
endsWith: function(word, character){
return word.charAt(word.length - 1) === character;
},
addEventListener: function(element, event, listener, bubble){
if('addEventListener' in element){
element.addEventListener(event, listener, bubble);
}else{
element.attachEvent("on"+event, listener);
}
}
};
});
define('deco/qvc/ExecutableResult',["deco/utils"], function(utils){
function ExecutableResult(result){
this.success = false;
this.valid = false;
this.result = null;
this.exception = null;
this.violations = [];
utils.extend(this, result);
};
return ExecutableResult;
});
define('deco/qvc/Constraint',[], function(){
function Constraint(type, attributes){
this.type = type;
this.attributes = attributes;
this.message = attributes.message;
this.init(type);
}
Constraint.prototype.init = function(type){
require(["deco/qvc/constraints/" + type], function(Tester){
var tester = new Tester(this.attributes);
this.validate = tester.isValid.bind(tester);
}.bind(this));
};
Constraint.prototype.validate = function(value){
return true;//real test not loaded yet
};
return Constraint;
});
define('deco/qvc/Validator',[
"deco/qvc/Constraint",
"knockout"
], function(
Constraint,
ko
){
function interpolate(message, attributes, value, name, path){
return message.replace(/\{([^}]+)\}/g, function(match, key){
if(key == "value") return value;
if(key == "this.name") return name;
if(key == "this.path") return path;
if(key in attributes) return attributes[key];
return match;
});
}
function Validator(target, options){
var self = this;
this.constraints = [];
this.isValid = ko.observable(true);
this.message = ko.observable("");
this.name = options && options.name;
this.path = options && options.path;
this.executableName = options && options.executableName;
if(target && ko.isObservable(target)){
target.isValid = function(){return self.isValid();};
}
}
Validator.prototype.setConstraints = function(constraints){
this.constraints = constraints.map(function(constraint){
return new Constraint(constraint.name, constraint.attributes);
});
};
Validator.prototype.reset = function(){
this.isValid(true);
this.message("");
};
Validator.prototype.validate = function(value){
if(this.constraints.length == 0){
this.reset();
}else if(this.constraints.every(function (constraint) {
if(constraint.validate(value)){
return true;
}else{
this.isValid(false);
this.message(interpolate(constraint.message, constraint.attributes, value, this.name, this.path));
return false;
}
}.bind(this))){
this.isValid(true);
this.message("");
}
};
return Validator;
});
define('deco/qvc/koExtensions',["deco/qvc/Validator", "knockout"], function(Validator, ko){
if (ko != null) {
ko.bindingHandlers.validationMessageFor = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor();
var validator = value.validator;
if (validator) {
ko.applyBindingsToNode(element, { hidden: validator.isValid, text: validator.message }, validator);
}else{
var attributes = Array.prototype.reduce.call(element.attributes, function(s,e){return s+" "+e.localName+"=\""+e.value+"\""}, "");
throw new Error("Could not bind `validationMessageFor` to value on element <"+element.tagName.toLowerCase() + attributes +">");
}
}
};
ko.bindingHandlers.command = ko.bindingHandlers.query = {
init: function (element, valueAccessor, allBindingAccessor, viewModel) {
ko.applyBindingsToNode(element, { click: valueAccessor() }, viewModel);
}
};
}
});
define('deco/qvc/Validatable',[
"deco/utils",
"deco/qvc/Validator",
"knockout",
"deco/qvc/koExtensions"
], function(
utils,
Validator,
ko
){
function Validatable(name, parameters, constraintResolver){
var self = this;
this.validator = new Validator();
this.validatableFields = [];
this.validatableParameters = parameters;
init: {
recursivlyExtendParameters(self.validatableParameters, self.validatableFields, [], name);
if(constraintResolver)
constraintResolver.applyValidationConstraints(name, self);
}
}
Validatable.prototype.isValid = function () {
return this.validatableFields.every(function(constraint){
return constraint.validator && constraint.validator.isValid();
}) && this.validator.isValid();
};
Validatable.prototype.applyViolations = function(violations){
violations.forEach(function(violation){
var message = violation.message;
var fieldName = violation.fieldName;
if (fieldName && fieldName.length > 0) {
//one of the fields violates a constraint
applyViolationMessageToField(this.validatableParameters, fieldName, message);
} else {
//the validatable violates a constraint
applyViolationMessageToValidatable(this, message);
}
}.bind(this));
};
Validatable.prototype.applyConstraints = function(fields){
var parameters = this.validatableParameters;
fields.forEach(function(field){
var fieldName = field.name;
var constraints = field.constraints;
if(constraints == null || constraints.length == 0)
return;
var object = findField(fieldName, parameters, "Error applying constraints to field");
if (ko.isObservable(object) && "validator" in object) {
object.validator.setConstraints(constraints);
} else {
throw new Error("Error applying constraints to field: " + fieldName + "\n" +
"It is not an observable or is not extended with a validator. \n" +
fieldName + "=`" + ko.toJSON(object) + "`");
}
});
};
Validatable.prototype.validate = function(){
this.validator.validate(true);
if (this.validator.isValid()) {
this.validatableFields.forEach(function(constraint){
var validator = constraint.validator;
if (validator) {
validator.validate(constraint());
}
});
}
};
Validatable.prototype.clearValidationMessages = function () {
this.validator.reset();
this.validatableFields.forEach(function(constraint){
var validator = constraint.validator;
if (validator) {
validator.reset();
}
});
};
function recursivlyExtendParameters(parameters, validatableFields, parents, executableName) {
for (var key in parameters) {
var property = parameters[key];
var path = parents.concat([key]);
if (ko.isObservable(property)) {
validatableFields.push(applyValidatorTo(property, key, path, executableName));
}
property = ko.utils.unwrapObservable(property);
if (typeof property === "object") {
recursivlyExtendParameters(property, validatableFields, path, executableName);
}
}
}
function findField(fieldPath, parameters, errorMessage){
return fieldPath.split(".").reduce(function(object, name){
var path = object.path;
var field = ko.utils.unwrapObservable(object.field);
if (name in field) {
return {
field: field[name],
path: path + "." + name
};
} else {
throw new Error(errorMessage + ": " + fieldPath + "\n" +
name + " is not a member of " + path + "\n" +
path + " = `" + ko.toJSON(field) + "`");
}
}, {
field: parameters,
path: "parameters"
}).field;
}
function applyViolationMessageToField(parameters, fieldPath, message) {
var object = findField(fieldPath, parameters, "Error applying violation");
if (typeof message === "string" && "validator" in object) {
object.validator.isValid(false);
object.validator.message(message);
}else{
throw new Error("Error applying violation\n"+fieldPath+" is not validatable\nit should be an observable");
}
};
function applyViolationMessageToValidatable(validatable, message) {
validatable.validator.isValid(false);
var oldMessage = validatable.validator.message();
var newMessage = oldMessage.length == 0 ? message : oldMessage + ", " + message;
validatable.validator.message(newMessage);
};
function applyValidatorTo(property, key, path, executableName){
if('validator' in property && property.validator instanceof Validator){
throw new Error("Observable `"+path+"` is parameter `"+property.validator.path+"` in "+property.validator.executableName+" and therefore cannot be a parameter in "+executableName+"!");
}
property.validator = new Validator(property, {
name: key,
path: path.join("."),
executableName: executableName
});
property.subscribe(function (newValue) {
property.validator.validate(newValue);
});
return property;
}
return Validatable;
});
define('deco/qvc/Executable',[
"deco/qvc/ExecutableResult",
"deco/qvc/Validatable",
"deco/utils",
"knockout"
], function(
ExecutableResult,
Validatable,
utils,
ko){
function Executable(name, type, parameters, hooks, qvc){
Validatable.call(this, name, parameters, qvc.constraintResolver)
this.name = name;
this.type = type;
this.qvc = qvc;
this.isBusy = ko.observable(false);
this.hasError = ko.observable(false);
this.result = new ExecutableResult();
this.parameters = Object.seal(parameters);
this.hooks = utils.extend({
beforeExecute: function () {},
canExecute: function(){return true;},
error: function () {},
success: function () {},
result: function(){},
complete: function () {},
invalid: function() {}
}, hooks);
}
Executable.prototype = utils.inheritsFrom(Validatable);
Executable.prototype.execute = function () {
if (this.isBusy()) {
return false;
}
this.hasError(false);
this.hooks.beforeExecute();
this.validate();
if (!this.isValid()) {
this.hooks.invalid();
return false;
}
if (this.hooks.canExecute() === false) {
return false;
}
this.isBusy(true);
this.qvc.execute(this);
return false;
};
Executable.prototype.onError = function () {
if("violations" in this.result && this.result.violations != null && this.result.violations.length > 0){
this.applyViolations(this.result.violations);
this.hooks.invalid();
}else{
this.hasError(true);
this.hooks.error(this.result);
}
};
Executable.prototype.onSuccess = function () {
this.hasError(false);
this.clearValidationMessages();
this.hooks.success(this.result);
this.hooks.result(this.result.result);
};
Executable.prototype.onComplete = function () {
this.hooks.complete();
this.isBusy(false);
};
Executable.Command = "command";
Executable.Query = "query";
return Executable;
});
define('deco/ajax',[], function(){
function dataToParams(data){
var params = []
for(var key in data){
var value = data[key];
params.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
}
return params.join("&");
}
function addParamToUrl(url, name, value){
return url + (url.match(/\?/) ? (url.match(/&$/) ? "" : "&") : "?") + encodeURIComponent(name) + "=" + encodeURIComponent(value);
}
function addParamsToUrl(url, data){
var params = dataToParams(data);
return url + (url.match(/\?/) ? (url.match(/&$/) ? "" : (params.length > 0 ? "&" : "")) : "?") + params;
}
function addToPath(url, segment){
return url + (url.match(/\/$/) ? "" : "/") + segment;
}
function cacheBust(url){
return addParamToUrl(url, "cacheKey", Math.floor(Math.random()*Math.pow(2,53)));
}
function ajax(url, object, method, callback){
var xhr = new XMLHttpRequest();
var isPost = (method === "POST");
var data = null;
if(object){
if(isPost){
data = dataToParams(object);
} else {
url = addParamsToUrl(url, object);
}
}
if(isPost){
url = cacheBust(url);
}
xhr.open(method, url, true);
if(isPost && data){
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
}
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
callback(xhr);
}
}
xhr.send(data);
return xhr;
}
ajax.addParamToUrl = addParamToUrl;
ajax.addParamsToUrl = addParamsToUrl;
ajax.addToPath = addToPath;
ajax.cacheBust = cacheBust;
return ajax;
});
define('deco/qvc/ConstraintResolver',[], function(){
var STATE_LOADING = 'loading';
var STATE_LOADED = 'loaded';
function findConstraint(name, constraints) {
for (var i = 0; i < constraints.length; i++) {
if (constraints[i].name == name) {
return constraints[i];
}
}
return false;
}
function constraintsLoaded(name, fields){
var constraint = findConstraint(name, this.constraints);
if(constraint){
constraint.validatables.forEach(function(validatable){
validatable.applyConstraints(fields);
});
constraint.fields = fields;
constraint.state = STATE_LOADED;
constraint.validatables = [];
}
}
function ConstraintResolver(qvc){
this.qvc = qvc;
this.constraints = [];
}
ConstraintResolver.prototype.applyValidationConstraints = function(name, validatable){
var constraint = findConstraint(name, this.constraints);
if(constraint === false){
this.constraints.push({
name: name,
state: STATE_LOADING,
validatables: [validatable]
});
this.qvc.loadConstraints(name, constraintsLoaded.bind(this));
}else{
if(constraint.state === STATE_LOADING){
constraint.validatables.push(validatable);
}else{
validatable.applyConstraints(constraint.fields);
}
}
};
return ConstraintResolver;
});
define('deco/errorHandler',[], function(){
return {
onError: function(error){
console.error(error && error.stack || error);
}
};
});
define('deco/qvc',[
"deco/qvc/Executable",
"deco/qvc/ExecutableResult",
"deco/utils",
"deco/ajax",
"deco/qvc/ConstraintResolver",
"deco/errorHandler",
"knockout",
"deco/qvc/koExtensions"],
function(
Executable,
ExecutableResult,
utils,
ajax,
ConstraintResolver,
errorHandler,
ko){
function QVC(){
var qvc = this;
this.constraintResolver = new ConstraintResolver(qvc);
this.execute = function(executable){
var parameters = ko.toJS(executable.parameters);
var data = {
parameters: JSON.stringify(parameters),
csrfToken: qvc.config.csrf
};
var url = ajax.addToPath(qvc.config.baseUrl, executable.type + "/" + executable.name);
ajax(url, data, "POST", function (xhr) {
if (xhr.status === 200) {
executable.result = new ExecutableResult(JSON.parse(xhr.responseText || "{}"));
if (executable.result.success === true) {
executable.onSuccess();
} else {
if(executable.result.exception && executable.result.exception.message){
errorHandler.onError(executable.result.exception.message);
}
executable.onError();
}
} else {
executable.result = new ExecutableResult({exception: {message: xhr.responseText, cause: xhr}});
errorHandler.onError(executable.result.exception.message);
executable.onError();
}
executable.onComplete();
});
};
this.loadConstraints = function(name, callback){
var url = ajax.addToPath(qvc.config.baseUrl, "constraints/" + name);
ajax(ajax.addParamToUrl(url, 'cacheKey', qvc.config.cacheKey), null, "GET", function(xhr){
if (xhr.status === 200) {
try{
var response = JSON.parse(xhr.responseText || "{\"parameters\":[]}");
if("parameters" in response == false){
response.parameters = [];
}
if(response.exception && response.exception.message){
errorHandler.onError(response.exception.message);
}
}catch(e){
var response = {parameters: []};
}
callback(name, response.parameters);
}else{
errorHandler.onError(xhr.responseText);
}
});
};
this.config = {
baseUrl: "/qvc",
csrf: "",
cachekey: Date.now()
}
};
var qvc = new QVC();
function createExecutable(name, type, parameters, callbacks){
var executable = new Executable(name, type, parameters || {}, callbacks || {}, qvc);
var execute = executable.execute.bind(executable);
execute.isValid = ko.computed(executable.isValid, executable);
execute.isBusy = ko.computed(executable.isBusy, executable);
execute.hasError = ko.computed(executable.hasError, executable);
execute.success = function(callback){
executable.hooks.success = callback;
return execute;
};
execute.error = function(callback){
executable.hooks.error = callback;
return execute;
};
execute.invalid = function(callback){
executable.hooks.invalid = callback;
return execute;
};
execute.beforeExecute = function(callback){
executable.hooks.beforeExecute = callback;
return execute;
};
execute.canExecute = function(callback){
executable.hooks.canExecute = callback;
return execute;
};
execute.result = function(){
if(arguments.length == 1){
executable.hooks.result = arguments[0];
return execute;
}
return executable.result.result;
};
execute.complete = function(callback){
executable.hooks.complete = callback;
return execute;
};
execute.clearValidationMessages = executable.clearValidationMessages.bind(executable);
execute.validator = executable.validator;
execute.parameters = executable.parameters;
execute.validate = executable.validate.bind(executable);
return execute;
}
return {
createCommand: function(name, parameters, hooks){
if(name == null || name.length == 0)
throw new Error("Command is missing name\nA command must have a name!\nusage: createCommand('name', [parameters, hooks])");
return createExecutable(name, Executable.Command, parameters, hooks);
},
createQuery: function(name, parameters, hooks){
if(name == null || name.length == 0)
throw new Error("Query is missing name\nA query must have a name!\nusage: createQuery('name', [parameters, hooks])");
return createExecutable(name, Executable.Query, parameters, hooks);
},
config: function(config){
utils.extend(qvc.config, config);
}
}
});
define('deco/spa/Outlet',[
"knockout"
], function(
ko
){
function Outlet(element, document){
this.element = element;
this.document = document || window.document;
}
Outlet.prototype.outletExists = function(){
return this.element != null;
};
Outlet.prototype.unloadCurrentPage = function(){
ko.cleanNode(this.element);
this.element.innerHTML = "";
};
Outlet.prototype.setPageContent = function(content){
this.element.innerHTML = content;
};
Outlet.prototype.getPageTitle = function(){
var titleMetaTag = this.element.querySelector("meta[name=title]");
return (titleMetaTag && titleMetaTag.getAttribute("content"));
};
Outlet.prototype.setDocumentTitle = function(title){
this.document.title = title;
};
Outlet.prototype.extractAndRunPageJavaScript = function(){
var scripts = this.element.querySelectorAll("script[type='text/javascript']");
for(var i=0; i<scripts.length; i++){
scripts[i].parentNode.removeChild(scripts[i]);
if(scripts[i].id === '') throw new Error("The script must have an id");
if(this.document.getElementById(scripts[i].id) == null){
var script = this.document.createElement("script");
script.id = scripts[i].id;
script.text = scripts[i].textContent;
script.setAttribute('type', scripts[i].getAttribute('type'));
this.document.body.appendChild(script);
}
}
};
Outlet.prototype.indicatePageIsLoading = function(){
this.element.setAttribute("data-loading", "true");
};
Outlet.prototype.pageHasLoaded = function(){
this.element.setAttribute("data-loading", "false");
};
return Outlet;
});
define('deco/spa/whenContext',[
], function(
){
function unsubscribe(event, reaction){
event.dont(reaction);
}
function subscribe(event, reaction){
event(reaction);
return event.dont.bind(null, reaction);
}
function whenSomething(){
if(this.destroyed) throw new Error("This context has been destroyed!");
if(arguments.length == 0){
var childContext = createContext();
this.childContexts.push(childContext);
return childContext.when;
}else if(arguments.length == 1 && typeof arguments[0] === "function"){
return {
dont: unsubscribe.bind(null, arguments[0])
}
}else if(arguments.length == 2 && typeof arguments[1] === "function"){
this.eventSubscribers.push(subscribe(arguments[0], arguments[1]));
}
}
function thisIsDestroyed(reaction){
this.onDestroyListeners.push(reaction);
}
function destroyChildContexts(){
var context;
while(context = this.childContexts.pop())
context.destroyContext();
}
function destroyContext(){
var subscriber, listener, context;
while(subscriber = this.eventSubscribers.pop())
subscriber();
while(listener = this.onDestroyListeners.pop())
listener();
while(context = this.childContexts.pop())
context.destroyContext();
this.destroyed = true;
}
function createContext(){
var context = {
destroyed: false,
onDestroyListeners: [],
childContexts: [],
eventSubscribers: []
};
var when = whenSomething.bind(context);
when.thisIsDestroyed = thisIsDestroyed.bind(context);
when.destroyChildContexts = destroyChildContexts.bind(context);
return {
when: when,
destroyContext: destroyContext.bind(context)
};
}
return createContext().when;
});
define('deco/spa/viewModelFactory',[], function() {
return {
getViewModelFromAttributes: function(target){
var viewModelName = target.getAttribute("data-viewmodel");
var model = target.getAttribute("data-model");
return {
target: target,
viewModelName: viewModelName,
model: model
};
},
getParentViewModelElement: function(element, maxAncestor){
while(element = element.parentNode){
if(element === maxAncestor) return null;
if(element.hasAttribute("data-viewmodel")) return element;
}
return null;
},
loadViewModel: function(data){
return new Promise(function(resolve, reject){
require([data.viewModelName], resolve, reject);
}).then(function(ViewModel){
return {
viewModelName: data.viewModelName,
ViewModel: ViewModel,
model: data.model,
target: data.target
}
}, function(error){
throw new Error("Could not load the following modules:\n"+error.requireModules.join("\n"));
});
},
createViewModel: function(data, subscribe, params) {
var model = (data.model && (data.model.charAt(0) == '{' || data.model.charAt(0) == '['))
? JSON.parse(data.model)
: params;
var whenContext = subscribe();
var viewModel = new data.ViewModel(model, whenContext);
viewModel['@SymbolDecoWhenContext'] = whenContext;
return {
viewModelName: data.viewModelName,
viewModel: viewModel,
target: data.target
}
}
}
});
define('deco/spa/extendKnockout',[
"deco/spa/viewModelFactory",
"deco/errorHandler",
"knockout"
], function(
viewModelFactory,
errorHandler,
ko
){
var nativeBindingProviderInstance = new ko.bindingProvider();
var originalBindingProvider = ko.bindingProvider.instance;
ko.bindingProvider.instance = {
nodeHasBindings: function(node){
return hasViewModel(node) || originalBindingProvider.nodeHasBindings(node);
},
getBindingAccessors: function(node, context){
if(hasViewModel(node)){
return {
'@SymbolDecoApplyViewModel': function(){ return null; }
};
}else{
return originalBindingProvider.getBindingAccessors(node, context);
}
}
};
ko.bindingHandlers['@SymbolDecoApplyViewModel'] = {
init: function(element, valueAccessor, allBindingsAccessor, deprecated, parentContext){
var parentViewModel = viewModelFactory.getParentViewModelElement(element)['@SymbolDecoViewModel'];
var whenContext = parentViewModel['@SymbolDecoWhenContext']();
try{
var params = getComponentParamsFromCustomElement(element, parentContext);
}catch(e){
console.error(e.stack);
}
Promise.resolve(viewModelFactory.getViewModelFromAttributes(element))
.then(function(data){
return viewModelFactory.loadViewModel(data)
}).then(function(data){
return viewModelFactory.createViewModel(data, whenContext, params);
}).then(function(data){
data.target['@SymbolDecoViewModel'] = data.viewModel;
var childContext = parentContext.createChildContext(data.viewModel);
ko.utils.domData.clear(data.target);
ko.applyBindings(childContext, data.target);
ko.utils.domNodeDisposal.addDisposeCallback(data.target, function() {
delete data.target['@SymbolDecoViewModel'];
whenContext.destroyChildContexts();
});
})['catch'](errorHandler.onError);
return {
controlsDescendantBindings: true
};
}
};
//this is stolen from the knockout sourcecode, and I had to copy it since it's not exposed as a public api
function getComponentParamsFromCustomElement(elem, bindingContext) {
var paramsAttribute = elem.getAttribute('data-params');
if (!paramsAttribute) {
return undefined;
}
var params = nativeBindingProviderInstance['parseBindingsString'](paramsAttribute, bindingContext, elem, { 'valueAccessors': true, 'bindingParams': true });
var rawParamComputedValues = Object.create(null);
for(paramName in params) {
var paramValue = params[paramName];
rawParamComputedValues[paramName] = ko.computed(paramValue, null, { disposeWhenNodeIsRemoved: elem });
}
var result = Object.create(null);
for(paramName in rawParamComputedValues) {
var paramValueComputed = rawParamComputedValues[paramName];
var paramValue = paramValueComputed.peek();
// Does the evaluation of the parameter value unwrap any observables?
if (!paramValueComputed.isActive()) {
// No it doesn't, so there's no need for any computed wrapper. Just pass through the supplied value directly.
// Example: "someVal: firstName, age: 123" (whether or not firstName is an observable/computed)
result[paramName] = paramValue;
} else {
// Yes it does. Supply a computed property that unwraps both the outer (binding expression)
// level of observability, and any inner (resulting model value) level of observability.
// This means the component doesn't have to worry about multiple unwrapping. If the value is a
// writable observable, the computed will also be writable and pass the value on to the observable.
result[paramName] = ko.computed({
'read': function() {
return ko.unwrap(paramValueComputed());
},
'write': ko.isWriteableObservable(paramValue) && function(value) {
paramValueComputed()(value);
},
disposeWhenNodeIsRemoved: elem
});
}
}
return result;
}
function hasViewModel(node){
return node.nodeType === 1 && node.hasAttribute("data-viewmodel") && !('@SymbolDecoViewModel' in node);
}
});
define('deco/spa/applyViewModels',[
"deco/utils",
"deco/errorHandler",
"deco/spa/viewModelFactory",
"knockout",
"deco/spa/extendKnockout"
], function (
utils,
errorHandler,
viewModelFactory,
ko
) {
function applyViewModel(data) {
data.target['@SymbolDecoViewModel'] = data.viewModel;
ko.applyBindings(data.viewModel, data.target);
}
function promisify(t,c){
return function(promise){
return promise.then(t,c);
};
}
return function (domElement, subscribe) {
domElement = domElement || document.body;
var viewModelsLoaded = utils.toArray(domElement.querySelectorAll("[data-viewmodel]"))
.map(viewModelFactory.getViewModelFromAttributes)
.map(viewModelFactory.loadViewModel)
.map(promisify(function(data){
if(viewModelFactory.getParentViewModelElement(data.target, domElement) ? false : true){
return data;
}
}))
.map(promisify(function(data){
if(data){
return viewModelFactory.createViewModel(data, subscribe);
}
}))
.map(promisify(function(data){
if(data){
applyViewModel(data);
}
}));
return Promise.all(viewModelsLoaded)['catch'](errorHandler.onError);
};
});
define('deco/spa/hashNavigation',[
"deco/utils"
],function(
_
){
function findNewPath(currentPath, link, index){
var isRelative = _.startsWith(link, '/') === false;
var isFolder = _.endsWith(link, '/');
if(link === "/"){
var path = [];
}else if(link === ""){
var path = [index];
}else{
var path = _.trim(link, "/").split("/");
}
if(isFolder){
path.push(index);
}
if(isRelative){
path = _.popTail(currentPath).concat(path);
}
var out = [];
var isPretty = true;
var segmentsToSkip = 0;
for(var i = path.length-1; i>=0; i--){
if(path[i] === ""){
isPretty = false;
}else if(path[i] === "."){
isPretty = false;
}else if(path[i] === "..") {
isPretty = false;
segmentsToSkip++;
}else if(segmentsToSkip > 0){
segmentsToSkip--;
}else{
out.unshift(path[i]);
}
}
return {isAbsoluteAndPretty: !isFolder && !isRelative && isPretty, path: out};
}
function hashChanged(config, onPageChanged, document){
var link = _.after(document.location.href, '#');
var isHashBang = _.startsWith(link, '!');
var result = findNewPath(this.currentPath, link.substr(isHashBang ? 1 : 0), config.index);
if(result.isAbsoluteAndPretty && isHashBang){
this.currentPath = result.path;
onPageChanged(result.path.join('/'), result.path.map(decodeURIComponent));
}else{
document.location.replace("#!/" + result.path.join('/'));
}
}
function startHashNavigation(config, onPageChanged, doc, global){
doc = doc || document;
global = global || window;
var state = {
currentPath: []
};
var onHashChanged = hashChanged.bind(state, config, onPageChanged, doc);
onHashChanged();
_.addEventListener(global, "hashchange", onHashChanged, false);
return {
stop: function(){
global.removeEventListener("hashchange", onHashChanged, false);
}
};
}
return {
start: startHashNavigation
};
});
define('deco/spa/PageLoader',[
"deco/ajax"
], function(
ajax
){
function PageLoader(config){
this.pathToUrl = config && config.pathToUrl || function(a){ return a; };
this.cache = (config && 'cachePages' in config ? config.cachePages : true);
this.currentXHR = null;
}
PageLoader.prototype.loadPage = function(path){
this.abort();
var url = this.pathToUrl(path);
if(this.cache === false)
url = ajax.cacheBust(url);
var self = this;
return new Promise(function(resolve, reject){
self.currentXHR = ajax(url, {}, "GET", function(xhr){
self.currentXHR = null;
if(xhr.status === 200){
resolve(xhr.responseText);
}else{
reject({error: xhr.status, content: xhr.responseText});
}
});
});
};
PageLoader.prototype.abort = function(){
if(this.currentXHR && this.currentXHR.readyState !== 4){
this.currentXHR.abort();
}
};
return PageLoader;
});
define('deco/spa/Templates',[
"deco/spa/PageLoader",
"deco/utils"
], function(
PageLoader,
utils
){
function defaultConfig(){
return {
pathToUrl: function(a){ return a; }
}
}
function findTemplatesInDocument(doc){
var nodeList = doc.querySelectorAll("[type='text/page-template']");
var nodes = utils.toArray(nodeList);
var templateList = nodes.map(function(template){
return {
id: template.id.toLowerCase(),
content: template.innerHTML
};
});
return utils.arrayToObject(templateList, function(item, object){
object[item.id] = item.content;
});
}
function Templates(document, config){
this.pageLoader = new PageLoader(config || defaultConfig());
this.cachePages = (config && 'cachePages' in config ? config.cachePages : true)
this.templates = findTemplatesInDocument(document);
}
Templates.prototype.getTemplate = function(path){
this.pageLoader.abort();
var normalizedPath = path.toLowerCase();
if(normalizedPath in this.templates){
return Promise.resolve(this.templates[normalizedPath]);
}else{
return this.pageLoader.loadPage(path)
.then(function(content){
if(this.cachePages)
this.templates[normalizedPath] = content;
return content;
}.bind(this), function(notFound){
var errorTemplate = "error" + notFound.error;
if(errorTemplate in this.templates){
return this.templates[errorTemplate];
}else{
return notFound.content;
}
}.bind(this));
}
};
return Templates;
});
define('deco/proclaimWhen',[], function () {
function publish(name, subscribers, data) {
subscribers.forEach(function (subscriber) {
subscriber.apply(subscriber, data);
});
}
function subscribeTo(name, subscribers, subscriber) {
var index = subscribers.indexOf(subscriber);
if(index < 0)
subscribers.push(subscriber);
return index < 0;
}
function unsubscribeFrom(name, subscribers, subscriber){
var index = subscribers.indexOf(subscriber);
if(index >= 0)
subscribers.splice(index, 1);
return index >= 0;
}
function extendEvent(name, event){
event.subscribers = [];
event.subscribeSubscribers = [];
event.unsubscribeSubscribers = [];
var extendedEvent = function(){
if(arguments.length == 1 && typeof arguments[0] === "function"){
var subscriber = arguments[0];
var wasNew = subscribeTo(name, event.subscribers, subscriber);
if(wasNew)
publish(name+".isSubscribedTo", event.subscribeSubscribers, [function(){subscriber.apply(subscriber, arguments);}]);
}else{
publish(name, event.subscribers, arguments);
}
}
extendedEvent.dont = function(subscriber){
if(unsubscribeFrom(name, event.subscribers, subscriber))
publish(name+".isUnsubscribedFrom", event.unsubscribeSubscribers);
};
extendedEvent.isSubscribedTo = function(subscriber){
subscribeTo(name+".isSubscribedTo", event.subscribeSubscribers, subscriber);
};
extendedEvent.isSubscribedTo.dont = function(subscriber){
unsubscribeFrom(name+".isSubscribedTo", event.subscribeSubscribers, subscriber);
};
extendedEvent.isUnsubscribedFrom = function(subscriber){
subscribeTo(name+".isUnsubscribedFrom", event.unsubscribeSubscribers, subscriber);
};
extendedEvent.isUnsubscribedFrom.dont = function(subscriber){
unsubscribeFrom(name+".isUnsubscribedFrom", event.unsubscribeSubscribers, subscriber);
};
extendedEvent.toString = function(){
return "[Event "+name+"]";
}
return extendedEvent;
}
function extend(events){
for(var i in events){
events[i] = extendEvent(i, events[i]);
}
return events;
}
function create(arg1, arg2){
if(arg2)
return extendEvent(arg1, arg2);
else
return extendEvent("anonymous event", arg1)
}
return {
extend: extend,
create: create
};
});
define('deco/events',['deco/proclaimWhen'], function(proclaimWhen){
return proclaimWhen.extend({
thePageHasChanged: function(path, segments, url){ }
});
});
define('deco/spa',[
"deco/spa/Outlet",
"deco/spa/whenContext",
"deco/spa/applyViewModels",
"deco/spa/hashNavigation",
"deco/spa/Templates",
"deco/utils",
"deco/events",
"deco/errorHandler"
], function(
Outlet,
whenContext,
applyViewModels,
hashNavigation,
Templates,
utils,
proclaim,
errorHandler
){
var _config = {
index: "index"
},
_document,
_outlet,
_originalTitle,
_templates,
_whenContext;
function applyContent(content){
_outlet.unloadCurrentPage();
_outlet.setPageContent(content);
_outlet.setDocumentTitle(_outlet.getPageTitle() || _originalTitle);
_outlet.extractAndRunPageJavaScript();
return applyViewModels(_outlet.element, _whenContext());
}
function pageChanged(path, segments){
_outlet.indicatePageIsLoading();
_whenContext.destroyChildContexts();
return _templates.getTemplate(path)
.then(applyContent)
.then(function(){
_outlet.pageHasLoaded();
proclaim.thePageHasChanged(path, segments, document.location)
})['catch'](errorHandler.onError);;
}
function start(config, document){
_document = document || window.document;
_config = utils.extend(_config, config);
_outlet = new Outlet(_document.querySelector("[data-outlet]"), _document);
_originalTitle = _document.title;
_whenContext = whenContext();
return applyViewModels(_document, whenContext()).then(function(){
if(_outlet.outletExists()){
_templates = new Templates(_document, _config);
hashNavigation.start(_config, pageChanged, _document);
}
});
}
return {
start: start
};
});
define('deco/deco',[
'deco/qvc',
'deco/spa'
], function(
qvc,
spa
){
function config(config){
config = config || {};
var spaConfig = config.spa || {};
var qvcConfig = config.qvc || {};
qvc.config(qvcConfig);
return {
start: spa.start.bind(null, spaConfig, document)
}
}
return {
config: config
};
});
define('deco', ['deco/deco'], function (main) { return main; });
define('deco/qvc/constraints/NotEmpty',[], function(){
function NotEmpty(attributes){
}
NotEmpty.prototype.isValid = function(value){
if(value == null) return false;
if(typeof value == "string" && value.length == 0) return false;
return true;
};
return NotEmpty;
});
define('deco/qvc/constraints/Pattern',[], function(){
function Pattern(attributes){
attributes.flags = attributes.flags || [];
var flags = '';
if(attributes.flags.indexOf("CASE_INSENSITIVE") >= 0) flags += 'i';
this.regex = new RegExp(attributes.regexp, flags);
}
Pattern.prototype.isValid = function(value){
if(value == null) return false;
var result = this.regex.exec(value);
if(result == null) return false;
return result[0] == value;
};
return Pattern;
});
//# sourceMappingURL=deco.js.map