oxe
Version:
A mighty tiny web components framework/library
530 lines (407 loc) • 15 kB
JavaScript
import Path from './path.js';
import Loader from './loader.js';
import Events from './events.js';
import Utility from './utility.js';
import Definer from './definer.js';
import Component from './component.js';
const Event = Object.create(Events);
export default {
on: Event.on.bind(Event),
off: Event.off.bind(Event),
emit: Event.emit.bind(Event),
data: [],
ran: false,
location: {},
mode: 'push',
target: null,
contain: false,
folder: './routes',
async setup (option) {
option = option || {};
this.base = option.base === undefined ? this.base : option.base;
this.mode = option.mode === undefined ? this.mode : option.mode;
this.after = option.after === undefined ? this.after : option.after;
this.folder = option.folder === undefined ? this.folder : option.folder;
this.before = option.before === undefined ? this.before : option.before;
this.change = option.change === undefined ? this.change : option.change;
this.target = option.target === undefined ? this.target : option.target;
this.contain = option.contain === undefined ? this.contain : option.contain;
this.external = option.external === undefined ? this.external : option.external;
if (!this.target || typeof this.target === 'string') {
this.target = document.body.querySelector(this.target || 'o-router');
}
if (this.mode !== 'href') {
window.addEventListener('popstate', this.state.bind(this), true);
window.document.addEventListener('click', this.click.bind(this), true);
}
Definer.define('o-router');
await this.add(option.routes);
await this.route(window.location.href, { mode: 'replace' });
},
compareParts (routePath, userPath, split) {
const compareParts = [];
const routeParts = routePath.split(split);
const userParts = userPath.split(split);
if (userParts.length > 1 && userParts[userParts.length - 1] === '') {
userParts.pop();
}
if (routeParts.length > 1 && routeParts[routeParts.length - 1] === '') {
routeParts.pop();
}
for (let i = 0, l = routeParts.length; i < l; i++) {
if (routeParts[i].slice(0, 1) === '(' && routeParts[i].slice(-1) === ')') {
if (routeParts[i] === '(~)') {
return true;
} else if (routeParts[i].indexOf('~') !== -1) {
if (userParts[i]) {
compareParts.push(userParts[i]);
}
} else {
compareParts.push(userParts[i]);
}
} else if (routeParts[i] !== userParts[i]) {
return false;
} else {
compareParts.push(routeParts[i]);
}
}
if (compareParts.join(split) === userParts.join(split)) {
return true;
} else {
return false;
}
},
compare (routePath, userPath) {
const base = Path.normalize(Path.base);
userPath = Path.normalize(userPath);
routePath = Path.normalize(routePath);
if (userPath.slice(0, base.length) !== base) {
userPath = Path.join(base, userPath);
}
if (routePath.slice(0, base.length) !== base) {
routePath = Path.join(base, routePath);
}
if (this.compareParts(routePath, userPath, '/')) {
return true;
}
if (this.compareParts(routePath, userPath, '-')) {
return true;
}
return false;
},
toParameterObject (routePath, userPath) {
let result = {};
if (
!routePath
|| !userPath
|| routePath === '/'
|| userPath === '/'
) return result;
const userParts = userPath.split(/\/|-/);
const routeParts = routePath.split(/\/|-/);
for (let i = 0, l = routeParts.length; i < l; i++) {
let part = routeParts[i];
if (part.slice(0, 1) === '(' && part.slice(-1) === ')') {
const name = part.slice(1, part.length - 1).replace('~', '');
result[name] = userParts[i];
}
}
return result;
},
toQueryString (data) {
let result = '?';
for (let key in data) {
let value = data[key];
result += key + '=' + value + '&';
}
if (result.slice(-1) === '&') {
result = result.slice(0, -1);
}
return result;
},
toQueryObject (path) {
let result = {};
if (path.indexOf('?') === 0) path = path.slice(1);
let queries = path.split('&');
for (let i = 0, l = queries.length; i < l; i++) {
let query = queries[i].split('=');
if (query[0] && query[1]) {
result[query[0]] = query[1];
}
}
return result;
},
toLocationObject (href) {
const location = {};
const parser = document.createElement('a');
parser.href = href;
location.href = parser.href;
location.host = parser.host;
location.port = parser.port;
location.hash = parser.hash;
location.search = parser.search;
location.protocol = parser.protocol;
location.hostname = parser.hostname;
location.pathname = parser.pathname[0] === '/' ? parser.pathname : '/' + parser.pathname;
location.path = location.pathname + location.search + location.hash;
return location;
},
scroll (x, y) {
window.scroll(x, y);
},
back () {
window.history.back();
},
forward () {
window.history.forward();
},
redirect (path) {
window.location.href = path;
},
async add (data) {
if (!data) {
return;
} else if (data.constructor === String) {
let path = data;
if (path.slice(-3) === '.js') {
path = path.slice(0, -3);
}
let load = path;
if (path.slice(-5) === 'index') {
path = path.slice(0, -5);
}
if (path.slice(-6) === 'index/') {
path = path.slice(0, -6);
}
if (path.slice(0, 2) === './') {
path = path.slice(2);
}
if (path.slice(0, 1) !== '/') {
path = '/' + path;
}
load = load + '.js';
load = Path.join(this.folder, load);
this.data.push({ path, load });
} else if (data.constructor === Object) {
if (!data.path) {
throw new Error('Oxe.router.add - route path required');
}
if (!data.name && !data.load && !data.component) {
throw new Error('Oxe.router.add - route requires name, load, or component property');
}
this.data.push(data);
} else if (data.constructor === Array) {
for (let i = 0, l = data.length; i < l; i++) {
await this.add(data[i]);
}
}
},
async load (route) {
if (route.load) {
const load = await Loader.load(route.load);
route = Object.assign({}, load.default, route);
}
if (typeof route.component === 'string') {
route.load = route.component;
const load = await Loader.load(route.load);
route.component = load.default;
}
return route;
},
async remove (path) {
for (let i = 0, l = this.data.length; i < l; i++) {
if (this.data[i].path === path) {
this.data.splice(i, 1);
}
}
},
async get (path) {
for (let i = 0, l = this.data.length; i < l; i++) {
if (this.data[i].path === path) {
this.data[i] = await this.load(this.data[i]);
return this.data[i];
}
}
},
async filter (path) {
const result = [];
for (let i = 0, l = this.data.length; i < l; i++) {
if (this.compare(this.data[i].path, path)) {
this.data[i] = await this.load(this.data[i]);
result.push(this.data[i]);
}
}
return result;
},
async find (path) {
for (let i = 0, l = this.data.length; i < l; i++) {
if (this.compare(this.data[i].path, path)) {
this.data[i] = await this.load(this.data[i]);
return this.data[i];
}
}
},
async render (route) {
if (!route) {
throw new Error('Oxe.render - route argument required. Missing object option.');
}
if (route.title) {
document.title = route.title;
}
const ensures = [];
if (route.keywords) {
ensures.push({
name: 'meta',
query: '[name="keywords"]',
attributes: [
{ name: 'name', value: 'keywords' },
{ name: 'content', value: route.keywords }
]
});
}
if (route.description) {
ensures.push({
name: 'meta',
query: '[name="description"]',
attributes: [
{ name: 'name', value: 'description' },
{ name: 'content', value: route.description }
]
});
}
if (route.canonical) {
ensures.push({
name: 'link',
query: '[rel="canonical"]',
attributes: [
{ name: 'rel', value: 'canonical' },
{ name: 'href', value: route.canonical }
]
});
}
if (ensures.length) {
Promise.all(ensures.map(function (option) {
return Promise.resolve().then(function () {
option.position = 'afterbegin';
option.scope = document.head;
return Utility.ensureElement(option);
});
}));
}
if (!route.target) {
if (!route.component) {
Component.define(route);
route.target = window.document.createElement(route.name);
} else if (route.component.constructor === String) {
route.target = window.document.createElement(route.component);
} else if (route.component.constructor === Object) {
Component.define(route.component);
route.target = window.document.createElement(route.component.name);
} else {
throw new Error('Oxe.router.render - route requires name, load, or component property');
}
}
if (this.target) {
while (this.target.firstChild) {
this.target.removeChild(this.target.firstChild);
}
this.target.appendChild(route.target);
}
this.scroll(0, 0);
},
async route (path, options) {
options = options || {};
if (options.query) {
path += this.toQueryString(options.query);
}
const mode = options.mode || this.mode;
const location = this.toLocationObject(path);
const route = await this.find(location.pathname);
if (!route) {
throw new Error(`Oxe.router.route - missing route ${location.pathname}`);
}
location.route = route;
location.title = location.route.title;
location.query = this.toQueryObject(location.search);
location.parameters = this.toParameterObject(location.route.path, location.pathname);
if (location.route && location.route.handler) {
return await location.route.handler(location);
}
if (location.route && location.route.redirect) {
return await this.redirect(location.route.redirect);
}
if (typeof this.before === 'function') {
await this.before(location);
}
this.emit('route:before', location);
if (mode === 'href') {
return window.location.assign(location.path);
}
window.history[mode + 'State']({ path: location.path }, '', location.path);
this.location = location;
await this.render(location.route);
if (typeof this.after === 'function') {
await this.after(location);
}
this.emit('route:after', location);
},
async state (event) {
const path = event && event.state ? event.state.path : window.location.href;
this.route(path, { mode: 'replace' });
},
async click (event) {
// ignore canceled events, modified clicks, and right clicks
if (
event.target.type ||
event.button !== 0 ||
event.defaultPrevented ||
event.altKey || event.ctrlKey || event.metaKey || event.shiftKey
) {
return;
}
// if shadow dom use
var target = event.path ? event.path[0] : event.target;
var parent = target.parentElement;
if (this.contain) {
while (parent) {
if (parent.nodeName === 'O-ROUTER') {
break;
} else {
parent = parent.parentElement;
}
}
if (parent.nodeName !== 'O-ROUTER') {
return;
}
}
while (target && 'A' !== target.nodeName) {
target = target.parentElement;
}
if (!target || 'A' !== target.nodeName) {
return;
}
// check non-acceptables
if (target.hasAttribute('download') ||
target.hasAttribute('external') ||
target.hasAttribute('o-external') ||
target.href.indexOf('tel:') === 0 ||
target.href.indexOf('ftp:') === 0 ||
target.href.indexOf('file:') === 0 ||
target.href.indexOf('mailto:') === 0 ||
target.href.indexOf(window.location.origin) !== 0 ||
(target.hash !== '' &&
target.origin === window.location.origin &&
target.pathname === window.location.pathname)
) return;
// if external is true then default action
if (this.external &&
(this.external.constructor === RegExp && this.external.test(target.href) ||
this.external.constructor === Function && this.external(target.href) ||
this.external.constructor === String && this.external === target.href)
) return;
event.preventDefault();
if (this.location.href !== target.href) {
this.route(target.href);
}
}
};