UNPKG

ember-link

Version:

Link primitive to pass around self-contained route references

424 lines (380 loc) 12.6 kB
import { tracked } from '@glimmer/tracking'; import { addListener, removeListener } from '@ember/object/events'; import * as services from '@ember/service'; import services__default from '@ember/service'; import { macroCondition, isDevelopingApp, dependencySatisfies, importSync } from '@embroider/macros'; import { assert } from '@ember/debug'; import { action } from '@ember/object'; import { g, i, n } from 'decorator-transforms/runtime-esm'; function isQueryParams(maybeQueryParam) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return maybeQueryParam?.isQueryParams !== undefined && typeof maybeQueryParam.values === 'object'; } function freezeParams(params) { if (macroCondition(isDevelopingApp())) { if (params.models) Object.freeze(params.models); if (params.query) Object.freeze(params.query); return Object.freeze(params); } return params; } const BEHAVIOR = Symbol('prevent'); const MAIN_BUTTON = 0; function isUnmodifiedLeftClick(event) { return event.button === MAIN_BUTTON && !event.ctrlKey && !event.metaKey; } function isMouseEvent(event) { return typeof event === 'object' && event !== null && 'button' in event; } function isRegularOpening(event) { return isMouseEvent(event) && !isUnmodifiedLeftClick(event); } // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents function preventDefault(event) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (typeof event?.preventDefault === 'function') { event.preventDefault(); } } // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents function prevent(event) { if (isRegularOpening(event)) { return true; } preventDefault(event); return false; } const owner = macroCondition(dependencySatisfies('ember-source', '>=4.10')) ? importSync('@ember/owner') : importSync('@ember/application'); const { getOwner, setOwner } = owner; class Link { static { g(this.prototype, "_params", [tracked]); } #_params = (i(this, "_params"), void 0); _linkManager; constructor(linkManager, params) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion setOwner(this, getOwner(linkManager)); this._linkManager = linkManager; this._params = freezeParams(params); } get _routeArgs() { const { routeName, models, queryParams } = this; if (queryParams) { return [routeName, ...models, // Cloning `queryParams` is necessary, since we freeze it, but Ember // wants to mutate it. { queryParams: { ...queryParams } }]; } return [routeName, ...models]; } get behavior() { return { ...this._linkManager[BEHAVIOR], ...this._params.behavior }; } /** * Whether this route is currently active, including potentially supplied * models and query params. */ get isActive() { if (!this._linkManager.isRouterInitialized) return false; this._linkManager.currentTransitionStack; // eslint-disable-line @typescript-eslint/no-unused-expressions // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return this._linkManager.router.isActive(...this._routeArgs); } /** * Whether this route is currently active, including potentially supplied * models, but ignoring query params. */ get isActiveWithoutQueryParams() { if (!this._linkManager.isRouterInitialized) return false; this._linkManager.currentTransitionStack; // eslint-disable-line @typescript-eslint/no-unused-expressions return this._linkManager.router.isActive(this.routeName, // Unfortunately TypeScript is not clever enough to support "rest" // parameters in the middle. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ...this.models); } /** * Whether this route is currently active, but ignoring models and query * params. */ get isActiveWithoutModels() { if (!this._linkManager.isRouterInitialized) return false; this._linkManager.currentTransitionStack; // eslint-disable-line @typescript-eslint/no-unused-expressions return this._linkManager.router.isActive(this.routeName); } /** * Whether this route is currently being transitioned into / entered. */ get isEntering() { return this._isTransitioning('to'); } /** * Whether this route is currently being transitioned out of / exited. */ get isExiting() { return this._isTransitioning('from'); } /** * Whether this link leaves Ember */ get isExternal() { return this._params.isExternal ?? false; } /** * The URL for this link that you can pass to an `<a>` tag as the `href` * attribute. */ get url() { if (this.isExternal) { return this._params.route; } if (!this._linkManager.isRouterInitialized) return ''; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return this._linkManager.router.urlFor(...this._routeArgs); } /** * Alias for `url`. * * Allows for more ergonomic composition as query parameters. * * ```hbs * {{link "foo" query=(hash bar=(link "bar"))}} * ``` */ toString() { return this.url; } /** * The `RouteInfo` object for the target route. */ // get route(): RouteInfo { // return this._linkManager.router.recognize(this.url); // } /** * The target route name of this link. */ get routeName() { return this._params.route; } /** * The fully qualified target route name of this link. */ get qualifiedRouteName() { // Ignore `Property 'recognize' does not exist on type 'RouterService'.` // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const routeInfo = this._linkManager.router.recognize(this.url); return routeInfo.name; } /** * The route models passed in this link. */ get models() { return this._params.models ?? []; } /** * The query params for this link, if specified. */ get queryParams() { return this._params.query; } _isTransitioning(direction) { return this._linkManager.currentTransitionStack?.some(transition => { return transition[direction]?.name === this.qualifiedRouteName; }) ?? false; } // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents canOpen(event) { if (this.isExternal) { return false; } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.behavior.prevent?.(event, this)) { return false; } assert('You can only call `open`, when the router is initialized, e.g. when using `setupApplicationTest`.', this._linkManager.isRouterInitialized); return true; } /** * Transition into the target route. */ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents transitionTo(event) { if (!this.canOpen(event)) { return; } this._params.onTransitionTo?.(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return this._linkManager.router.transitionTo(...this._routeArgs); } /** * Transition into the target route while replacing the current URL, if * possible. */ static { n(this.prototype, "transitionTo", [action]); } // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents replaceWith(event) { if (!this.canOpen(event)) { return; } this._params.onReplaceWith?.(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return this._linkManager.router.replaceWith(...this._routeArgs); } static { n(this.prototype, "replaceWith", [action]); } // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents open(event) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const method = this.behavior.open ?? 'transition'; if (method === 'replace') { return this.replaceWith(event); } return this.transitionTo(event); } static { n(this.prototype, "open", [action]); } } // ember 3.28 has no exported `service` but `inject` // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-deprecated const service = services.service ?? services.inject; class LinkManagerService extends services__default { static { g(this.prototype, "internalCurrentTransitionStack", [tracked]); } #internalCurrentTransitionStack = (i(this, "internalCurrentTransitionStack"), void 0); static { g(this.prototype, "router", [service('router')]); } #router = (i(this, "router"), void 0); /** * The `RouterService` instance to be used by the generated `Link` instances. */ [BEHAVIOR] = { open: 'transition', prevent }; /** * Configure the default behavior of _all_ links. * * This can be overwritten at a particular link instance */ configureBehavior(behavior) { this[BEHAVIOR] = { ...this[BEHAVIOR], ...behavior }; } /** * Whether the router has been initialized. * This will be `false` in render tests. * * @see https://github.com/buschtoens/ember-link/issues/126 */ get isRouterInitialized() { // Ideally we would use the public `router` service here, but accessing // the `currentURL` on that service automatically starts the routing layer // as a side-effect, which is not our intention here. Once or if Ember.js // provides a flag on the `router` service to figure out if routing was // already initialized we should switch back to the public service instead. // // Inspiration for this workaround was taken from the `currentURL()` test // helper (see https://github.com/emberjs/ember-test-helpers/blob/v2.1.4/addon-test-support/@ember/test-helpers/setup-application-context.ts#L180) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line ember/no-private-routing-service return Boolean(getOwner(this).lookup('router:main').currentURL); } /** * The currently active `Transition` objects. */ get currentTransitionStack() { return this.internalCurrentTransitionStack; } /** * Creates a `Link` instance. */ createLink(linkParams) { return new Link(this, linkParams); } /** * Deserializes the `LinkParams` to be passed to `createLink` / `createUILink` * from a URL. * * If the URL cannot be recognized by the router, an error is thrown. */ getLinkParamsFromURL(url) { const routeInfo = this.router.recognize(url); return LinkManagerService.getLinkParamsFromRouteInfo(routeInfo); } /** * Converts a `RouteInfo` object into `LinkParams`. */ static getLinkParamsFromRouteInfo(routeInfo) { const models = []; let currentRoute = routeInfo; do { models.unshift( // eslint-disable-next-line @typescript-eslint/no-loop-func ...currentRoute.paramNames.map(name => currentRoute.params?.[name])); currentRoute = currentRoute.parent; } while (currentRoute.name !== 'application'); return { route: routeInfo.name, query: routeInfo.queryParams, models }; } constructor(owner) { super(owner); // Ignore `Argument of type '"routeWillChange"' is not assignable to parameter of type ...` // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore addListener(this.router, 'routeWillChange', this.handleRouteWillChange); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore addListener(this.router, 'routeDidChange', this.handleRouteDidChange); } willDestroy() { super.willDestroy(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore removeListener(this.router, 'routeWillChange', this.handleRouteWillChange); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore removeListener(this.router, 'routeDidChange', this.handleRouteDidChange); } handleRouteWillChange = transition => { this.internalCurrentTransitionStack = [...(this.internalCurrentTransitionStack ?? []), transition]; }; handleRouteDidChange = () => { this.internalCurrentTransitionStack = undefined; }; } export { Link as L, LinkManagerService as a, isQueryParams as i, prevent as p }; //# sourceMappingURL=link-manager-DRbwO37N.js.map