oxe
Version:
A mighty tiny web components framework/library
324 lines (256 loc) • 10.9 kB
text/typescript
import Load from './load';
type Option = {
cache?: boolean;
folder?: string;
contain?: boolean;
dynamic?: boolean;
external?: string;
target: string | Element;
after?: (location: any, target: Element) => Promise<any>;
before?: (location: any, target: Element) => Promise<any>;
};
const absolute = function (path: string) {
const a = document.createElement('a');
a.href = path;
return a.pathname;
};
export default new class Router {
#target: Element;
#data: object = {};
#folder: string = '';
#cache: boolean = true;
#dynamic: boolean = true;
#contain: boolean = false;
#external: string | RegExp | Function;
#after?: (location: any, target: Element) => Promise<any>;
#before?: (location: any, target: Element) => Promise<any>;
get hash () { return window.location.hash; }
get host () { return window.location.host; }
get hostname () { return window.location.hostname; }
get href () { return window.location.href; }
get origin () { return window.location.origin; }
get pathname () { return window.location.pathname; }
get port () { return window.location.port; }
get protocol () { return window.location.protocol; }
get search () { return window.location.search; }
get query () {
const result = {};
const search = window.location.search;
if (!search) return result;
const queries = search.slice(1).split('&');
for (const query of queries) {
let [ name, value ] = query.split('=');
name = decodeURIComponent(name.replace(/\+/g, ' '));
value = decodeURIComponent(value.replace(/\+/g, ' '));
if (name in result) {
if (typeof result[ name ] === 'object') {
result[ name ].push(value);
} else {
result[ name ] = [ result[ name ], value ];
}
} else {
result[ name ] = value;
}
}
return result;
}
// set query (search) { }
back () { window.history.back(); }
forward () { window.history.forward(); }
reload () { window.location.reload(); }
redirect (href: string) { window.location.href = href; }
async setup (option: Option) {
if ('folder' in option) this.#folder = option.folder;
if ('contain' in option) this.#contain = option.contain;
if ('dynamic' in option) this.#dynamic = option.dynamic;
if ('external' in option) this.#external = option.external;
if ('before' in option) this.#before = option.before;
if ('after' in option) this.#after = option.after;
if ('cache' in option) this.#cache = option.cache;
// if ('beforeConnected' in option) this.#beforeConnected = option.beforeConnected;
// if ('afterConnected' in option) this.#afterConnected = option.afterConnected;
this.#target = option.target instanceof Element ? option.target : document.body.querySelector(option.target);
if (this.#dynamic) {
window.addEventListener('popstate', this.#state.bind(this), true);
if (this.#contain) {
this.#target.addEventListener('click', this.#click.bind(this), true);
} else {
window.document.addEventListener('click', this.#click.bind(this), true);
}
}
return this.replace(window.location.href);
}
async assign (data: string) {
return this.#go(data, { mode: 'push' });
}
async replace (data: string) {
return this.#go(data, { mode: 'replace' });
}
#location (href: string = window.location.href) {
const parser = document.createElement('a');
parser.href = href;
return {
// path: '',
// path: parser.pathname,
href: parser.href,
host: parser.host,
port: parser.port,
hash: parser.hash,
search: parser.search,
protocol: parser.protocol,
hostname: parser.hostname,
pathname: parser.pathname
// pathname: parser.pathname[0] === '/' ? parser.pathname : '/' + parser.pathname
};
// location.path = location.pathname + location.search + location.hash;
// return location;
}
async #go (path: string, options: any = {}) {
// if (options.query) {
// path += Query(options.query);
// }
const mode = options.mode || 'push';
const location = this.#location(path);
let element;
if (location.pathname in this.#data) {
const route = this.#data[ location.pathname ];
element = this.#cache ? route.element : window.document.createElement(route.name);
} else {
const path = location.pathname.endsWith('/') ? `${location.pathname}index` : location.pathname;
const base = document.baseURI.replace(window.location.origin, '');
let load = path.startsWith(base) ? path.replace(base, '') : path;
if (load.slice(0, 2) === './') load = load.slice(2);
if (load.slice(0, 1) !== '/') load = '/' + load;
if (load.slice(0, 1) === '/') load = load.slice(1);
load = `${this.#folder}/${load}.js`.replace(/\/+/g, '/');
load = absolute(load);
let component;
try {
component = (await Load(load)).default;
} catch (error) {
if (error.message === `Failed to fetch dynamically imported module: ${window.location.origin}${load}`) {
component = (await Load(absolute(`${this.#folder}/all.js`))).default;
} else {
throw error;
}
}
const name = 'route' + path.replace(/\/+/g, '-');
window.customElements.define(name, component);
element = window.document.createElement(name);
this.#data[ location.pathname ] = { element: this.#cache ? element : null, name };
}
if (this.#before) await this.#before(location, element);
if (!this.#dynamic) {
return window.location[ mode === 'push' ? 'assign' : mode ](location.href);
}
window.history.replaceState({
href: window.location.href,
top: document.documentElement.scrollTop || document.body.scrollTop || 0
}, '', window.location.href);
window.history[ mode + 'State' ]({
top: 0,
href: location.href
}, '', location.href);
const keywords = document.querySelector('meta[name="keywords"]');
const description = document.querySelector('meta[name="description"]');
if (element.title) window.document.title = element.title;
if (element.keywords && keywords) keywords.setAttribute('content', element.keywords);
if (element.description && description) description.setAttribute('content', element.description);
while (this.#target.firstChild) {
this.#target.removeChild(this.#target.firstChild);
}
if (this.#after) {
element.removeEventListener('afterconnected', this.#data[ location.pathname ].after);
const after = this.#after.bind(this.#after, location, element);
this.#data[ location.pathname ].after = after;
element.addEventListener('afterconnected', after);
}
this.#target.appendChild(element);
window.dispatchEvent(new CustomEvent('router', { detail: location }));
}
async #state (event) {
await this.replace(event.state?.href || window.location.href);
window.scroll(event.state?.top || 0, 0);
}
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
let target = event.path ? event.path[ 0 ] : event.target;
let parent = target.parentElement;
if (this.#contain) {
while (parent) {
if (parent.nodeName === this.#target.nodeName) {
break;
} else {
parent = parent.parentElement;
}
}
if (parent.nodeName !== this.#target.nodeName) {
return;
}
}
while (target && 'A' !== target.nodeName) {
target = target.parentElement;
}
if (!target || 'A' !== target.nodeName) {
return;
}
if (target.hasAttribute('download') ||
target.hasAttribute('external') ||
target.hasAttribute('o-external') ||
target.href.startsWith('tel:') ||
target.href.startsWith('ftp:') ||
target.href.startsWith('file:)') ||
target.href.startsWith('mailto:') ||
!target.href.startsWith(window.location.origin)
// ||
// (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 instanceof RegExp && this.#external.test(target.href) ||
typeof this.#external === 'function' && this.#external(target.href) ||
typeof this.#external === 'string' && this.#external === target.href)
) return;
event.preventDefault();
this.assign(target.href);
}
};
// function Query (data) {
// data = data || window.location.search;
// if (typeof data === 'string') {
// const result = {};
// if (data.indexOf('?') === 0) data = data.slice(1);
// const queries = data.split('&');
// for (let i = 0; i < queries.length; i++) {
// const [ name, value ] = queries[i].split('=');
// if (name !== undefined && value !== undefined) {
// if (name in result) {
// if (typeof result[name] === 'string') {
// result[name] = [ value ];
// } else {
// result[name].push(value);
// }
// } else {
// result[name] = value;
// }
// }
// }
// return result;
// } else {
// const result = [];
// for (const key in data) {
// const value = data[key];
// result.push(`${key}=${value}`);
// }
// return `?${result.join('&')}`;
// }
// }