UNPKG

15 kBJavaScriptView Raw
1import Path from './path.js';
2import Loader from './loader.js';
3import Events from './events.js';
4import Utility from './utility.js';
5import Definer from './definer.js';
6import Component from './component.js';
7
8const Event = Object.create(Events);
9
10export 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 // ignore canceled events, modified clicks, and right clicks
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 // if shadow dom use
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 // check non-acceptables
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 // if external is true then default action
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};