/*global define, module, console, MontageElement, Reflect, customElements */
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define('montage', [], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require, exports, module);
} else {
// Browser globals (root is window)
root.Montage = factory({}, {}, {});
}
}(this, function (require, exports, module) {
"use strict";
// reassigning causes eval to not use lexical scope.
var globalEval = eval,
/*jshint evil:true */
global = globalEval('this');
/*jshint evil:false */
// Here we expose global for legacy mop support.
// TODO move to mr cause it's loader role to expose
// TODO make sure mop closure has it also cause it's mop role to expose
global.global = global;
var browser = {
makeResolve: function () {
try {
var testHost = "http://example.org",
testPath = "/test.html",
resolved = new URL(testPath, testHost).href;
if (!resolved || resolved !== testHost + testPath) {
throw new Error('NotSupported');
}
return function (base, relative) {
return new URL(relative, base).href;
};
} catch (err) {
var IS_ABSOLUTE_REG = /^[\w\-]+:/,
head = document.querySelector("head"),
currentBaseElement = head.querySelector("base"),
baseElement = document.createElement("base"),
relativeElement = document.createElement("a"),
needsRestore = false;
if(currentBaseElement) {
needsRestore = true;
}
else {
currentBaseElement = document.createElement("base");
}
// Optimization, we won't check ogain if there's a base tag.
baseElement.href = "";
return function (base, relative) {
var restore;
if (!needsRestore) {
head.appendChild(currentBaseElement);
}
base = String(base);
if (IS_ABSOLUTE_REG.test(base) === false) {
throw new Error("Can't resolve from a relative location: " + JSON.stringify(base) + " " + JSON.stringify(relative));
}
if(needsRestore) {
restore = currentBaseElement.href;
}
currentBaseElement.href = base;
relativeElement.href = relative;
var resolved = relativeElement.href;
if (needsRestore) {
currentBaseElement.href = restore;
} else {
head.removeChild(currentBaseElement);
}
return resolved;
};
}
},
load: function (location,loadCallback) {
var script = document.createElement("script");
script.src = location;
script.onload = function () {
if(loadCallback) {
loadCallback(script);
}
// remove clutter
script.parentNode.removeChild(script);
};
document.getElementsByTagName("head")[0].appendChild(script);
},
getParams: function () {
var i, j,
match,
script,
montage,
attr,
name;
if (!this._params) {
this._params = {};
// Find the <script> that loads us, so we can divine our
// parameters from its attributes.
var scripts = document.getElementsByTagName("script");
for (i = 0; i < scripts.length; i++) {
script = scripts[i];
montage = false;
if (script.src && (match = script.src.match(/^(.*)montage.js(?:[\?\.]|$)/i))) {
this._params.montageLocation = match[1];
montage = true;
}
if (script.hasAttribute("data-montage-location")) {
this._params.montageLocation = script.getAttribute("data-montage-location");
montage = true;
}
if (montage) {
if (script.dataset) {
for (name in script.dataset) {
if (script.dataset.hasOwnProperty(name)) {
this._params[name] = script.dataset[name];
}
}
} else if (script.attributes) {
var dataRe = /^data-(.*)$/, // TODO cache RegEx
letterAfterDash = /-([a-z])/g,
upperCaseChar = function (_, c) {
return c.toUpperCase();
};
for (j = 0; j < script.attributes.length; j++) {
attr = script.attributes[j];
match = attr.name.match(dataRe);
if (match) {
this._params[match[1].replace(letterAfterDash, upperCaseChar)] = attr.value;
}
}
}
// Permits multiple montage.js <scripts>; by
// removing as they are discovered, next one
// finds itself.
script.parentNode.removeChild(script);
break;
}
}
}
return this._params;
},
bootstrap: function (callback) {
var Require, DOM, Promise, URL;
var params = this.getParams();
var resolve = this.makeResolve();
// observe dom loading and load scripts in parallel
function callbackIfReady() {
if (DOM && Require) {
callback(Require, Promise, URL);
}
}
// observe dom loaded
function domLoad() {
document.removeEventListener("DOMContentLoaded", domLoad, true);
DOM = true;
// Give a threshold before we decide we need to show the bootstrapper progress
// Applications that use our loader will interact with this timeout
// and class name to coordinate a nice loading experience. Applications that do not will
// just go about business as usual and draw their content as soon as possible.
var root = document.documentElement;
if(!!root.classList) {
root.classList.add("montage-app-bootstrapping");
} else {
root.className = root.className + " montage-app-bootstrapping";
}
document._montageTiming = document._montageTiming || {};
document._montageTiming.bootstrappingStartTime = Date.now();
callbackIfReady();
}
// this permits montage.js to be injected after DOMContentLoaded
// http://jsperf.com/readystate-boolean-vs-regex/2
if (/interactive|complete/.test(document.readyState)) {
domLoad();
} else {
document.addEventListener("DOMContentLoaded", domLoad, true);
}
// determine which scripts to load
var pending = {
"require": "node_modules/mr/require.js",
"require/browser": "node_modules/mr/browser.js",
"promise": "node_modules/bluebird/js/browser/bluebird.min.js"
// "shim-string": "core/shim/string.js" // needed for the `endsWith` function.
};
// miniature module system
var definitions = {};
var bootModules = {};
function bootRequire(id) {
if (!bootModules[id] && definitions[id]) {
var exports = bootModules[id] = {};
bootModules[id] = definitions[id](bootRequire, exports) || exports;
}
return bootModules[id];
}
// execute bootstrap scripts
function allModulesLoaded() {
URL = bootRequire("mini-url");
Promise = bootRequire("promise");
Require = bootRequire("require");
// if we get past the for loop, bootstrapping is complete. get rid
// of the bootstrap function and proceed.
delete global.bootstrap;
callbackIfReady();
}
// register module definitions for deferred,
// serial execution
global.bootstrap = function (id, factory) {
definitions[id] = factory;
delete pending[id];
for (var module in pending) {
if (pending.hasOwnProperty(module)) {
// this causes the function to exit if there are any remaining
// scripts loading, on the first iteration. consider it
// equivalent to an array length check
return;
}
}
allModulesLoaded();
};
// load in parallel, but only if we're not using a preloaded cache.
// otherwise, these scripts will be inlined after already
if (typeof global.BUNDLE === "undefined") {
var montageLocation = resolve(global.location, params.montageLocation);
//Special Case bluebird for now:
browser.load(resolve(montageLocation, pending.promise), function() {
delete pending.promise;
//global.bootstrap cleans itself from global once all known are loaded. "bluebird" is not known, so needs to do it first
global.bootstrap("bluebird", function (require, exports) {
return global.Promise;
});
global.bootstrap("promise", function (require, exports) {
return global.Promise;
});
for (var module in pending) {
if (pending.hasOwnProperty(module)) {
browser.load(resolve(montageLocation, pending[module]));
}
}
});
} else {
global.nativePromise = global.Promise;
Object.defineProperty(global, "Promise", {
configurable: true,
set: function(PromiseValue) {
Object.defineProperty(global, "Promise", {
value: PromiseValue
});
global.bootstrap("bluebird", function (require, exports) {
return global.Promise;
});
global.bootstrap("promise", function (require, exports) {
return global.Promise;
});
}
});
}
// global.bootstrap("shim-string");
// one module loaded for free, for use in require.js, browser.js
global.bootstrap("mini-url", function (require, exports) {
exports.resolve = resolve;
});
},
initMontage: function (montageRequire, applicationRequire, params) {
//exports.Require.delegate = this;
var dependencies = [
"core/core",
"core/event/event-manager",
"core/serialization/deserializer/montage-reviver",
"core/logger"
];
var Promise = montageRequire("core/promise").Promise;
var deepLoadPromises = [];
var self = this;
for (var i = 0, iDependency; (iDependency = dependencies[i]); i++) {
deepLoadPromises.push(montageRequire.deepLoad(iDependency));
}
return Promise.all(deepLoadPromises).then(function () {
for (var i = 0, iDependency; (iDependency = dependencies[i]); i++) {
montageRequire(iDependency);
}
var Montage = montageRequire("core/core").Montage;
var EventManager = montageRequire("core/event/event-manager").EventManager;
var defaultEventManager = montageRequire("core/event/event-manager").defaultEventManager;
var MontageReviver = montageRequire("core/serialization/deserializer/montage-reviver").MontageReviver;
var logger = montageRequire("core/logger").logger;
var application;
// montageWillLoad is mostly for testing purposes
if (typeof global.montageWillLoad === "function") {
global.montageWillLoad();
}
// Load the application
var appProto = applicationRequire.packageDescription.applicationPrototype,
applicationLocation, appModulePromise;
if (appProto) {
applicationLocation = MontageReviver.parseObjectLocationId(appProto);
appModulePromise = applicationRequire.async(applicationLocation.moduleId);
} else {
appModulePromise = montageRequire.async("core/application");
}
return appModulePromise.then(function (exports) {
var Application = exports[(applicationLocation ? applicationLocation.objectName : "Application")];
application = new Application();
defaultEventManager.application = application;
application.eventManager = defaultEventManager;
return application._load(applicationRequire, function () {
if (params.module) {
// If a module was specified in the config then we initialize it now
applicationRequire.async(params.module);
}
if (typeof global.montageDidLoad === "function") {
global.montageDidLoad();
}
if (window.MontageElement) {
MontageElement.ready(applicationRequire, application, MontageReviver);
}
});
});
});
}
};
exports.initMontageCustomElement = function () {
if (typeof window.customElements === 'undefined' || typeof window.Reflect === 'undefined') {
return void 0;
}
function makeCustomElementConstructor(superConstructor) {
var constructor = function () {
return Reflect.construct(
HTMLElement, [], constructor
);
};
Object.setPrototypeOf(
constructor.prototype, (superConstructor || HTMLElement).prototype
);
Object.setPrototypeOf(constructor, superConstructor || HTMLElement);
return constructor;
}
var MontageElement = makeCustomElementConstructor();
function defineMontageElement(name, options) {
if (!customElements.get(name)) {
var customElementConstructor = makeCustomElementConstructor(MontageElement);
customElementConstructor.componentConstructor = options.constructor;
customElementConstructor.observedAttributes = options.observedAttributes;
customElements.define(name, customElementConstructor);
}
}
MontageElement.pendingCustomElements = new Map();
MontageElement.define = function (name, constructor, options) {
if (options && typeof options === 'object') {
options.constructor = constructor;
} else {
options = { constructor: constructor };
}
if (this.require) {
defineMontageElement(name, options);
} else {
this.pendingCustomElements.set(name, options);
}
};
MontageElement.ready = function (require, application, reviver) {
MontageElement.prototype.findProxyForElement = reviver.findProxyForElement;
this.application = application;
this.require = require;
this.pendingCustomElements.forEach(function (constructor, name) {
defineMontageElement(name, constructor);
});
this.pendingCustomElements.clear();
};
Object.defineProperties(MontageElement.prototype, {
require: {
get: function () {
return MontageElement.require;
},
configurable: false
},
application: {
get: function () {
return MontageElement.application;
},
configurable: false
},
componentConstructor: {
get: function () {
return this.constructor.componentConstructor;
},
configurable: false
},
observedAttributes: {
get: function () {
return this.constructor.observedAttributes;
},
configurable: false
}
});
MontageElement.prototype.connectedCallback = function () {
if (!this._instance) {
var self = this,
component = this.instantiateComponent();
return this.findParentComponent().then(function (parentComponent) {
self._instance = component;
parentComponent.addChildComponent(component);
component._canDrawOutsideDocument = true;
component.needsDraw = true;
});
}
};
MontageElement.prototype.disconnectedCallback = function () {
//TODO
};
MontageElement.prototype.findParentComponent = function () {
var eventManager = this.application.eventManager,
anElement = this,
parentComponent,
aParentNode,
candidate;
while ((aParentNode = anElement.parentNode) !== null &&
!(candidate = eventManager.eventHandlerForElement(aParentNode))) {
anElement = aParentNode;
}
return Promise.resolve(candidate) || this.getRootComponent();
};
MontageElement.prototype.getRootComponent = function () {
if (!MontageElement.rootComponentPromise) {
MontageElement.rootComponentPromise = this.require.async("montage/ui/component")
.then(function (exports) {
return exports.__root__;
});
}
return MontageElement.rootComponentPromise;
};
MontageElement.prototype.instantiateComponent = function () {
var component = new this.componentConstructor();
this.bootstrapComponent(component);
component.element = document.createElement("div");
return component;
};
MontageElement.prototype.bootstrapComponent = function (component) {
var shadowRoot = this.attachShadow({ mode: 'open' }),
mainEnterDocument = component.enterDocument,
mainTemplateDidLoad = component.templateDidLoad,
proxyElement = this.findProxyForElement(this);
if (proxyElement) {
var observedAttributes = this.observedAttributes,
observedAttribute,
self = this,
length;
if (observedAttributes && (length = observedAttributes.length)) {
for (var i = 0; i < length; i++) {
observedAttribute = observedAttributes[i];
component.defineBinding(observedAttribute, {
"<->": "" + observedAttribute, source: proxyElement
});
}
}
}
this.application.eventManager.registerTargetForActivation(shadowRoot);
component.templateDidLoad = function () {
var resources = component.getResources();
if (resources) {
self.injectResourcesWithinCustomElement(
resources.styles,
shadowRoot
);
self.injectResourcesWithinCustomElement(
resources.scripts,
shadowRoot
);
}
this.templateDidLoad = mainTemplateDidLoad;
if (typeof this.templateDidLoad === "function") {
this.templateDidLoad();
}
};
component.enterDocument = function (firstTime) {
shadowRoot.appendChild(this.element);
this.enterDocument = mainEnterDocument;
if (typeof this.enterDocument === "function") {
this.enterDocument(firstTime);
}
};
};
MontageElement.prototype.injectResourcesWithinCustomElement = function (resources, shadowRoot) {
if (resources && resources.length) {
for (var i = 0, length = resources.length; i < length; i++) {
shadowRoot.appendChild(resources[i]);
}
}
};
global.MontageElement = MontageElement;
};
/**
* Initializes Montage and creates the application singleton if
* necessary.
*/
exports.initMontage = function () {
var platform = exports.getPlatform();
// Platform dependent
platform.bootstrap(function (Require, Promise, URL) {
var params = platform.getParams();
var config = {
// This takes <base> into account
location: Require.getLocation()
};
exports.Require = Require;
var montageLocation = URL.resolve(config.location, params.montageLocation);
var location = URL.resolve(config.location, params.package || ".");
var applicationHash = params.applicationHash;
if (typeof global.BUNDLE === "object") {
var bundleDefinitions = {};
var getDefinition = function (name) {
if(!bundleDefinitions[name]) {
var defer = bundleDefinitions[name] = {};
var deferPromise = new Promise(function(resolve, reject) {
defer.resolve = resolve;
defer.reject = reject;
});
defer.promise = deferPromise;
return defer;
}
return bundleDefinitions[name];
};
global.bundleLoaded = function (name) {
getDefinition(name).resolve();
};
var preloading = {};
var preloadingPromise = new Promise(function(resolve, reject) {
preloading.resolve = resolve;
preloading.reject = reject;
});
preloading.promise = preloadingPromise;
config.preloaded = preloading.promise;
// preload bundles sequentially
var preloaded = Promise.resolve();
global.BUNDLE.forEach(function (bundleLocations) {
preloaded = preloaded.then(function () {
return Promise.all(bundleLocations.map(function (bundleLocation) {
browser.load(bundleLocation);
return getDefinition(bundleLocation).promise;
}));
});
});
// then release the module loader to run normally
preloading.resolve(preloaded.then(function () {
delete global.BUNDLE;
delete global.bundleLoaded;
}));
}
var applicationRequirePromise;
if (!("remoteTrigger" in params)) {
if ("autoPackage" in params) {
Require.injectPackageDescription(location, {
dependencies: {
montage: "*"
}
}, config);
} else {
// handle explicit package.json location
if (location.slice(location.length - 5) === ".json") {
var packageDescriptionLocation = location;
location = URL.resolve(location, ".");
Require.injectPackageDescriptionLocation(
location,
packageDescriptionLocation,
config
);
}
}
applicationRequirePromise = Require.loadPackage({
location: location,
hash: applicationHash
}, config);
} else {
// allows the bootstrapping to be remote controlled by the
// parent window, with a dynamically generated package
// description
window.postMessage({
type: "montageReady"
}, "*");
var trigger = new Promise(function(resolve) {
var messageCallback = function (event) {
if (
params.remoteTrigger === event.origin &&
(event.source === window || event.source === window.parent)
) {
switch (event.data.type) {
case "montageInit":
window.removeEventListener("message", messageCallback);
resolve([event.data.location, event.data.injections]);
break;
case "isMontageReady":
// allow the injector to query the state in case
// they missed the first message
window.postMessage({
type: "montageReady"
}, "*");
}
}
};
window.addEventListener("message", messageCallback);
});
applicationRequirePromise = trigger.spread(function (location, injections) {
var promise = Require.loadPackage({
location: location,
hash: applicationHash
}, config);
if (injections) {
promise = promise.then(function (applicationRequire) {
location = URL.resolve(location, ".");
var packageDescriptions = injections.packageDescriptions,
packageDescriptionLocations = injections.packageDescriptionLocations,
mappings = injections.mappings,
dependencies = injections.dependencies,
index, injectionsLength;
if (packageDescriptions) {
injectionsLength = packageDescriptions.length;
for (index = 0; index < injectionsLength; index++) {
applicationRequire.injectPackageDescription(
packageDescriptions[index].location,
packageDescriptions[index].description);
}
}
if (packageDescriptionLocations) {
injectionsLength = packageDescriptionLocations.length;
for (index = 0; index < injectionsLength; index++) {
applicationRequire.injectPackageDescriptionLocation(
packageDescriptionLocations[index].location,
packageDescriptionLocations[index].descriptionLocation);
}
}
if (mappings) {
injectionsLength = mappings.length;
for (index = 0; index < injectionsLength; index++) {
applicationRequire.injectMapping(
mappings[index].dependency,
mappings[index].name);
}
}
if (dependencies) {
injectionsLength = dependencies.length;
for (index = 0; index < injectionsLength; index++) {
applicationRequire.injectDependency(
dependencies[index].name,
dependencies[index].version);
}
}
return applicationRequire;
});
}
return promise;
});
}
applicationRequirePromise.then(function (applicationRequire) {
return applicationRequire.loadPackage({
location: montageLocation,
hash: params.montageHash
})
.then(function (montageRequire) {
// load the promise package so we can inject the bootstrapped
// promise library back into it
var promiseLocation;
if (params.promiseLocation) {
promiseLocation = URL.resolve(Require.getLocation(), params.promiseLocation);
} else {
//promiseLocation = URL.resolve(montageLocation, "packages/mr/packages/q");
//node tools/build --features="core timers call_get" --browser
promiseLocation = URL.resolve(montageLocation, "node_modules/bluebird");
}
var result = [
montageRequire,
montageRequire.loadPackage({
location: promiseLocation,
hash: params.promiseHash
})
];
return result;
})
.spread(function (montageRequire, promiseRequire) {
montageRequire.inject("core/mini-url", URL);
montageRequire.inject("core/promise", {Promise: Promise});
promiseRequire.inject("bluebird", Promise);
// This prevents bluebird to be loaded twice by mousse's code
promiseRequire.inject("js/browser/bluebird", Promise);
// install the linter, which loads on the first error
config.lint = function (module) {
montageRequire.async("jshint/dist/jshint")
.then(function (JSHINT) {
if (!JSHINT.JSHINT(module.text)) {
console.warn("JSHint Error: "+module.location);
JSHINT.JSHINT.errors.forEach(function (error) {
if (error) {
console.warn("Problem at line "+error.line+" character "+error.character+": "+error.reason);
if (error.evidence) {
console.warn(" " + error.evidence);
}
}
});
}
});
};
// Expose global require and mr
global.require = global.mr = applicationRequire;
return platform.initMontage(montageRequire, applicationRequire, params);
});
});
});
};
// Bootstrapping for multiple-platforms
exports.getPlatform = function () {
if (typeof window !== "undefined" && window && window.document) {
return browser;
} else if (typeof process !== "undefined") {
return require("./node.js");
} else {
throw new Error("Platform not supported.");
}
};
if (typeof window !== "undefined") {
if (global.__MONTAGE_LOADED__) {
console.warn("Montage already loaded!");
} else {
global.__MONTAGE_LOADED__ = true;
exports.initMontage();
exports.initMontageCustomElement();
}
} else {
// may cause additional exports to be injected:
exports.getPlatform();
}
return exports;
}));