1 | import Path from './path.js';
|
2 | import Loader from './loader.js';
|
3 | import Events from './events.js';
|
4 | import Utility from './utility.js';
|
5 | import Definer from './definer.js';
|
6 | import Component from './component.js';
|
7 |
|
8 | const Event = Object.create(Events);
|
9 |
|
10 | export default {
|
11 |
|
12 | on: Event.on.bind(Event),
|
13 | off: Event.off.bind(Event),
|
14 | emit: Event.emit.bind(Event),
|
15 |
|
16 | data: [],
|
17 | ran: false,
|
18 | location: {},
|
19 | mode: 'push',
|
20 | target: null,
|
21 | contain: false,
|
22 | folder: './routes',
|
23 |
|
24 | async setup (option) {
|
25 | option = option || {};
|
26 |
|
27 | this.base = option.base === undefined ? this.base : option.base;
|
28 | this.mode = option.mode === undefined ? this.mode : option.mode;
|
29 | this.after = option.after === undefined ? this.after : option.after;
|
30 | this.folder = option.folder === undefined ? this.folder : option.folder;
|
31 | this.before = option.before === undefined ? this.before : option.before;
|
32 | this.change = option.change === undefined ? this.change : option.change;
|
33 | this.target = option.target === undefined ? this.target : option.target;
|
34 | this.contain = option.contain === undefined ? this.contain : option.contain;
|
35 | this.external = option.external === undefined ? this.external : option.external;
|
36 |
|
37 | if (!this.target || typeof this.target === 'string') {
|
38 | this.target = document.body.querySelector(this.target || 'o-router');
|
39 | }
|
40 |
|
41 | if (this.mode !== 'href') {
|
42 | window.addEventListener('popstate', this.state.bind(this), true);
|
43 | window.document.addEventListener('click', this.click.bind(this), true);
|
44 | }
|
45 |
|
46 | Definer.define('o-router');
|
47 |
|
48 | await this.add(option.routes);
|
49 | await this.route(window.location.href, { mode: 'replace' });
|
50 | },
|
51 |
|
52 | compareParts (routePath, userPath, split) {
|
53 | const compareParts = [];
|
54 |
|
55 | const routeParts = routePath.split(split);
|
56 | const userParts = userPath.split(split);
|
57 |
|
58 | if (userParts.length > 1 && userParts[userParts.length - 1] === '') {
|
59 | userParts.pop();
|
60 | }
|
61 |
|
62 | if (routeParts.length > 1 && routeParts[routeParts.length - 1] === '') {
|
63 | routeParts.pop();
|
64 | }
|
65 |
|
66 | for (let i = 0, l = routeParts.length; i < l; i++) {
|
67 |
|
68 | if (routeParts[i].slice(0, 1) === '(' && routeParts[i].slice(-1) === ')') {
|
69 |
|
70 | if (routeParts[i] === '(~)') {
|
71 | return true;
|
72 | } else if (routeParts[i].indexOf('~') !== -1) {
|
73 | if (userParts[i]) {
|
74 | compareParts.push(userParts[i]);
|
75 | }
|
76 | } else {
|
77 | compareParts.push(userParts[i]);
|
78 | }
|
79 |
|
80 | } else if (routeParts[i] !== userParts[i]) {
|
81 | return false;
|
82 | } else {
|
83 | compareParts.push(routeParts[i]);
|
84 | }
|
85 |
|
86 | }
|
87 |
|
88 | if (compareParts.join(split) === userParts.join(split)) {
|
89 | return true;
|
90 | } else {
|
91 | return false;
|
92 | }
|
93 | },
|
94 |
|
95 | compare (routePath, userPath) {
|
96 | const base = Path.normalize(Path.base);
|
97 |
|
98 | userPath = Path.normalize(userPath);
|
99 | routePath = Path.normalize(routePath);
|
100 |
|
101 | if (userPath.slice(0, base.length) !== base) {
|
102 | userPath = Path.join(base, userPath);
|
103 | }
|
104 |
|
105 | if (routePath.slice(0, base.length) !== base) {
|
106 | routePath = Path.join(base, routePath);
|
107 | }
|
108 |
|
109 | if (this.compareParts(routePath, userPath, '/')) {
|
110 | return true;
|
111 | }
|
112 |
|
113 | if (this.compareParts(routePath, userPath, '-')) {
|
114 | return true;
|
115 | }
|
116 |
|
117 | return false;
|
118 | },
|
119 |
|
120 | toParameterObject (routePath, userPath) {
|
121 | let result = {};
|
122 |
|
123 | if (
|
124 | !routePath
|
125 | || !userPath
|
126 | || routePath === '/'
|
127 | || userPath === '/'
|
128 | ) return result;
|
129 |
|
130 | const userParts = userPath.split(/\/|-/);
|
131 | const routeParts = routePath.split(/\/|-/);
|
132 |
|
133 | for (let i = 0, l = routeParts.length; i < l; i++) {
|
134 | let part = routeParts[i];
|
135 |
|
136 | if (part.slice(0, 1) === '(' && part.slice(-1) === ')') {
|
137 | const name = part.slice(1, part.length - 1).replace('~', '');
|
138 | result[name] = userParts[i];
|
139 | }
|
140 |
|
141 | }
|
142 |
|
143 | return result;
|
144 | },
|
145 |
|
146 | toQueryString (data) {
|
147 | let result = '?';
|
148 |
|
149 | for (let key in data) {
|
150 | let value = data[key];
|
151 | result += key + '=' + value + '&';
|
152 | }
|
153 |
|
154 | if (result.slice(-1) === '&') {
|
155 | result = result.slice(0, -1);
|
156 | }
|
157 |
|
158 | return result;
|
159 | },
|
160 |
|
161 | toQueryObject (path) {
|
162 | let result = {};
|
163 |
|
164 | if (path.indexOf('?') === 0) path = path.slice(1);
|
165 | let queries = path.split('&');
|
166 |
|
167 | for (let i = 0, l = queries.length; i < l; i++) {
|
168 | let query = queries[i].split('=');
|
169 |
|
170 | if (query[0] && query[1]) {
|
171 | result[query[0]] = query[1];
|
172 | }
|
173 |
|
174 | }
|
175 |
|
176 | return result;
|
177 | },
|
178 |
|
179 | toLocationObject (href) {
|
180 | const location = {};
|
181 | const parser = document.createElement('a');
|
182 |
|
183 | parser.href = href;
|
184 |
|
185 | location.href = parser.href;
|
186 | location.host = parser.host;
|
187 | location.port = parser.port;
|
188 | location.hash = parser.hash;
|
189 | location.search = parser.search;
|
190 | location.protocol = parser.protocol;
|
191 | location.hostname = parser.hostname;
|
192 | location.pathname = parser.pathname[0] === '/' ? parser.pathname : '/' + parser.pathname;
|
193 |
|
194 | location.path = location.pathname + location.search + location.hash;
|
195 |
|
196 | return location;
|
197 | },
|
198 |
|
199 | scroll (x, y) {
|
200 | window.scroll(x, y);
|
201 | },
|
202 |
|
203 | back () {
|
204 | window.history.back();
|
205 | },
|
206 |
|
207 | forward () {
|
208 | window.history.forward();
|
209 | },
|
210 |
|
211 | redirect (path) {
|
212 | window.location.href = path;
|
213 | },
|
214 |
|
215 | async add (data) {
|
216 | if (!data) {
|
217 | return;
|
218 | } else if (data.constructor === String) {
|
219 | let path = data;
|
220 |
|
221 | if (path.slice(-3) === '.js') {
|
222 | path = path.slice(0, -3);
|
223 | }
|
224 |
|
225 | let load = path;
|
226 |
|
227 | if (path.slice(-5) === 'index') {
|
228 | path = path.slice(0, -5);
|
229 | }
|
230 |
|
231 | if (path.slice(-6) === 'index/') {
|
232 | path = path.slice(0, -6);
|
233 | }
|
234 |
|
235 | if (path.slice(0, 2) === './') {
|
236 | path = path.slice(2);
|
237 | }
|
238 |
|
239 | if (path.slice(0, 1) !== '/') {
|
240 | path = '/' + path;
|
241 | }
|
242 |
|
243 | load = load + '.js';
|
244 | load = Path.join(this.folder, load);
|
245 |
|
246 | this.data.push({ path, load });
|
247 | } else if (data.constructor === Object) {
|
248 |
|
249 | if (!data.path) {
|
250 | throw new Error('Oxe.router.add - route path required');
|
251 | }
|
252 |
|
253 | if (!data.name && !data.load && !data.component) {
|
254 | throw new Error('Oxe.router.add - route requires name, load, or component property');
|
255 | }
|
256 |
|
257 | this.data.push(data);
|
258 | } else if (data.constructor === Array) {
|
259 |
|
260 | for (let i = 0, l = data.length; i < l; i++) {
|
261 | await this.add(data[i]);
|
262 | }
|
263 |
|
264 | }
|
265 | },
|
266 |
|
267 | async load (route) {
|
268 |
|
269 | if (route.load) {
|
270 | const load = await Loader.load(route.load);
|
271 | route = Object.assign({}, load.default, route);
|
272 | }
|
273 |
|
274 | if (typeof route.component === 'string') {
|
275 | route.load = route.component;
|
276 | const load = await Loader.load(route.load);
|
277 | route.component = load.default;
|
278 | }
|
279 |
|
280 | return route;
|
281 | },
|
282 |
|
283 | async remove (path) {
|
284 | for (let i = 0, l = this.data.length; i < l; i++) {
|
285 | if (this.data[i].path === path) {
|
286 | this.data.splice(i, 1);
|
287 | }
|
288 | }
|
289 | },
|
290 |
|
291 | async get (path) {
|
292 | for (let i = 0, l = this.data.length; i < l; i++) {
|
293 | if (this.data[i].path === path) {
|
294 | this.data[i] = await this.load(this.data[i]);
|
295 | return this.data[i];
|
296 | }
|
297 | }
|
298 | },
|
299 |
|
300 | async filter (path) {
|
301 | const result = [];
|
302 |
|
303 | for (let i = 0, l = this.data.length; i < l; i++) {
|
304 | if (this.compare(this.data[i].path, path)) {
|
305 | this.data[i] = await this.load(this.data[i]);
|
306 | result.push(this.data[i]);
|
307 | }
|
308 | }
|
309 |
|
310 | return result;
|
311 | },
|
312 |
|
313 | async find (path) {
|
314 | for (let i = 0, l = this.data.length; i < l; i++) {
|
315 | if (this.compare(this.data[i].path, path)) {
|
316 | this.data[i] = await this.load(this.data[i]);
|
317 | return this.data[i];
|
318 | }
|
319 | }
|
320 | },
|
321 |
|
322 | async render (route) {
|
323 |
|
324 | if (!route) {
|
325 | throw new Error('Oxe.render - route argument required. Missing object option.');
|
326 | }
|
327 |
|
328 | if (route.title) {
|
329 | document.title = route.title;
|
330 | }
|
331 |
|
332 | const ensures = [];
|
333 |
|
334 | if (route.keywords) {
|
335 | ensures.push({
|
336 | name: 'meta',
|
337 | query: '[name="keywords"]',
|
338 | attributes: [
|
339 | { name: 'name', value: 'keywords' },
|
340 | { name: 'content', value: route.keywords }
|
341 | ]
|
342 | });
|
343 | }
|
344 |
|
345 | if (route.description) {
|
346 | ensures.push({
|
347 | name: 'meta',
|
348 | query: '[name="description"]',
|
349 | attributes: [
|
350 | { name: 'name', value: 'description' },
|
351 | { name: 'content', value: route.description }
|
352 | ]
|
353 | });
|
354 | }
|
355 |
|
356 | if (route.canonical) {
|
357 | ensures.push({
|
358 | name: 'link',
|
359 | query: '[rel="canonical"]',
|
360 | attributes: [
|
361 | { name: 'rel', value: 'canonical' },
|
362 | { name: 'href', value: route.canonical }
|
363 | ]
|
364 | });
|
365 | }
|
366 |
|
367 | if (ensures.length) {
|
368 | Promise.all(ensures.map(function (option) {
|
369 | return Promise.resolve().then(function () {
|
370 | option.position = 'afterbegin';
|
371 | option.scope = document.head;
|
372 | return Utility.ensureElement(option);
|
373 | });
|
374 | }));
|
375 | }
|
376 |
|
377 | if (!route.target) {
|
378 | if (!route.component) {
|
379 | Component.define(route);
|
380 | route.target = window.document.createElement(route.name);
|
381 | } else if (route.component.constructor === String) {
|
382 | route.target = window.document.createElement(route.component);
|
383 | } else if (route.component.constructor === Object) {
|
384 | Component.define(route.component);
|
385 | route.target = window.document.createElement(route.component.name);
|
386 | } else {
|
387 | throw new Error('Oxe.router.render - route requires name, load, or component property');
|
388 | }
|
389 | }
|
390 |
|
391 | if (this.target) {
|
392 | while (this.target.firstChild) {
|
393 | this.target.removeChild(this.target.firstChild);
|
394 | }
|
395 |
|
396 | this.target.appendChild(route.target);
|
397 | }
|
398 |
|
399 | this.scroll(0, 0);
|
400 | },
|
401 |
|
402 | async route (path, options) {
|
403 | options = options || {};
|
404 |
|
405 | if (options.query) {
|
406 | path += this.toQueryString(options.query);
|
407 | }
|
408 |
|
409 | const mode = options.mode || this.mode;
|
410 | const location = this.toLocationObject(path);
|
411 | const route = await this.find(location.pathname);
|
412 |
|
413 | if (!route) {
|
414 | throw new Error(`Oxe.router.route - missing route ${location.pathname}`);
|
415 | }
|
416 |
|
417 | location.route = route;
|
418 | location.title = location.route.title;
|
419 | location.query = this.toQueryObject(location.search);
|
420 | location.parameters = this.toParameterObject(location.route.path, location.pathname);
|
421 |
|
422 | if (location.route && location.route.handler) {
|
423 | return await location.route.handler(location);
|
424 | }
|
425 |
|
426 | if (location.route && location.route.redirect) {
|
427 | return await this.redirect(location.route.redirect);
|
428 | }
|
429 |
|
430 | if (typeof this.before === 'function') {
|
431 | await this.before(location);
|
432 | }
|
433 |
|
434 | this.emit('route:before', location);
|
435 |
|
436 | if (mode === 'href') {
|
437 | return window.location.assign(location.path);
|
438 | }
|
439 |
|
440 | window.history[mode + 'State']({ path: location.path }, '', location.path);
|
441 |
|
442 | this.location = location;
|
443 |
|
444 | await this.render(location.route);
|
445 |
|
446 | if (typeof this.after === 'function') {
|
447 | await this.after(location);
|
448 | }
|
449 |
|
450 | this.emit('route:after', location);
|
451 | },
|
452 |
|
453 | async state (event) {
|
454 | const path = event && event.state ? event.state.path : window.location.href;
|
455 | this.route(path, { mode: 'replace' });
|
456 | },
|
457 |
|
458 | async click (event) {
|
459 |
|
460 |
|
461 | if (
|
462 | event.target.type ||
|
463 | event.button !== 0 ||
|
464 | event.defaultPrevented ||
|
465 | event.altKey || event.ctrlKey || event.metaKey || event.shiftKey
|
466 | ) {
|
467 | return;
|
468 | }
|
469 |
|
470 |
|
471 | var target = event.path ? event.path[0] : event.target;
|
472 | var parent = target.parentElement;
|
473 |
|
474 | if (this.contain) {
|
475 |
|
476 | while (parent) {
|
477 |
|
478 | if (parent.nodeName === 'O-ROUTER') {
|
479 | break;
|
480 | } else {
|
481 | parent = parent.parentElement;
|
482 | }
|
483 |
|
484 | }
|
485 |
|
486 | if (parent.nodeName !== 'O-ROUTER') {
|
487 | return;
|
488 | }
|
489 |
|
490 | }
|
491 |
|
492 | while (target && 'A' !== target.nodeName) {
|
493 | target = target.parentElement;
|
494 | }
|
495 |
|
496 | if (!target || 'A' !== target.nodeName) {
|
497 | return;
|
498 | }
|
499 |
|
500 |
|
501 | if (target.hasAttribute('download') ||
|
502 | target.hasAttribute('external') ||
|
503 | target.hasAttribute('o-external') ||
|
504 | target.href.indexOf('tel:') === 0 ||
|
505 | target.href.indexOf('ftp:') === 0 ||
|
506 | target.href.indexOf('file:') === 0 ||
|
507 | target.href.indexOf('mailto:') === 0 ||
|
508 | target.href.indexOf(window.location.origin) !== 0 ||
|
509 | (target.hash !== '' &&
|
510 | target.origin === window.location.origin &&
|
511 | target.pathname === window.location.pathname)
|
512 | ) return;
|
513 |
|
514 |
|
515 | if (this.external &&
|
516 | (this.external.constructor === RegExp && this.external.test(target.href) ||
|
517 | this.external.constructor === Function && this.external(target.href) ||
|
518 | this.external.constructor === String && this.external === target.href)
|
519 | ) return;
|
520 |
|
521 | event.preventDefault();
|
522 |
|
523 | if (this.location.href !== target.href) {
|
524 | this.route(target.href);
|
525 | }
|
526 |
|
527 | }
|
528 |
|
529 | };
|