1 | import { global, runtime } from './contexts';
|
2 | import crossroads from 'crossroads';
|
3 | import React from 'react';
|
4 |
|
5 | let _stateRouteMapping = {};
|
6 |
|
7 | class UrlDefinition {
|
8 | constructor(state, controller) {
|
9 | this._state = state;
|
10 | this._controller = controller;
|
11 | }
|
12 |
|
13 | get state() {
|
14 | return this._state;
|
15 | }
|
16 |
|
17 | get controller() {
|
18 | return this._controller;
|
19 | }
|
20 | }
|
21 |
|
22 | class IncludeDefinition {
|
23 | constructor(router) {
|
24 | this._router = router;
|
25 | }
|
26 |
|
27 | get router() {
|
28 | return this._router;
|
29 | }
|
30 | }
|
31 |
|
32 | export function url(state, controller) {
|
33 | return new UrlDefinition(state, controller);
|
34 | }
|
35 |
|
36 | export function include(router) {
|
37 | return new IncludeDefinition(router);
|
38 | }
|
39 |
|
40 | export class RouteUtils {
|
41 | static listen(routerClass) {
|
42 | new routerClass();
|
43 | if (runtime.isClient) {
|
44 | RouteUtils._listenClient();
|
45 | } else {
|
46 | RouteUtils._listenServer();
|
47 | }
|
48 | }
|
49 |
|
50 | static _listenServer() {
|
51 | var http = require('http');
|
52 | var urlModule = require('url');
|
53 | crossroads.ignoreState = true;
|
54 | crossroads.bypassed.add((req, res) => {
|
55 | res.writeHead(404, {'Content-Type': 'text/html'});
|
56 | res.end('<html><body><h1>HTTP 404 - Page Not Found</h1><hr/><p>OutlineJS Server</p></body></html>');
|
57 | });
|
58 | http.createServer((req, res) => {
|
59 | var requestedUrl = urlModule.parse(req.url).pathname;
|
60 | RouteUtils.parseUrl(requestedUrl, req, res);
|
61 | }).listen(1337, '0.0.0.0');
|
62 | }
|
63 |
|
64 | static _listenClient() {
|
65 | require('html5-history-api');
|
66 | var location = window.history.location || window.location;
|
67 | var eventDef = window.addEventListener ? ['addEventListener', ''] : ['attachEvent', 'on'];
|
68 | window[eventDef[0]](`${eventDef[1]}popstate`, () => {
|
69 | RouteUtils.parseUrl(location.pathname);
|
70 | }, false);
|
71 | RouteUtils.parseUrl(location.pathname);
|
72 | }
|
73 |
|
74 | static parseUrl(path, req, res) {
|
75 | crossroads.parse(path, [req, res]);
|
76 | }
|
77 |
|
78 | static reverse(state, params = {}) {
|
79 | var url = _stateRouteMapping[state].interpolate(params);
|
80 | return `/${url}`;
|
81 | }
|
82 |
|
83 | static navigate(state, params = {}) {
|
84 | var url = RouteUtils.reverse(state, params);
|
85 | if (runtime.isClient) {
|
86 | var history = require('html5-history-api');
|
87 | history.pushState(null, null, url);
|
88 | RouteUtils.parseUrl(url);
|
89 | }
|
90 | }
|
91 |
|
92 | static query() {
|
93 |
|
94 | var href = window.location.href;
|
95 | var result = {};
|
96 | if (href.includes('#')) {
|
97 | href = href.split('#')[1];
|
98 | }
|
99 | href = href.split('?')[1];
|
100 | if (href) {
|
101 | for (let param of href.split('&')) {
|
102 | let [k, v] = param.split('=');
|
103 | result[decodeURIComponent(k)] = decodeURIComponent(v);
|
104 | }
|
105 | }
|
106 | return result;
|
107 | }
|
108 |
|
109 | static isState(state, className = 'active') {
|
110 | if (global.state.indexOf(state) === 0) {
|
111 | return className;
|
112 | }
|
113 | }
|
114 | }
|
115 |
|
116 | export class BaseRouter {
|
117 | constructor(prefix = '') {
|
118 | if (prefix !== '') {
|
119 | prefix = `${prefix}/`;
|
120 | }
|
121 |
|
122 | for (let rt of Object.keys(this.urlPatterns)) {
|
123 | let rtObj = this.urlPatterns[rt];
|
124 | if (rt !== '') {
|
125 | rt = `${rt}/`;
|
126 | }
|
127 | if (rtObj instanceof IncludeDefinition) {
|
128 |
|
129 | new rtObj.router(`${prefix}${rt}`);
|
130 | } else {
|
131 | var rUrl = `${prefix}${rt}`;
|
132 | _stateRouteMapping[rtObj.state] = crossroads.addRoute(rUrl, (...args) => {
|
133 | this.routeTo(rtObj, ...args);
|
134 | });
|
135 | }
|
136 | }
|
137 | }
|
138 |
|
139 | routeTo(urlDef, req, res, ...args) {
|
140 | var Controller = urlDef.controller;
|
141 | var midPromises = [];
|
142 | for (var mid of runtime.middleware) {
|
143 | if (mid.preControllerInit) {
|
144 | midPromises.push(mid.preControllerInit());
|
145 | }
|
146 | }
|
147 | Promise.all(midPromises).then(() => {
|
148 | global.state = urlDef.state;
|
149 | let controller = new Controller(req, res);
|
150 | if (runtime.isClient) {
|
151 | controller.reconcileWithServer();
|
152 | }
|
153 | controller.init(...args);
|
154 | }, (error) => {
|
155 | if (error) {
|
156 | console.log(error);
|
157 | }
|
158 | });
|
159 | }
|
160 |
|
161 | get urlPatterns() {
|
162 | throw 'NotImplemented';
|
163 | }
|
164 | }
|
165 |
|
166 | export class Link extends React.Component {
|
167 | static isLeftClickEvent(event) {
|
168 | return event.button === 0;
|
169 | }
|
170 |
|
171 | static isModifiedEvent(event) {
|
172 | return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
|
173 | }
|
174 |
|
175 | handleClick(event) {
|
176 | let allowTransition = true;
|
177 |
|
178 | if (this.props.onClick) {
|
179 | this.props.onClick(event);
|
180 | }
|
181 |
|
182 | if (Link.isModifiedEvent(event) || !Link.isLeftClickEvent(event)) {
|
183 | return;
|
184 | }
|
185 |
|
186 | if (event.defaultPrevented === true) {
|
187 | allowTransition = false;
|
188 | }
|
189 |
|
190 |
|
191 | if (this.props.target) {
|
192 | if (!allowTransition) {
|
193 | event.preventDefault();
|
194 | }
|
195 | return;
|
196 | }
|
197 |
|
198 | event.preventDefault();
|
199 |
|
200 | if (allowTransition) {
|
201 | const { state, params } = this.props;
|
202 | RouteUtils.navigate(state, params);
|
203 | }
|
204 | }
|
205 |
|
206 | render() {
|
207 | var props = {};
|
208 | props.href = RouteUtils.reverse(this.props.state, this.props.params);
|
209 | if (this.props.activeClassName) {
|
210 | if (RouteUtils.isState(this.props.state)) {
|
211 | props.className = this.props.className === '' ? this.props.activeClassName : `${this.props.className} ${this.props.activeClassName}`;
|
212 | }
|
213 | }
|
214 | props.children = this.props.children;
|
215 | return <a {...props} onClick={this.handleClick.bind(this)} />;
|
216 | }
|
217 | }
|
218 |
|
\ | No newline at end of file |