Created
September 11, 2018 12:30
-
-
Save akc42/46edb621544753295df8551feb9cc874 to your computer and use it in GitHub Desktop.
Page management
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default function domHost(self) { | |
let parent = self.parentNode; | |
while(parent && parent.nodeType !== 11) { | |
parent = parent.parentNode; //work up the hierarchy | |
} | |
return parent ? parent.host : self; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import domHost from '../modules/host.js'; | |
/** | |
* @polymer | |
* @mixinClass | |
* @mixinFunction | |
*/ | |
export const PasPageManager = (superClass) => class extends superClass { | |
static get properties() { | |
return { | |
page: { | |
type: String, | |
observer: '_pageChanged', | |
readOnly: true | |
}, | |
route: { | |
type: Object | |
}, | |
subRoute: { | |
type: Object | |
}, | |
pageTitle: { | |
type: String, | |
value: '', | |
notify: true | |
} | |
}; | |
} | |
static get observers() { | |
return [ | |
'_routePageChanged(subRoute.active, subRoute.params.page)', | |
'_routeInactive(route.active)' | |
]; | |
} | |
ready() { | |
super.ready(); | |
this.addEventListener('pas-close', e => this._closeRequest(e)); | |
this.addEventListener('pas-closed', e => this._closeResponse(e)); | |
} | |
_closeRequest(e) { | |
var element; | |
e.stopPropagation(); | |
if (e.composedPath()[0] === this) { //we don'tneed to cater for event retargetting | |
//this event was meant for us, so lets see if we are at the home page or not | |
if (this.page !== this.homePage()) { | |
/* not at home page, so we should tell our subordinate (who | |
is currently selected) that there has been a close request | |
*/ | |
element = this.shadowRoot.querySelector('[name=' + this.page + ']'); | |
if (element) { | |
element.dispatchEvent(new CustomEvent('pas-close', {bubbles: true, composed: true})); | |
} else { | |
//eslint-disable-next-line no-console | |
console.warn('didn\'t find the element with page name = %s', this.page); | |
window.dispatchEvent(new CustomEvent('route-updated',{ | |
detail: { | |
segment: this.route.segment, | |
path: '/' | |
} | |
})); | |
} | |
} else { | |
/* we are at home page, either tell our host that we have therefore closed | |
or if we are trying to do an explicit reroute then do if via abstract */ | |
if (!this.closeReroute() && domHost(this)) { | |
domHost(this).dispatchEvent(new CustomEvent('pas-closed', {bubbles: true, composed: true})); | |
} | |
} | |
} else { | |
/* | |
* we have informed our child of the close request but he hasn't responded, | |
* so we just switch to the home page (at our level) | |
* | |
* In order to do that, we take advantage of the fact that we know that the | |
* url for out home page doesn't contain the value of the correct "page" in | |
* the url, and therefore we can emulate what pas-route does without the complexity | |
* that it uses | |
*/ | |
window.dispatchEvent(new CustomEvent('route-updated',{ | |
detail: { | |
segment: this.route.segment, | |
path: '/' | |
} | |
})); | |
} | |
} | |
_closeResponse(e) { | |
/* | |
* we have been told that our child was already at home, so is | |
* closed, we need to switch to the home page (at our level) | |
*/ | |
e.stopPropagation(); | |
//see comment above to see why this works | |
window.dispatchEvent(new CustomEvent('route-updated',{ | |
detail: { | |
segment: this.route.segment, | |
path: '/' | |
} | |
})); | |
} | |
_routeInactive(active) { | |
if (active === undefined) return; | |
if (!active) this.aboutToClose(); | |
} | |
_routePageChanged(active, page) { | |
if (active !== undefined && page !== undefined) { | |
this._setPage((active && page) ? page : this.homePage()); | |
if (active && this.constructor.titles !== undefined) { | |
const title = this.constructor.titles[this.page]; | |
if (title !== undefined) this.pageTitle = title; | |
} | |
} | |
} | |
_pageChanged(page) { | |
if (this.subRoute && this.subRoute.active) { //only load anything if we have a route | |
const element = this.shadowRoot.querySelector('[name=' + page + ']'); | |
if (element !== undefined) { | |
// load page import on demand. | |
performance.mark('start_page_' + page); | |
this.loadPage(page); | |
} | |
} | |
} | |
// abstract | |
aboutToClose() {} | |
closeReroute() {return false;} | |
homePage() {return 'home';} | |
loadPage() {} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let routeCallback = null; | |
let dwellTime = 2000; | |
let lastChangedAt; | |
let route; | |
export function connectUrl(callback) { | |
routeCallback = callback; | |
window.addEventListener('hashchange',urlChanged); | |
window.addEventListener('popstate', urlChanged); | |
window.addEventListener('location-changed',urlChanged); | |
window.addEventListener('route-updated', routeUpdated); | |
Promise.resolve().then(() => { | |
urlChanged(); | |
lastChangedAt = window.performance.now() - (dwellTime - 200); | |
}); | |
} | |
export function disconnectUrl() { | |
routeCallback = null; | |
window.removeEventListener('hashchange',urlChanged); | |
window.removeEventListener('popstate', urlChanged); | |
window.removeEventListener('location-changed',urlChanged); | |
window.removeEventListener('route-updated', routeUpdated); | |
} | |
function urlChanged() { | |
const path = window.decodeURIComponent(window.location.pathname); | |
const query = decodeParams(window.location.search.substring(1)); | |
if (route && route.path === path && route.query === query) return; | |
lastChangedAt = window.performance.now(); | |
route = { | |
path: path, | |
segment: 0, | |
params: {}, | |
query: query, | |
active: true | |
}; | |
if (routeCallback) routeCallback(route); | |
} | |
function routeUpdated(e) { | |
let newPath = route.path; | |
if(e.detail.path !== undefined) { | |
if (Number.isInteger(e.detail.segment)) { | |
let segments = route.path.split('/'); | |
if (segments[0] === '') segments.shift(); //loose leeding | |
if(segments.length > e.detail.segment) segments.length = e.detail.segment; //truncate to just before path | |
if (e.detail.path.length > 1) { | |
const newPaths = e.detail.path.split('/'); | |
if (newPaths[0] === '') newPaths.shift(); //ignore blank if first char of path is '/' | |
segments = segments.concat(newPaths); | |
} | |
newPath = '/' + segments.join('/'); | |
//lose trailing slash if not just a single '/' | |
if (newPath.slice(-1) === '/' && newPath.length > 1) newPath = newPath.slice(0,-1); | |
} else { | |
throw new Error('Invalid segment info in route-updated event'); | |
} | |
} | |
let query = Object.assign({}, route.query); | |
if (e.detail.query !== undefined) { | |
query = e.detail.query; | |
} | |
let newUrl = window.encodeURI(newPath).replace(/#/g, '%23').replace(/\?/g, '%3F'); | |
if (Object.keys(query).length > 0) { | |
newUrl += '?' + encodeParams(query) | |
.replace(/%3F/g, '?') | |
.replace(/%2F/g, '/') | |
.replace(/'/g, '%27') | |
.replace(/#/g, '%23') | |
; | |
} | |
newUrl += window.location.hash; | |
// Tidy up if base tag in header | |
newUrl = new URL(newUrl, window.location.protocol + '//' + window.location.host).href; | |
if (newUrl !== window.location.href) { //has it changed? | |
let now = window.performance.now(); | |
if (lastChangedAt + dwellTime > now) { | |
window.history.replaceState({}, '', newUrl); | |
} else { | |
window.history.pushState({}, '', newUrl); | |
} | |
urlChanged(); | |
} | |
} | |
function encodeParams(params) { | |
const encodedParams = []; | |
for (let key in params) { | |
const value = params[key]; | |
if (value === '') { | |
encodedParams.push(encodeURIComponent(key)); | |
} else { | |
encodedParams.push( | |
encodeURIComponent(key) + '=' + | |
encodeURIComponent(value.toString())); | |
} | |
} | |
return encodedParams.join('&'); | |
} | |
function decodeParams(paramString) { | |
var params = {}; | |
// Work around a bug in decodeURIComponent where + is not | |
// converted to spaces: | |
paramString = (paramString || '').replace(/\+/g, '%20'); | |
var paramList = paramString.split('&'); | |
for (var i = 0; i < paramList.length; i++) { | |
var param = paramList[i].split('='); | |
if (param[0]) { | |
let value; | |
try { | |
value = decodeURIComponent(param[1]); | |
if (value === 'true') { | |
value = true; | |
} else if (value === 'false') { | |
value = false; | |
} else if (/^-?\d+$/.test(value)) { | |
value = parseInt(value,10); | |
} | |
} catch (e) { | |
value = ''; | |
} | |
params[decodeURIComponent(param[0])] = value; | |
} | |
} | |
return params; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default class Route { | |
constructor(match = '', ifmatched = '') { | |
//set default values | |
this.preroute = {active: false, segment: 0, path: '', params: {}, query: {}}; | |
if (ifmatched.length > 0) { | |
this.matcher = ifmatched.split(':'); | |
if (this.matcher.length !== 2) throw new Error('pas-route: Invalid ifmatched String'); | |
} else { | |
this.matcher = []; | |
} | |
this.match = match; | |
//this is our output | |
this._route = {active: false, segment: 0, path: '', params: {}, query: {}}; | |
this.sentRouteChanged = false; | |
} | |
routeChange(preroute) { | |
this.preroute = preroute; //remomber it | |
if (preroute !== undefined && preroute.active && preroute.path.length > 0 && | |
this._ifMatches(preroute.params) ) { | |
if (this.match.length > 0) { | |
let completed = false; | |
if (this.match.slice(-1) === '/') { | |
completed = true; //Special meaning | |
if (this.match.length > 1) { | |
this.match = this.match.slice(0,-1); | |
} | |
} | |
const matchedPieces = this.match.split('/'); | |
if (matchedPieces[0] === '') matchedPieces.shift(); //not interested in blank front | |
const urlPieces = preroute.path.split('/'); | |
if (urlPieces.length < 2 || urlPieces[0] !== '') { | |
//something is wrong with path as it should have started with a '/' | |
this._route.active = false; | |
throw new Error('Route: Invalid path (should start with /) in route'); | |
} | |
urlPieces.shift(); | |
let j = urlPieces.length; | |
const newRoute = { | |
segment: preroute.segment + matchedPieces.length, | |
params: {}, | |
active: true, | |
query: preroute.query | |
}; | |
for(let i = 0; i < matchedPieces.length; i++) { | |
if (j <= 0) { | |
return this._clearOutActive(); | |
} | |
let segment = urlPieces.shift(); | |
j--; | |
if (matchedPieces[i].length !== 0) { | |
if (matchedPieces[i].substring(0,1) === ':') { | |
const key = matchedPieces[i].substring(1); | |
if (key.length > 0) { | |
if (/^-?\d+$/.test(segment)) { | |
segment = parseInt(segment,10); | |
} | |
newRoute.params[key] = segment; | |
} else { | |
throw new Error('Route: Match pattern missing parameter name'); | |
} | |
} else if (matchedPieces[i] !== segment) { | |
return this._clearOutActive(); | |
} | |
} else if (segment.length > 0 ){ | |
return this._clearOutActive(); | |
} | |
} | |
if (completed || preroute.path === '/') { | |
newRoute.path = ''; | |
} else if (j == 0) { | |
newRoute.path = '/'; | |
} else { | |
newRoute.path = '/' + urlPieces.join('/'); | |
} | |
if (!this._route.active || | |
JSON.stringify(this._route.params) !== JSON.stringify(newRoute.params) || | |
JSON.stringify(this._route.query) !== JSON.stringify(newRoute.query) || | |
this._route.path !== newRoute.path || this._route.segment !== newRoute.segment) { | |
this._route = newRoute; | |
this.sentRouteChanged = true; | |
} | |
} else { | |
throw new Error('Route: Match String Required'); | |
} | |
} else { | |
this._clearOutActive(); | |
} | |
return this._route; | |
} | |
/* | |
* set new paramters provided route is active | |
*/ | |
set params(value) { | |
if (this._route.active) { | |
let match = this.match; | |
if (match.slice(-1) === '/' && match.length > 1) match = this.match.slice(0,-1); | |
const matchedPieces = match.split('/'); | |
if (matchedPieces[0] === '') matchedPieces.shift(); //not interested in blank front | |
let urlPieces = this.preroute.path.split('/'); | |
urlPieces.shift(); //loose blank front | |
let changeMade = false; | |
for (let i = 0; i < matchedPieces.length; i++) { | |
if (urlPieces.length < i) urlPieces.push(''); //ensure there is a url segment for this match | |
if (matchedPieces[i].length !== 0) { | |
if (matchedPieces[i].substring(0,1) === ':') { | |
const key = matchedPieces[i].substring(1); | |
if (value[key] !== undefined) { | |
if (Number.isInteger(value[key])) { | |
if (urlPieces[i] !== value[key].toString()) { | |
urlPieces[i] = value[key].toString(); | |
changeMade = true; | |
} | |
} else if (typeof value[key] === 'string') { | |
if (value[key].length > 0) { | |
if (urlPieces[i] !== value[key]) { | |
urlPieces[i] = value[key]; | |
changeMade = true; | |
} | |
} else { | |
//terminate url here | |
urlPieces.length = i; | |
changeMade = true; | |
break; | |
} | |
} else if (value[key] === null) { | |
//terminate url here | |
urlPieces.length = i; | |
changeMade = true; | |
break; | |
} else { | |
throw new Error('Route: Invalid params.' + key + ' provided (should be a String or an Integer)'); | |
} | |
} | |
} | |
} | |
} | |
if (changeMade) window.dispatchEvent(new CustomEvent('route-updated',{ | |
detail: { | |
segment: this.preroute.segment, | |
path: '/' + urlPieces.join('/') | |
} | |
})); | |
} | |
} | |
/* | |
* Set a new query value provided route is active | |
*/ | |
set query(value) { | |
if (this._route.active && JSON.stringify(this._route.query) !== JSON.stringify(value)) { | |
window.dispatchEvent(new CustomEvent('route-updated',{ | |
detail: { | |
query: value | |
} | |
})); | |
} | |
} | |
/* | |
* We can set or break the connection between a pre-route and its route | |
*/ | |
set connection(value) { | |
if (this.preroute.active) { | |
if (this._route.active) { | |
if (value) return; //can't set a matched route active | |
//just reset to a url | |
window.dispatchEvent(new CustomEvent('route-updated',{ | |
detail: { | |
segment: this.preroute.segment, | |
path: '/' | |
} | |
})); | |
} else { | |
if (value) { | |
let match = this.match; | |
if (match.slice(-1) === '/' && match.length > 1) match = this.match.slice(0,-1); | |
const matchedPieces = match.split('/'); | |
if (matchedPieces[0] === '') matchedPieces.shift(); //not interested in blank front | |
if (matchedPieces.length < 1) return; | |
if (matchedPieces.every(piece => piece.length > 0 && piece.indexOf(':') < 0)) { | |
window.dispatchEvent(new CustomEvent('route-updated',{ | |
detail: { | |
segment: this.preroute.segment, | |
path: '/' + matchedPieces.join('/') | |
} | |
})); | |
} | |
} | |
} | |
} | |
} | |
_ifMatches (params) { | |
if (this.matcher.length === 0) return true; //Empty String always matches | |
return (params[this.matcher[0]] !== undefined && params[this.matcher[0]] === this.matcher[1]); | |
} | |
_clearOutActive () { | |
if (this._route === undefined) return; | |
if (this._route.active || !this.sentRouteChanged) { | |
this._route = Object.assign({}, this._route, {active:false}); | |
this.sentRouteChanged = true; | |
} | |
return this._route; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment