UNPKG

ima

Version:

IMA.js framework for isomorphic javascript application

366 lines (294 loc) 8.86 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _AbstractRouter = require("./AbstractRouter"); var _AbstractRouter2 = _interopRequireDefault(_AbstractRouter); var _ActionTypes = require("./ActionTypes"); var _ActionTypes2 = _interopRequireDefault(_ActionTypes); var _RouteFactory = require("./RouteFactory"); var _RouteFactory2 = _interopRequireDefault(_RouteFactory); var _Dispatcher = require("../event/Dispatcher"); var _Dispatcher2 = _interopRequireDefault(_Dispatcher); var _PageManager = require("../page/manager/PageManager"); var _PageManager2 = _interopRequireDefault(_PageManager); var _Window = require("../window/Window"); var _Window2 = _interopRequireDefault(_Window); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // @client-side /** * Names of the DOM events the router responds to. * * @enum {string} * @type {Object<string, string>} */ const Events = Object.freeze({ /** * Name of the event produced when the user clicks the page using the * mouse, or touches the page and the touch event is not stopped. * * @const * @type {string} */ CLICK: 'click', /** * Name of the event fired when the user navigates back in the history. * * @const * @type {string} */ POP_STATE: 'popstate' }); /** * The number used as the index of the mouse left button in DOM * {@code MouseEvent}s. * * @const * @type {number} */ const MOUSE_LEFT_BUTTON = 0; /** * The client-side implementation of the {@codelink Router} interface. */ class ClientRouter extends _AbstractRouter2.default { static get $dependencies() { return [_PageManager2.default, _RouteFactory2.default, _Dispatcher2.default, _Window2.default]; } /** * Initializes the client-side router. * * @param {PageManager} pageManager The page manager handling UI rendering, * and transitions between pages if at the client side. * @param {RouteFactory} factory Factory for routes. * @param {Dispatcher} dispatcher Dispatcher fires events to app. * @param {Window} window The current global client-side APIs provider. */ constructor(pageManager, factory, dispatcher, window) { super(pageManager, factory, dispatcher); /** * Helper for accessing the native client-side APIs. * * @type {Window} */ this._window = window; } /** * @inheritdoc */ init(config) { super.init(config); this._host = config.$Host || this._window.getHost(); return this; } /** * @inheritdoc */ getUrl() { return this._window.getUrl(); } /** * @inheritdoc */ getPath() { return this._extractRoutePath(this._window.getPath()); } /** * @inheritdoc */ listen() { let nativeWindow = this._window.getWindow(); let eventName = Events.POP_STATE; this._window.bindEventListener(nativeWindow, eventName, event => { if (event.state && !event.defaultPrevented) { this.route(this.getPath(), {}, { type: _ActionTypes2.default.POP_STATE, event, url: this.getUrl() }); } }); this._window.bindEventListener(nativeWindow, Events.CLICK, event => { this._handleClick(event); }); return this; } /** * @inheritdoc */ redirect(url = '', options = {}, { type = _ActionTypes2.default.REDIRECT, event } = {}) { if (this._isSameDomain(url)) { let path = url.replace(this.getDomain(), ''); path = this._extractRoutePath(path); this.route(path, options, { type, event, url }); } else { this._window.redirect(url); } } /** * @inheritdoc */ route(path, options = {}, { event = null, type = _ActionTypes2.default.REDIRECT, url = null } = {}) { const action = { event, type, url: url || this.getUrl() }; return super.route(path, options, action).catch(error => { return this.handleError({ error }); }).catch(error => { this._handleFatalError(error); }); } /** * @inheritdoc */ handleError(params, options = {}) { if ($Debug) { console.error(params.error); } if (this.isClientError(params.error)) { return this.handleNotFound(params, options); } if (this.isRedirection(params.error)) { options.httpStatus = params.error.getHttpStatus(); this.redirect(params.error.getParams().url, options); return Promise.resolve({ content: null, status: options.httpStatus, error: params.error }); } return super.handleError(params, options).catch(error => { this._handleFatalError(error); }); } /** * @inheritdoc */ handleNotFound(params, options = {}) { return super.handleNotFound(params, options).catch(error => { return this.handleError({ error }); }); } /** * Handle a fatal error application state. IMA handle fatal error when IMA * handle error. * * @param {Error} error */ _handleFatalError(error) { if ($IMA && typeof $IMA.fatalErrorHandler === 'function') { $IMA.fatalErrorHandler(error); } else { if ($Debug) { console.warn('You must implement $IMA.fatalErrorHandler in ' + 'services.js'); } } } /** * Handles a click event. The method performs navigation to the target * location of the anchor (if it has one). * * The navigation will be handled by the router if the protocol and domain * of the anchor's target location (href) is the same as the current, * otherwise the method results in a hard redirect. * * @param {MouseEvent} event The click event. */ _handleClick(event) { let target = event.target || event.srcElement; let anchorElement = this._getAnchorElement(target); if (!anchorElement || typeof anchorElement.href !== 'string') { return; } let anchorHref = anchorElement.href; let isDefinedTargetHref = anchorHref !== undefined && anchorHref !== null; let isSetTarget = anchorElement.getAttribute('target') !== null; let isLeftButton = event.button === MOUSE_LEFT_BUTTON; let isCtrlPlusLeftButton = event.ctrlKey && isLeftButton; let isCMDPlusLeftButton = event.metaKey && isLeftButton; let isSameDomain = this._isSameDomain(anchorHref); let isHashLink = this._isHashLink(anchorHref); let isLinkPrevented = event.defaultPrevented; if (!isDefinedTargetHref || isSetTarget || !isLeftButton || !isSameDomain || isHashLink || isCtrlPlusLeftButton || isCMDPlusLeftButton || isLinkPrevented) { return; } event.preventDefault(); this.redirect(anchorHref, {}, { type: _ActionTypes2.default.CLICK, event, url: anchorHref }); } /** * The method determines whether an anchor element or a child of an anchor * element has been clicked, and if it was, the method returns anchor * element else null. * * @param {Node} target * @return {?Node} */ _getAnchorElement(target) { let self = this; while (target && !hasReachedAnchor(target)) { target = target.parentNode; } function hasReachedAnchor(nodeElement) { return nodeElement.parentNode && nodeElement !== self._window.getBody() && nodeElement.href !== undefined && nodeElement.href !== null; } return target; } /** * Tests whether the provided target URL contains only an update of the * hash fragment of the current URL. * * @param {string} targetUrl The target URL. * @return {boolean} {@code true} if the navigation to target URL would * result only in updating the hash fragment of the current URL. */ _isHashLink(targetUrl) { if (targetUrl.indexOf('#') === -1) { return false; } let currentUrl = this._window.getUrl(); let trimmedCurrentUrl = currentUrl.indexOf('#') === -1 ? currentUrl : currentUrl.substring(0, currentUrl.indexOf('#')); let trimmedTargetUrl = targetUrl.substring(0, targetUrl.indexOf('#')); return trimmedTargetUrl === trimmedCurrentUrl; } /** * Tests whether the the protocol and domain of the provided URL are the * same as the current. * * @param {string=} [url=''] The URL. * @return {boolean} {@code true} if the protocol and domain of the * provided URL are the same as the current. */ _isSameDomain(url = '') { return !!url.match(this.getBaseUrl()); } } exports.default = ClientRouter; typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/router/ClientRouter', [], function (_export, _context) { 'use strict'; return { setters: [], execute: function () { _export('default', exports.default); } }; });