UNPKG

9.46 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _AbstractRouter = require('./AbstractRouter');
8
9var _AbstractRouter2 = _interopRequireDefault(_AbstractRouter);
10
11var _RouteFactory = require('./RouteFactory');
12
13var _RouteFactory2 = _interopRequireDefault(_RouteFactory);
14
15var _Dispatcher = require('../event/Dispatcher');
16
17var _Dispatcher2 = _interopRequireDefault(_Dispatcher);
18
19var _PageManager = require('../page/manager/PageManager');
20
21var _PageManager2 = _interopRequireDefault(_PageManager);
22
23var _Window = require('../window/Window');
24
25var _Window2 = _interopRequireDefault(_Window);
26
27function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28
29/**
30 * Names of the DOM events the router responds to.
31 *
32 * @enum {string}
33 * @type {Object<string, string>}
34 */
35const Events = Object.freeze({
36 /**
37 * Name of the event produced when the user clicks the page using the
38 * mouse, or touches the page and the touch event is not stopped.
39 *
40 * @const
41 * @type {string}
42 */
43 CLICK: 'click',
44
45 /**
46 * Name of the event fired when the user navigates back in the history.
47 *
48 * @const
49 * @type {string}
50 */
51 POP_STATE: 'popstate'
52});
53
54/**
55 * The number used as the index of the mouse left button in DOM
56 * {@code MouseEvent}s.
57 *
58 * @const
59 * @type {number}
60 */
61// @client-side
62
63const MOUSE_LEFT_BUTTON = 0;
64
65/**
66 * The client-side implementation of the {@codelink Router} interface.
67 */
68class ClientRouter extends _AbstractRouter2.default {
69 static get $dependencies() {
70 return [_PageManager2.default, _RouteFactory2.default, _Dispatcher2.default, _Window2.default];
71 }
72
73 /**
74 * Initializes the client-side router.
75 *
76 * @param {PageManager} pageManager The page manager handling UI rendering,
77 * and transitions between pages if at the client side.
78 * @param {RouteFactory} factory Factory for routes.
79 * @param {Dispatcher} dispatcher Dispatcher fires events to app.
80 * @param {Window} window The current global client-side APIs provider.
81 */
82 constructor(pageManager, factory, dispatcher, window) {
83 super(pageManager, factory, dispatcher);
84
85 /**
86 * Helper for accessing the native client-side APIs.
87 *
88 * @type {Window}
89 */
90 this._window = window;
91 }
92
93 /**
94 * @inheritdoc
95 */
96 init(config) {
97 super.init(config);
98 this._host = config.$Host || this._window.getHost();
99
100 return this;
101 }
102
103 /**
104 * @inheritdoc
105 */
106 getUrl() {
107 return this._window.getUrl();
108 }
109
110 /**
111 * @inheritdoc
112 */
113 getPath() {
114 return this._extractRoutePath(this._window.getPath());
115 }
116
117 /**
118 * @inheritdoc
119 */
120 listen() {
121 let nativeWindow = this._window.getWindow();
122
123 this._saveScrollHistory();
124 let eventName = Events.POP_STATE;
125 this._window.bindEventListener(nativeWindow, eventName, event => {
126 if (event.state && !event.defaultPrevented) {
127 this.route(this.getPath()).then(() => {
128 let scroll = event.state.scroll;
129
130 if (scroll) {
131 this._pageManager.scrollTo(scroll.x, scroll.y);
132 }
133 });
134 }
135 });
136
137 this._window.bindEventListener(nativeWindow, Events.CLICK, event => {
138 this._handleClick(event);
139 });
140
141 return this;
142 }
143
144 /**
145 * @inheritdoc
146 */
147 redirect(url = '', options = {}) {
148 if (this._isSameDomain(url)) {
149 let path = url.replace(this.getDomain(), '');
150 path = this._extractRoutePath(path);
151
152 this._saveScrollHistory();
153 this._setAddressBar(url);
154 this.route(path, options);
155 } else {
156 this._window.redirect(url);
157 }
158 }
159
160 /**
161 * @inheritdoc
162 */
163 route(path, options = {}) {
164 return super.route(path, options).catch(error => {
165 return this.handleError({ error });
166 }).catch(error => {
167 this._handleFatalError(error);
168 });
169 }
170
171 /**
172 * @inheritdoc
173 */
174 handleError(params, options = {}) {
175 if ($Debug) {
176 console.error(params.error);
177 }
178
179 if (this.isClientError(params.error)) {
180 return this.handleNotFound(params, options);
181 }
182
183 if (this.isRedirection(params.error)) {
184 options.httpStatus = params.error.getHttpStatus();
185 this.redirect(params.error.getParams().url, options);
186 return Promise.resolve({
187 content: null,
188 status: options.httpStatus,
189 error: params.error
190 });
191 }
192
193 return super.handleError(params, options).catch(error => {
194 this._handleFatalError(error);
195 });
196 }
197
198 /**
199 * @inheritdoc
200 */
201 handleNotFound(params, options = {}) {
202 return super.handleNotFound(params, options).catch(error => {
203 return this.handleError({ error });
204 });
205 }
206
207 /**
208 * Handle a fatal error application state. IMA handle fatal error when IMA
209 * handle error.
210 *
211 * @param {Error} error
212 */
213 _handleFatalError(error) {
214 if ($IMA && typeof $IMA.fatalErrorHandler === 'function') {
215 $IMA.fatalErrorHandler(error);
216 } else {
217 if ($Debug) {
218 console.warn('You must implement $IMA.fatalErrorHandler in ' + 'services.js');
219 }
220 }
221 }
222
223 /**
224 * Handles a click event. The method performs navigation to the target
225 * location of the anchor (if it has one).
226 *
227 * The navigation will be handled by the router if the protocol and domain
228 * of the anchor's target location (href) is the same as the current,
229 * otherwise the method results in a hard redirect.
230 *
231 * @param {MouseEvent} event The click event.
232 */
233 _handleClick(event) {
234 let target = event.target || event.srcElement;
235 let anchorElement = this._getAnchorElement(target);
236
237 if (!anchorElement || typeof anchorElement.href !== 'string') {
238 return;
239 }
240
241 let anchorHref = anchorElement.href;
242 let isDefinedTargetHref = anchorHref !== undefined && anchorHref !== null;
243 let isSetTarget = anchorElement.getAttribute('target') !== null;
244 let isLeftButton = event.button === MOUSE_LEFT_BUTTON;
245 let isCtrlPlusLeftButton = event.ctrlKey && isLeftButton;
246 let isCMDPlusLeftButton = event.metaKey && isLeftButton;
247 let isSameDomain = this._isSameDomain(anchorHref);
248 let isHashLink = this._isHashLink(anchorHref);
249 let isLinkPrevented = event.defaultPrevented;
250
251 if (!isDefinedTargetHref || isSetTarget || !isLeftButton || !isSameDomain || isHashLink || isCtrlPlusLeftButton || isCMDPlusLeftButton || isLinkPrevented) {
252 return;
253 }
254
255 event.preventDefault();
256 this.redirect(anchorHref);
257 }
258
259 /**
260 * The method determines whether an anchor element or a child of an anchor
261 * element has been clicked, and if it was, the method returns anchor
262 * element else null.
263 *
264 * @param {Node} target
265 * @return {?Node}
266 */
267 _getAnchorElement(target) {
268 let self = this;
269
270 while (target && !hasReachedAnchor(target)) {
271 target = target.parentNode;
272 }
273
274 function hasReachedAnchor(nodeElement) {
275 return nodeElement.parentNode && nodeElement !== self._window.getBody() && nodeElement.href !== undefined && nodeElement.href !== null;
276 }
277
278 return target;
279 }
280
281 /**
282 * Tests whether the provided target URL contains only an update of the
283 * hash fragment of the current URL.
284 *
285 * @param {string} targetUrl The target URL.
286 * @return {boolean} {@code true} if the navigation to target URL would
287 * result only in updating the hash fragment of the current URL.
288 */
289 _isHashLink(targetUrl) {
290 if (targetUrl.indexOf('#') === -1) {
291 return false;
292 }
293
294 let currentUrl = this._window.getUrl();
295 let trimmedCurrentUrl = currentUrl.indexOf('#') === -1 ? currentUrl : currentUrl.substring(0, currentUrl.indexOf('#'));
296 let trimmedTargetUrl = targetUrl.substring(0, targetUrl.indexOf('#'));
297
298 return trimmedTargetUrl === trimmedCurrentUrl;
299 }
300
301 /**
302 * Sets the provided URL to the browser's address bar by pushing a new
303 * state to the history.
304 *
305 * The state object pushed to the history will be an object with the
306 * following structure: {@code {url: string}}. The {@code url} field will
307 * be set to the provided URL.
308 *
309 * @param {string} url The URL.
310 */
311 _setAddressBar(url) {
312 let scroll = {
313 x: 0,
314 y: 0
315 };
316 let state = { url, scroll };
317
318 this._window.pushState(state, null, url);
319 }
320
321 /**
322 * Save user's scroll state to history.
323 *
324 * Replace scroll values in current state for actual scroll values in
325 * document.
326 */
327 _saveScrollHistory() {
328 let url = this.getUrl();
329 let scroll = {
330 x: this._window.getScrollX(),
331 y: this._window.getScrollY()
332 };
333 let state = { url, scroll };
334
335 let oldState = this._window.getHistoryState();
336 let newState = Object.assign({}, oldState, state);
337
338 this._window.replaceState(newState, null, url);
339 }
340
341 /**
342 * Tests whether the the protocol and domain of the provided URL are the
343 * same as the current.
344 *
345 * @param {string=} [url=''] The URL.
346 * @return {boolean} {@code true} if the protocol and domain of the
347 * provided URL are the same as the current.
348 */
349 _isSameDomain(url = '') {
350 return !!url.match(this.getBaseUrl());
351 }
352}
353exports.default = ClientRouter;
354
355typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/router/ClientRouter', [], function (_export, _context) {
356 'use strict';
357 return {
358 setters: [],
359 execute: function () {
360 _export('default', exports.default);
361 }
362 };
363});