single-spa
Advanced tools
Comparing version 1.1.2 to 1.2.0
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { | ||
value: true | ||
value: true | ||
}); | ||
@@ -17,341 +17,397 @@ exports.declareChildApplication = declareChildApplication; | ||
window.singlespa = function (element) { | ||
window.history.pushState(undefined, '', element.getAttribute('href')); | ||
setTimeout(function () { | ||
triggerAppChange(); | ||
}, 10); | ||
return false; | ||
}; | ||
window.singlespa = {}; | ||
window.singlespa.prependUrl = prependUrl; | ||
function prependUrl(prefix, url) { | ||
if (!url.startsWith('/')) { | ||
//relative urls are taken care of by the <base> tag | ||
return url; | ||
} | ||
var parsedURL = document.createElement('a'); | ||
parsedURL.href = url; | ||
if (parsedURL.host === window.location.host && !parsedURL.pathname.startsWith(url)) { | ||
return parsedURL.protocol + '//' + (parsedURL.hostname + ':' + parsedURL.port + '/' + prefix + '/' + parsedURL.pathname + parsedURL.search + parsedURL.hash).replace(/[\/]+/g, '/'); | ||
} else { | ||
return url; | ||
} | ||
} | ||
function declareChildApplication(appLocation, activeWhen) { | ||
if (typeof appLocation !== 'string' || appLocation.length === 0) throw new Error('The first argument must be a non-empty string \'appLocation\''); | ||
if (typeof activeWhen !== 'function') throw new Error('The second argument must be a function \'activeWhen\''); | ||
if (appLocationToApp[appLocation]) throw new Error('There is already an app declared at location ' + appLocation); | ||
if (typeof appLocation !== 'string' || appLocation.length === 0) throw new Error('The first argument must be a non-empty string \'appLocation\''); | ||
if (typeof activeWhen !== 'function') throw new Error('The second argument must be a function \'activeWhen\''); | ||
if (appLocationToApp[appLocation]) throw new Error('There is already an app declared at location ' + appLocation); | ||
appLocationToApp[appLocation] = { | ||
appLocation: appLocation, | ||
activeWhen: activeWhen, | ||
parentApp: mountedApp ? mountedApp.appLocation : null | ||
}; | ||
appLocationToApp[appLocation] = { | ||
appLocation: appLocation, | ||
activeWhen: activeWhen, | ||
parentApp: mountedApp ? mountedApp.appLocation : null | ||
}; | ||
triggerAppChange(); | ||
triggerAppChange(); | ||
} | ||
function addUnhandledRouteHandler(handler) { | ||
if (typeof handler !== 'function') { | ||
throw new Error('The first argument must be a handler function'); | ||
} | ||
unhandledRouteHandlers.push(handler); | ||
if (typeof handler !== 'function') { | ||
throw new Error('The first argument must be a handler function'); | ||
} | ||
unhandledRouteHandlers.push(handler); | ||
} | ||
function updateApplicationSourceCode(appName) { | ||
if (!appLocationToApp[appName]) { | ||
throw new Error('No such app \'' + appName + '\''); | ||
} | ||
var app = appLocationToApp[appName]; | ||
app.lifecycleFunctions.activeApplicationSourceWillUpdate().then(function (resolve) { | ||
//TODO reload the app | ||
resolve(); | ||
}).then(app.lifecycleFunctions.activeApplicationSourceWasUpdated); | ||
if (!appLocationToApp[appName]) { | ||
throw new Error('No such app \'' + appName + '\''); | ||
} | ||
var app = appLocationToApp[appName]; | ||
app.lifecycleFunctions.activeApplicationSourceWillUpdate().then(function (resolve) { | ||
//TODO reload the app | ||
resolve(); | ||
}).then(app.lifecycleFunctions.activeApplicationSourceWasUpdated); | ||
} | ||
function callLifecycleFunction(app, funcName) { | ||
for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { | ||
args[_key - 2] = arguments[_key]; | ||
} | ||
for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { | ||
args[_key - 2] = arguments[_key]; | ||
} | ||
return new Promise(function (resolve) { | ||
callFunc(0); | ||
function callFunc(i) { | ||
var _app$lifecycles$i; | ||
return new Promise(function (resolve) { | ||
callFunc(0); | ||
function callFunc(i) { | ||
var _app$lifecycles$i; | ||
(_app$lifecycles$i = app.lifecycles[i])[funcName].apply(_app$lifecycles$i, args).then(function () { | ||
if (i === app.lifecycles.length - 1) { | ||
resolve(); | ||
} else { | ||
callFunc(++i); | ||
} | ||
}); | ||
} | ||
}); | ||
(_app$lifecycles$i = app.lifecycles[i])[funcName].apply(_app$lifecycles$i, args).then(function () { | ||
if (i === app.lifecycles.length - 1) { | ||
resolve(); | ||
} else { | ||
callFunc(++i); | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
function triggerAppChange() { | ||
var newApp = appForCurrentURL(); | ||
if (!newApp) { | ||
unhandledRouteHandlers.forEach(function (handler) { | ||
handler(mountedApp); | ||
}); | ||
} | ||
function triggerAppChange(event) { | ||
var newApp = appForCurrentURL(); | ||
if (!newApp) { | ||
unhandledRouteHandlers.forEach(function (handler) { | ||
handler(mountedApp); | ||
}); | ||
//nothing to do. Leave the app how it was | ||
console.warn('No app matches the url ' + window.location.toString() + ', and there are no unhandledRouteHandlers'); | ||
return; | ||
} | ||
if (newApp !== mountedApp) { | ||
if (newApp !== mountedApp) { | ||
(mountedApp ? callLifecycleFunction(mountedApp, 'applicationWillUnmount') : new Promise(function (resolve) { | ||
return resolve(); | ||
})).then(function () { | ||
return cleanupDom(); | ||
}).then(function () { | ||
return finishUnmountingApp(mountedApp); | ||
}).then(function () { | ||
return mountedApp ? callLifecycleFunction(mountedApp, 'applicationWasUnmounted') : new Promise(function (resolve) { | ||
return resolve(); | ||
}); | ||
}).then(function () { | ||
return newApp.scriptsLoaded ? new Promise(function (resolve) { | ||
return resolve(); | ||
}) : loadAppForFirstTime(newApp.appLocation); | ||
}).then(function () { | ||
return callLifecycleFunction(newApp, 'applicationWillMount'); | ||
}).then(function () { | ||
return appWillBeMounted(newApp); | ||
}).then(function () { | ||
return insertDomFrom(newApp); | ||
}).then(function () { | ||
return callLifecycleFunction(newApp, 'applicationWasMounted'); | ||
}).then(function () { | ||
return mountedApp = newApp; | ||
}); | ||
} | ||
(mountedApp ? callLifecycleFunction(mountedApp, 'applicationWillUnmount') : new Promise(function (resolve) { | ||
return resolve(); | ||
})).then(function () { | ||
return cleanupDom(); | ||
}).then(function () { | ||
return finishUnmountingApp(mountedApp); | ||
}).then(function () { | ||
return mountedApp ? callLifecycleFunction(mountedApp, 'applicationWasUnmounted') : new Promise(function (resolve) { | ||
return resolve(); | ||
}); | ||
}).then(function () { | ||
return newApp.scriptsLoaded ? new Promise(function (resolve) { | ||
return resolve(); | ||
}) : loadAppForFirstTime(newApp.appLocation); | ||
}).then(function () { | ||
return updateBaseTag(newApp.publicRoot); | ||
}).then(function () { | ||
return callLifecycleFunction(newApp, 'applicationWillMount'); | ||
}).then(function () { | ||
return appWillBeMounted(newApp); | ||
}).then(function () { | ||
return insertDomFrom(newApp); | ||
}).then(function () { | ||
return callLifecycleFunction(newApp, 'applicationWasMounted'); | ||
}).then(function () { | ||
return mountedApp = newApp; | ||
}); | ||
} | ||
} | ||
function updateBaseTag(newBaseHref) { | ||
return new Promise(function (resolve) { | ||
if (document.baseURI === window.location.protocol + '//' + (window.location.hostname + ':' + window.location.port + newBaseHref)) { | ||
debugger; | ||
resolve(); | ||
} else { | ||
newBaseHref = ('/' + newBaseHref + '/').replace(/[\/]+/g, '/'); | ||
var baseTags = document.querySelectorAll('base'); | ||
for (var i = 0; i < baseTags.length; i++) { | ||
baseTags[i].parentNode.removeChild(baseTags[i]); | ||
} | ||
var newBase = document.createElement('base'); | ||
newBase.setAttribute('href', newBaseHref); | ||
document.head.appendChild(newBase); | ||
resolve(); | ||
} | ||
}); | ||
} | ||
function cleanupDom() { | ||
return new Promise(function (resolve) { | ||
while (document.head.childNodes.length > 0) { | ||
document.head.removeChild(document.head.childNodes[0]); | ||
} | ||
while (document.body.childNodes.length > 0) { | ||
document.body.removeChild(document.body.childNodes[0]); | ||
} | ||
resolve(); | ||
}); | ||
return new Promise(function (resolve) { | ||
for (var i = 0; i < document.documentElement.attributes.length; i++) { | ||
document.documentElement.removeAttribute(document.documentElement.attributes[i].name); | ||
} | ||
var numHeadElsToSkip = 0; | ||
while (document.head.childNodes.length > numHeadElsToSkip) { | ||
if (document.head.childNodes[numHeadElsToSkip].tagName !== 'BASE') document.head.removeChild(document.head.childNodes[numHeadElsToSkip]);else numHeadElsToSkip++; | ||
} | ||
while (document.body.childNodes.length > 0) { | ||
document.body.removeChild(document.body.childNodes[0]); | ||
} | ||
resolve(); | ||
}); | ||
} | ||
function insertDomFrom(app) { | ||
return new Promise(function (resolve) { | ||
var deepClone = true; | ||
var clonedAppDom = app.parsedDom.cloneNode(deepClone); | ||
return new Promise(function (resolve) { | ||
var deepClone = true; | ||
var clonedAppDom = app.parsedDom.cloneNode(deepClone); | ||
for (var i = 0; i < clonedAppDom.attributes.length; i++) { | ||
var attr = clonedAppDom.attributes[i]; | ||
document.documentElement.setAttribute(attr.name, attr.value); | ||
} | ||
for (var i = 0; i < clonedAppDom.attributes.length; i++) { | ||
var attr = clonedAppDom.attributes[i]; | ||
document.documentElement.setAttribute(attr.name, attr.value); | ||
} | ||
var appHead = app.parsedDom.querySelector('head'); | ||
while (appHead.childNodes.length > 0) { | ||
document.head.appendChild(appHead.childNodes[0]); | ||
} | ||
var appHead = app.parsedDom.querySelector('head'); | ||
while (appHead.childNodes.length > 0) { | ||
document.head.appendChild(appHead.childNodes[0]); | ||
} | ||
var appBody = app.parsedDom.querySelector('body'); | ||
while (appBody.childNodes.length > 0) { | ||
document.body.appendChild(appBody.childNodes[0]); | ||
} | ||
var appBody = app.parsedDom.querySelector('body'); | ||
while (appBody.childNodes.length > 0) { | ||
document.body.appendChild(appBody.childNodes[0]); | ||
} | ||
app.parsedDom = clonedAppDom; | ||
resolve(); | ||
}); | ||
app.parsedDom = clonedAppDom; | ||
resolve(); | ||
}); | ||
} | ||
function loadAppForFirstTime(appLocation) { | ||
return new Promise(function (resolve, reject) { | ||
var currentAppSystemGlobal = window.System; | ||
window.System = nativeSystemGlobal; | ||
nativeSystemGlobal['import'](appLocation).then(function (restOfApp) { | ||
registerApplication(appLocation, restOfApp.publicRoot, restOfApp.pathToIndex, restOfApp.lifecycles); | ||
var app = appLocationToApp[appLocation]; | ||
window.System = currentAppSystemGlobal; | ||
callLifecycleFunction(app, 'scriptsWillBeLoaded').then(function () { | ||
return loadIndex(app); | ||
}).then(function () { | ||
return callLifecycleFunction(app, 'scriptsWereLoaded'); | ||
}).then(function () { | ||
return resolve(); | ||
}); | ||
}); | ||
}); | ||
return new Promise(function (resolve, reject) { | ||
var currentAppSystemGlobal = window.System; | ||
window.System = nativeSystemGlobal; | ||
nativeSystemGlobal['import'](appLocation).then(function (restOfApp) { | ||
registerApplication(appLocation, restOfApp.publicRoot, restOfApp.pathToIndex, restOfApp.lifecycles); | ||
var app = appLocationToApp[appLocation]; | ||
window.System = currentAppSystemGlobal; | ||
updateBaseTag(app.publicRoot).then(function () { | ||
return callLifecycleFunction(app, 'scriptsWillBeLoaded'); | ||
}).then(function () { | ||
return loadIndex(app); | ||
}).then(function () { | ||
return callLifecycleFunction(app, 'scriptsWereLoaded'); | ||
}).then(function () { | ||
return resolve(); | ||
}); | ||
}); | ||
}); | ||
} | ||
function loadIndex(app) { | ||
return new Promise(function (resolve) { | ||
var request = new XMLHttpRequest(); | ||
request.addEventListener('load', htmlLoaded); | ||
request.open('GET', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + '/' + app.publicRoot + '/' + app.pathToIndex); | ||
request.send(); | ||
return new Promise(function (resolve) { | ||
var request = new XMLHttpRequest(); | ||
request.addEventListener('load', htmlLoaded); | ||
request.open('GET', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + '/' + app.publicRoot + '/' + app.pathToIndex); | ||
request.send(); | ||
function htmlLoaded() { | ||
var parser = new DOMParser(); | ||
var dom = parser.parseFromString(this.responseText, 'text/html'); | ||
var isLoadingScript = false; | ||
var scriptsToBeLoaded = []; | ||
function htmlLoaded() { | ||
var parser = new DOMParser(); | ||
var dom = parser.parseFromString(this.responseText, 'text/html'); | ||
var isLoadingScript = false; | ||
var scriptsToBeLoaded = []; | ||
traverseNode(dom); | ||
app.parsedDom = dom.documentElement; | ||
if (app.scriptsLoaded) { | ||
setTimeout(function () { | ||
resolve(); | ||
}, 10); | ||
} | ||
traverseNode(dom); | ||
app.parsedDom = dom.documentElement; | ||
if (app.scriptsLoaded) { | ||
setTimeout(function () { | ||
resolve(); | ||
}, 10); | ||
} | ||
function traverseNode(node) { | ||
for (var i = 0; i < node.childNodes.length; i++) { | ||
var child = node.childNodes[i]; | ||
if (child.tagName === 'SCRIPT') { | ||
if (child.getAttribute('src')) { | ||
child.setAttribute('src', prependURL(child.getAttribute('src'), app.publicRoot)); | ||
} | ||
//we put the scripts onto the page as part of the scriptsLoaded lifecycle | ||
scriptsToBeLoaded.push(child); | ||
appendScriptTag(); | ||
} else if (child.tagName === 'LINK' && child.getAttribute('href')) { | ||
child.setAttribute('href', prependURL(child.getAttribute('href'), app.publicRoot)); | ||
} else if (child.tagName === 'IMG' && child.getAttribute('src')) { | ||
child.setAttribute('src', prependURL(child.getAttribute('src'), app.publicRoot)); | ||
} | ||
traverseNode(child); | ||
} | ||
} | ||
function traverseNode(node) { | ||
for (var i = 0; i < node.childNodes.length; i++) { | ||
var child = node.childNodes[i]; | ||
if (child.tagName === 'SCRIPT') { | ||
scriptsToBeLoaded.push(child); | ||
appendScriptTag(); | ||
} | ||
traverseNode(child); | ||
} | ||
} | ||
function prependURL(url, prefix) { | ||
var parsedURL = document.createElement('a'); | ||
parsedURL.href = url; | ||
var result = parsedURL.protocol + '//' + (parsedURL.hostname + ':' + parsedURL.port + '/' + prefix + '/' + parsedURL.pathname + parsedURL.search + parsedURL.hash).replace(/[\/]+/g, '/'); | ||
return result; | ||
} | ||
function appendScriptTag() { | ||
if (isLoadingScript) { | ||
return; | ||
} | ||
if (scriptsToBeLoaded.length === 0) { | ||
app.scriptsLoaded = true; | ||
if (app.parsedDom) { | ||
//loading a script was the last thing we were waiting on | ||
setTimeout(function () { | ||
resolve(); | ||
}, 10); | ||
} | ||
return; | ||
} | ||
var originalScriptTag = scriptsToBeLoaded.splice(0, 1)[0]; | ||
//one does not simply append script tags to the dom | ||
var scriptTag = document.createElement('script'); | ||
for (var i = 0; i < originalScriptTag.attributes.length; i++) { | ||
scriptTag.setAttribute(originalScriptTag.attributes[i].nodeName, originalScriptTag.getAttribute(originalScriptTag.attributes[i].nodeName)); | ||
} | ||
if (!scriptTag.src) { | ||
scriptTag.text = originalScriptTag.text; | ||
} | ||
isLoadingScript = true; | ||
document.head.appendChild(scriptTag); | ||
if (scriptTag.src) { | ||
scriptTag.onload = function () { | ||
isLoadingScript = false; | ||
appendScriptTag(); | ||
}; | ||
} else { | ||
isLoadingScript = false; | ||
appendScriptTag(); | ||
} | ||
//normally when you appendChild, the old parent no longer has the child anymore. We have to simulate that since we're not really appending the child | ||
originalScriptTag.remove(); | ||
} | ||
} | ||
}); | ||
function appendScriptTag() { | ||
if (isLoadingScript) { | ||
return; | ||
} | ||
if (scriptsToBeLoaded.length === 0) { | ||
app.scriptsLoaded = true; | ||
if (app.parsedDom) { | ||
//loading a script was the last thing we were waiting on | ||
setTimeout(function () { | ||
resolve(); | ||
}, 10); | ||
} | ||
return; | ||
} | ||
var originalScriptTag = scriptsToBeLoaded.splice(0, 1)[0]; | ||
//one does not simply append script tags to the dom | ||
var scriptTag = document.createElement('script'); | ||
for (var i = 0; i < originalScriptTag.attributes.length; i++) { | ||
scriptTag.setAttribute(originalScriptTag.attributes[i].nodeName, originalScriptTag.getAttribute(originalScriptTag.attributes[i].nodeName)); | ||
} | ||
if (!scriptTag.src) { | ||
scriptTag.text = originalScriptTag.text; | ||
} | ||
isLoadingScript = true; | ||
document.head.appendChild(scriptTag); | ||
if (scriptTag.src) { | ||
scriptTag.onload = function () { | ||
isLoadingScript = false; | ||
appendScriptTag(); | ||
}; | ||
} else { | ||
isLoadingScript = false; | ||
appendScriptTag(); | ||
} | ||
//normally when you appendChild, the old parent no longer has the child anymore. We have to simulate that since we're not really appending the child | ||
originalScriptTag.remove(); | ||
} | ||
} | ||
}); | ||
} | ||
function registerApplication(appLocation, publicRoot, pathToIndex, lifecycles) { | ||
//validate | ||
if (typeof publicRoot !== 'string') { | ||
throw new Error('App ' + appLocation + ' must export a publicRoot string'); | ||
} | ||
if (typeof pathToIndex !== 'string') { | ||
throw new Error('App ' + appLocation + ' must export a pathToIndex string'); | ||
} | ||
if (typeof lifecycles !== 'object' && typeof lifecycles !== 'function') { | ||
throw new Error('App ' + appLocation + ' must export a \'lifecycles\' object or array of objects'); | ||
} | ||
if (!Array.isArray(lifecycles)) { | ||
lifecycles = [lifecycles]; | ||
} | ||
//validate | ||
if (typeof publicRoot !== 'string') { | ||
throw new Error('App ' + appLocation + ' must export a publicRoot string'); | ||
} | ||
if (typeof pathToIndex !== 'string') { | ||
throw new Error('App ' + appLocation + ' must export a pathToIndex string'); | ||
} | ||
if (typeof lifecycles !== 'object' && typeof lifecycles !== 'function') { | ||
throw new Error('App ' + appLocation + ' must export a \'lifecycles\' object or array of objects'); | ||
} | ||
if (!Array.isArray(lifecycles)) { | ||
lifecycles = [lifecycles]; | ||
} | ||
var _loop = function (i) { | ||
requiredLifeCycleFuncs.forEach(function (requiredLifeCycleFunc) { | ||
if (typeof lifecycles[i][requiredLifeCycleFunc] !== 'function') { | ||
throw new Error('In app \'' + appLocation + '\', The lifecycle at index ' + i + ' does not have required function ' + requiredLifeCycleFunc); | ||
} | ||
}); | ||
}; | ||
var _loop = function (i) { | ||
requiredLifeCycleFuncs.forEach(function (requiredLifeCycleFunc) { | ||
if (typeof lifecycles[i][requiredLifeCycleFunc] !== 'function') { | ||
throw new Error('In app \'' + appLocation + '\', The lifecycle at index ' + i + ' does not have required function ' + requiredLifeCycleFunc); | ||
} | ||
}); | ||
}; | ||
for (var i = 0; i < lifecycles.length; i++) { | ||
_loop(i); | ||
} | ||
for (var i = 0; i < lifecycles.length; i++) { | ||
_loop(i); | ||
} | ||
//register | ||
var app = appLocationToApp[appLocation]; | ||
app.publicRoot = publicRoot; | ||
app.pathToIndex = pathToIndex; | ||
app.hashChangeFunctions = []; | ||
app.popStateFunctions = []; | ||
app.lifecycles = lifecycles; | ||
//register | ||
var app = appLocationToApp[appLocation]; | ||
app.publicRoot = publicRoot; | ||
app.pathToIndex = pathToIndex; | ||
app.hashChangeFunctions = []; | ||
app.popStateFunctions = []; | ||
app.lifecycles = lifecycles; | ||
} | ||
nativeAddEventListener('popstate', triggerAppChange); | ||
nativeAddEventListener('popstate', function () { | ||
triggerAppChange.apply(undefined, arguments); | ||
}); | ||
function appForCurrentURL() { | ||
var appsForCurrentUrl = []; | ||
for (var appName in appLocationToApp) { | ||
var app = appLocationToApp[appName]; | ||
if (app.activeWhen(window.location)) { | ||
appsForCurrentUrl.push(app); | ||
} | ||
} | ||
switch (appsForCurrentUrl.length) { | ||
case 0: | ||
return undefined; | ||
case 1: | ||
return appsForCurrentUrl[0]; | ||
default: | ||
appNames = appsForCurrentUrl.map(function (app) { | ||
return app.name; | ||
}); | ||
throw new Error('The following applications all claim to own the location ' + window.location.href + ' -- ' + appnames.toString()); | ||
} | ||
var appsForCurrentUrl = []; | ||
for (var appName in appLocationToApp) { | ||
var app = appLocationToApp[appName]; | ||
if (app.activeWhen(window.location)) { | ||
appsForCurrentUrl.push(app); | ||
} | ||
} | ||
switch (appsForCurrentUrl.length) { | ||
case 0: | ||
return undefined; | ||
case 1: | ||
return appsForCurrentUrl[0]; | ||
default: | ||
appNames = appsForCurrentUrl.map(function (app) { | ||
return app.name; | ||
}); | ||
throw new Error('The following applications all claim to own the location ' + window.location.href + ' -- ' + appNames.toString()); | ||
} | ||
} | ||
function appWillBeMounted(app) { | ||
return new Promise(function (resolve) { | ||
app.hashChangeFunctions.forEach(function (hashChangeFunction) { | ||
nativeAddEventListener('hashchange', hashChangeFunction); | ||
}); | ||
app.popStateFunctions.forEach(function (popStateFunction) { | ||
nativeAddEventListener('popstate', popStateFunction); | ||
}); | ||
resolve(); | ||
}); | ||
return new Promise(function (resolve) { | ||
app.hashChangeFunctions.forEach(function (hashChangeFunction) { | ||
nativeAddEventListener('hashchange', hashChangeFunction); | ||
}); | ||
app.popStateFunctions.forEach(function (popStateFunction) { | ||
nativeAddEventListener('popstate', popStateFunction); | ||
}); | ||
resolve(); | ||
}); | ||
} | ||
function finishUnmountingApp(app) { | ||
return new Promise(function (resolve) { | ||
if (!app) { | ||
resolve(); | ||
return; | ||
} | ||
app.hashChangeFunctions.forEach(function (hashChangeFunction) { | ||
window.removeEventListener('hashchange', hashChangeFunction); | ||
}); | ||
app.popStateFunctions.forEach(function (popStateFunction) { | ||
window.removeEventListener('popstate', popStateFunction); | ||
}); | ||
resolve(); | ||
}); | ||
return new Promise(function (resolve) { | ||
if (!app) { | ||
resolve(); | ||
return; | ||
} | ||
app.hashChangeFunctions.forEach(function (hashChangeFunction) { | ||
window.removeEventListener('hashchange', hashChangeFunction); | ||
}); | ||
app.popStateFunctions.forEach(function (popStateFunction) { | ||
window.removeEventListener('popstate', popStateFunction); | ||
}); | ||
resolve(); | ||
}); | ||
} | ||
window.addEventListener = function (name, fn) { | ||
if (mountedApp) { | ||
if (name === 'popstate') { | ||
mountedApp.popStateFunctions.push(fn); | ||
} else if (name === 'hashchange') { | ||
mountedApp.hashChangeFunctions.push(fn); | ||
} | ||
nativeAddEventListener.apply(this, arguments); | ||
} | ||
if (mountedApp) { | ||
if (name === 'popstate') { | ||
mountedApp.popStateFunctions.push(fn); | ||
} else if (name === 'hashchange') { | ||
mountedApp.hashChangeFunctions.push(fn); | ||
} | ||
} | ||
nativeAddEventListener.apply(this, arguments); | ||
}; | ||
function addEventsToAnchors() { | ||
setTimeout(function () { | ||
var aTags = document.querySelectorAll('a:not([singlespa])'); | ||
for (var i = 0; i < aTags.length; i++) { | ||
aTags[i].addEventListener('click', anchorClicked); | ||
aTags[i].setAttribute('singlespa', ''); | ||
} | ||
addEventsToAnchors(); | ||
}, 12); | ||
} | ||
addEventsToAnchors(); | ||
function anchorClicked(event) { | ||
if (window.location.host !== this.host || window.location.protocol !== this.protocol) { | ||
//do the default thing | ||
return; | ||
} else { | ||
event.preventDefault(); | ||
if (this.getAttribute('href').startsWith('#')) { | ||
window.location.hash = this.getAttribute('href'); | ||
} else { | ||
window.history.pushState(undefined, '', this.href); | ||
} | ||
setTimeout(function () { | ||
return triggerAppChange(); | ||
}, 2); | ||
} | ||
} |
{ | ||
"name": "single-spa", | ||
"version": "1.1.2", | ||
"version": "1.2.0", | ||
"description": "Multiple applications, one page", | ||
@@ -5,0 +5,0 @@ "main": "src/single-spa.js", |
@@ -6,3 +6,3 @@ # single-spa | ||
## View the demo! | ||
A [demo is live](http://single-spa.surge.sh) on surge.sh. Don't be turned off by the lack of styling -- I'll be fixing that soon. | ||
A [demo is live](http://single-spa.surge.sh) on surge.sh. Don't be turned off by the lack of styling -- I'll be fixing that soon. It's based on the code in the [examples](https://github.com/joeldenning/single-spa-examples) repository. | ||
@@ -9,0 +9,0 @@ ## Ideology |
@@ -18,6 +18,17 @@ let appLocationToApp = {}; | ||
window.singlespa = function(element) { | ||
window.history.pushState(undefined, '', element.getAttribute('href')); | ||
setTimeout(() => triggerAppChange(), 10); | ||
return false; | ||
window.singlespa = {}; | ||
window.singlespa.prependUrl = prependUrl; | ||
function prependUrl(prefix, url) { | ||
if (!url.startsWith('/')) { | ||
//relative urls are taken care of by the <base> tag | ||
return url; | ||
} | ||
let parsedURL = document.createElement('a'); | ||
parsedURL.href = url; | ||
if (parsedURL.host === window.location.host && !parsedURL.pathname.startsWith(url)) { | ||
return `${parsedURL.protocol}//` + `${parsedURL.hostname}:${parsedURL.port}/${prefix}/${parsedURL.pathname}${parsedURL.search}${parsedURL.hash}`.replace(/[\/]+/g, '/'); | ||
} else { | ||
return url; | ||
} | ||
} | ||
@@ -96,2 +107,3 @@ | ||
.then(() => (newApp.scriptsLoaded ? new Promise((resolve) => resolve()) : loadAppForFirstTime(newApp.appLocation))) | ||
.then(() => updateBaseTag(newApp.publicRoot)) | ||
.then(() => callLifecycleFunction(newApp, 'applicationWillMount')) | ||
@@ -102,11 +114,24 @@ .then(() => appWillBeMounted(newApp)) | ||
.then(() => mountedApp = newApp) | ||
} else if (mountedApp && event) { | ||
var eventArgs = arguments; | ||
if (event.type === 'popstate') | ||
mountedApp.popStateFunctions.forEach((popStateFunction) => popStateFunction.apply(window, eventArgs)); | ||
else if (event.type === 'hashchange') | ||
mountedApp.hashChangeFunctions.forEach((hashChangeFunction) => hashChangeFunction.apply(window, eventArgs)); | ||
} | ||
} | ||
function updateBaseTag(newBaseHref) { | ||
return new Promise((resolve) => { | ||
if (document.baseURI === `${window.location.protocol}//` + `${window.location.hostname}:${window.location.port}${newBaseHref}`) { | ||
debugger; | ||
resolve(); | ||
} else { | ||
newBaseHref = `/${newBaseHref}/`.replace(/[\/]+/g, '/'); | ||
let baseTags = document.querySelectorAll('base'); | ||
for (let i=0; i<baseTags.length; i++) { | ||
baseTags[i].parentNode.removeChild(baseTags[i]); | ||
} | ||
let newBase = document.createElement('base'); | ||
newBase.setAttribute('href', newBaseHref); | ||
document.head.appendChild(newBase); | ||
resolve(); | ||
} | ||
}); | ||
} | ||
function cleanupDom() { | ||
@@ -117,4 +142,8 @@ return new Promise((resolve) => { | ||
} | ||
while (document.head.childNodes.length > 0) { | ||
document.head.removeChild(document.head.childNodes[0]); | ||
let numHeadElsToSkip = 0; | ||
while (document.head.childNodes.length > numHeadElsToSkip) { | ||
if (document.head.childNodes[numHeadElsToSkip].tagName !== 'BASE') | ||
document.head.removeChild(document.head.childNodes[numHeadElsToSkip]); | ||
else | ||
numHeadElsToSkip++; | ||
} | ||
@@ -161,3 +190,4 @@ while (document.body.childNodes.length > 0) { | ||
window.System = currentAppSystemGlobal; | ||
callLifecycleFunction(app, 'scriptsWillBeLoaded') | ||
updateBaseTag(app.publicRoot) | ||
.then(() => callLifecycleFunction(app, 'scriptsWillBeLoaded')) | ||
.then(() => loadIndex(app)) | ||
@@ -195,12 +225,4 @@ .then(() => callLifecycleFunction(app, 'scriptsWereLoaded')) | ||
if (child.tagName === 'SCRIPT') { | ||
if (child.getAttribute('src')) { | ||
child.setAttribute('src', prependURL(child.getAttribute('src'), app.publicRoot)); | ||
} | ||
//we put the scripts onto the page as part of the scriptsLoaded lifecycle | ||
scriptsToBeLoaded.push(child); | ||
appendScriptTag(); | ||
} else if (child.tagName === 'LINK' && child.getAttribute('href')) { | ||
child.setAttribute('href', prependURL(child.getAttribute('href'), app.publicRoot)); | ||
} else if (child.tagName === 'IMG' && child.getAttribute('src')) { | ||
child.setAttribute('src', prependURL(child.getAttribute('src'), app.publicRoot)); | ||
} | ||
@@ -211,8 +233,2 @@ traverseNode(child); | ||
function prependURL(url, prefix) { | ||
let parsedURL = document.createElement('a'); | ||
parsedURL.href = url; | ||
let result = `${parsedURL.protocol}//` + `${parsedURL.hostname}:${parsedURL.port}/${prefix}/${parsedURL.pathname}${parsedURL.search}${parsedURL.hash}`.replace(/[\/]+/g, '/'); | ||
return result; | ||
} | ||
@@ -310,3 +326,3 @@ function appendScriptTag() { | ||
appNames = appsForCurrentUrl.map((app) => app.name); | ||
throw new Error(`The following applications all claim to own the location ${window.location.href} -- ${appnames.toString()}`) | ||
throw new Error(`The following applications all claim to own the location ${window.location.href} -- ${appNames.toString()}`) | ||
} | ||
@@ -349,8 +365,33 @@ } | ||
mountedApp.hashChangeFunctions.push(fn); | ||
} | ||
} | ||
nativeAddEventListener.apply(this, arguments); | ||
} | ||
function addEventsToAnchors() { | ||
setTimeout(function() { | ||
const aTags = document.querySelectorAll('a:not([singlespa])'); | ||
for (let i=0; i<aTags.length; i++) { | ||
aTags[i].addEventListener('click', anchorClicked); | ||
aTags[i].setAttribute('singlespa', ''); | ||
} | ||
addEventsToAnchors(); | ||
}, 12) | ||
} | ||
addEventsToAnchors(); | ||
function anchorClicked(event) { | ||
if (window.location.host !== this.host || window.location.protocol !== this.protocol) { | ||
//do the default thing | ||
return; | ||
} else { | ||
event.preventDefault(); | ||
if (this.getAttribute('href').startsWith('#')) { | ||
window.location.hash = this.getAttribute('href'); | ||
} else { | ||
nativeAddEventListener.apply(this, arguments); | ||
window.history.pushState(undefined, '', this.href); | ||
} | ||
} else { | ||
nativeAddEventListener.apply(this, arguments); | ||
setTimeout(() => triggerAppChange(), 2); | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
33980
726
0