All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.router.url.url-service.js Maven / Gradle / Ivy

import { isFunction, isDefined, isObject, isString } from "../../shared/utils";
import { is, pattern } from "../../shared/hof";
import { UrlRules } from "./url-rules";
import { TargetState } from "../state/target-state";
import { removeFrom } from "../../shared/common";
import { stripLastPathElement } from "../../shared/strings";
import { UrlMatcher } from "./url-matcher";
import { ParamFactory } from "../params/param-factory";
import { UrlRuleFactory } from "./url-rule";

/**
 * API for URL management
 */
export class UrlService {
  static $inject = [
    "$locationProvider",
    "$stateProvider",
    "$routerGlobalsProvider",
    "$urlConfigProvider",
  ];

  /**
   * @param {import("../../core/location/location").LocationProvider} $locationProvider
   */
  constructor($locationProvider, stateService, globals, urlConfigProvider) {
    this.stateService = stateService;
    this.stateService.urlService = this; // circular wiring
    this.$locationProvider = $locationProvider;

    this.$location = undefined;
    this.$browser = undefined;

    /** @type {boolean} */
    this.interceptDeferred = false;

    /** Provides services related to the URL */
    this.urlRuleFactory = new UrlRuleFactory(this, this.stateService, globals);

    /**
     * The nested [[UrlRules]] API for managing URL rules and rewrites
     *
     * See: [[UrlRules]] for details
     * @type {UrlRules}
     */
    this.rules = new UrlRules(this.urlRuleFactory);
    /**
     * The nested [[UrlConfig]] API to configure the URL and retrieve URL information
     *
     * See: [[UrlConfig]] for details
     * @type {import("./url-config").UrlConfigProvider}
     */
    this.config = urlConfigProvider;

    /** Creates a new [[Param]] for a given location (DefType) */
    this.paramFactory = new ParamFactory(this.config);

    /**
     * Gets the path part of the current url
     *
     * If the current URL is `/some/path?query=value#anchor`, this returns `/some/path`
     *
     * @return the path portion of the url
     */
    this.path = () => this.$location.path();
    /**
     * Gets the search part of the current url as an object
     *
     * If the current URL is `/some/path?query=value#anchor`, this returns `{ query: 'value' }`
     *
     * @return the search (query) portion of the url, as an object
     */
    this.search = () => this.$location.search();
    /**
     * Gets the hash part of the current url
     *
     * If the current URL is `/some/path?query=value#anchor`, this returns `anchor`
     *
     * @return the hash (anchor) portion of the url
     */
    this.hash = () => this.$location.hash();

    this._urlListeners = [];
  }

  $get = [
    "$location",
    "$browser",
    "$rootScope",
    /**
     *
     * @param {import('../../core/location/location').Location} $location
     * @param {import('../../services/browser').Browser} $browser
     * @param {import('../../core/scope/scope').Scope} $rootScope
     * @returns
     */
    ($location, $browser, $rootScope) => {
      this.$location = $location;
      this.$browser = $browser;
      $rootScope.$on("$locationChangeSuccess", (evt) =>
        this._urlListeners.forEach((fn) => fn(evt)),
      );
      this.listen();
      return this;
    },
  ];

  html5Mode() {
    let html5Mode = this.$locationProvider.html5Mode();
    html5Mode = isObject(html5Mode) ? html5Mode.enabled : html5Mode;
    return html5Mode && typeof history !== "undefined";
  }

  baseHref() {
    return (
      this._baseHref ||
      (this._baseHref = this.$browser.baseHref() || window.location.pathname)
    );
  }

  /**
   * Gets the current url, or updates the url
   *
   * ### Getting the current URL
   *
   * When no arguments are passed, returns the current URL.
   * The URL is normalized using the internal [[path]]/[[search]]/[[hash]] values.
   *
   * For example, the URL may be stored in the hash ([[HashLocationServices]]) or
   * have a base HREF prepended ([[PushStateLocationServices]]).
   *
   * The raw URL in the browser might be:
   *
   * ```
   * http://mysite.com/somepath/index.html#/internal/path/123?param1=foo#anchor
   * ```
   *
   * or
   *
   * ```
   * http://mysite.com/basepath/internal/path/123?param1=foo#anchor
   * ```
   *
   * then this method returns:
   *
   * ```
   * /internal/path/123?param1=foo#anchor
   * ```
   *
   *
   * #### Example:
   * ```js
   * locationServices.url(); // "/some/path?query=value#anchor"
   * ```
   *
   * ### Updating the URL
   *
   * When `newurl` arguments is provided, changes the URL to reflect `newurl`
   *
   * #### Example:
   * ```js
   * locationServices.url("/some/path?query=value#anchor", true);
   * ```
   *
   * @param {string} [newUrl] The new value for the URL.
   *               This url should reflect only the new internal [[path]], [[search]], and [[hash]] values.
   *               It should not include the protocol, site, port, or base path of an absolute HREF.
   * @param {boolean} [replace] When true, replaces the current history entry (instead of appending it) with this new url
   * @param {any} [state] The history's state object, i.e., pushState (if the LocationServices implementation supports it)
   *
   * @return the url (after potentially being processed)
   */
  url(newUrl, replace = false, state) {
    if (isDefined(newUrl)) this.$location.url(newUrl);
    if (replace) this.$location.replace();
    if (state) this.$location.state(state);
    return this.$location.url();
  }

  /**
   * @internal
   *
   * Registers a low level url change handler
   *
   * Note: Because this is a low level handler, it's not recommended for general use.
   *
   * #### Example:
   * ```js
   * let deregisterFn = locationServices.onChange((evt) => console.log("url change", evt));
   * ```
   *
   * @param callback a function that will be called when the url is changing
   * @return a function that de-registers the callback
   */
  onChange(callback) {
    this._urlListeners.push(callback);
    return () => removeFrom(this._urlListeners)(callback);
  }

  /**
   * Gets the current URL parts
   *
   * This method returns the different parts of the current URL (the [[path]], [[search]], and [[hash]]) as a [[UrlParts]] object.
   */
  parts() {
    return { path: this.path(), search: this.search(), hash: this.hash() };
  }
  /**
   * Activates the best rule for the current URL
   *
   * Checks the current URL for a matching [[UrlRule]], then invokes that rule's handler.
   * This method is called internally any time the URL has changed.
   *
   * This effectively activates the state (or redirect, etc) which matches the current URL.
   *
   * #### Example:
   * ```js
   * urlService.deferIntercept();
   *
   * fetch('/states.json').then(resp => resp.json()).then(data => {
   *   data.forEach(state => $stateRegistry.register(state));
   *   urlService.listen();
   *   // Find the matching URL and invoke the handler.
   *   urlService.sync();
   * });
   * ```
   */
  sync(evt) {
    if (evt && evt.defaultPrevented) return;
    const stateService = this.stateService;
    const url = {
      path: this.path(),
      search: this.search(),
      hash: this.hash(),
    };
    /**
     * @type {*}
     */
    const best = this.match(url);
    const applyResult = pattern([
      [isString, (newurl) => this.url(newurl, true)],
      [
        TargetState.isDef,
        (def) => stateService.go(def.state, def.params, def.options),
      ],
      [
        is(TargetState),
        (target) =>
          stateService.go(target.state(), target.params(), target.options()),
      ],
    ]);

    applyResult(best && best.rule.handler(best.match, url));
  }
  /**
   * Starts or stops listening for URL changes
   *
   * Call this sometime after calling [[deferIntercept]] to start monitoring the url.
   * This causes ng-router to start listening for changes to the URL, if it wasn't already listening.
   *
   * If called with `false`, ng-router will stop listening (call listen(true) to start listening again).
   *
   * #### Example:
   * ```js
   * urlService.deferIntercept();
   *
   * fetch('/states.json').then(resp => resp.json()).then(data => {
   *   data.forEach(state => $stateRegistry.register(state));
   *   // Start responding to URL changes
   *   urlService.listen();
   *   urlService.sync();
   * });
   * ```
   *
   * @param enabled `true` or `false` to start or stop listening to URL changes
   */
  listen(enabled) {
    if (enabled === false) {
      this._stopListeningFn && this._stopListeningFn();
      delete this._stopListeningFn;
    } else {
      return (this._stopListeningFn =
        this._stopListeningFn || this.onChange((evt) => this.sync(evt)));
    }
  }
  /**
   * Disables monitoring of the URL.
   *
   * Call this method before ng-router has bootstrapped.
   * It will stop ng-router from performing the initial url sync.
   *
   * This can be useful to perform some asynchronous initialization before the router starts.
   * Once the initialization is complete, call [[listen]] to tell ng-router to start watching and synchronizing the URL.
   *
   * #### Example:
   * ```js
   * // Prevent ng-router from automatically intercepting URL changes when it starts;
   * urlService.deferIntercept();
   *
   * fetch('/states.json').then(resp => resp.json()).then(data => {
   *   data.forEach(state => $stateRegistry.register(state));
   *   urlService.listen();
   *   urlService.sync();
   * });
   * ```
   *
   * @param defer Indicates whether to defer location change interception.
   *        Passing no parameter is equivalent to `true`.
   */
  deferIntercept(defer) {
    if (defer === undefined) defer = true;
    this.interceptDeferred = defer;
  }
  /**
   * Matches a URL
   *
   * Given a URL (as a [[UrlParts]] object), check all rules and determine the best matching rule.
   * Return the result as a [[MatchResult]].
   * @returns {any}
   */
  match(url) {
    url = Object.assign({ path: "", search: {}, hash: "" }, url);
    const rules = this.rules.rules();
    // Checks a single rule. Returns { rule: rule, match: match, weight: weight } if it matched, or undefined
    /**
     *
     * @param {import("./url-rule").BaseUrlRule} rule
     */
    const checkRule = (rule) => {
      const match = rule.match(url);
      return match && { match, rule, weight: rule.matchPriority(match) };
    };
    // The rules are pre-sorted.
    // - Find the first matching rule.
    // - Find any other matching rule that sorted *exactly the same*, according to `.sort()`.
    // - Choose the rule with the highest match weight.
    let best;
    for (let i = 0; i < rules.length; i++) {
      // Stop when there is a 'best' rule and the next rule sorts differently than it.
      if (best && best.rule._group !== rules[i]._group) break;
      const current = checkRule(rules[i]);
      // Pick the best MatchResult
      best =
        !best || (current && current.weight > best.weight) ? current : best;
    }
    return best;
  }

  update(read) {
    if (read) {
      this.location = this.url();
      return;
    }
    if (this.url() === this.location) return;
    this.url(/** @type {string} */ (this.location), true);
  }

  /**
   * Internal API.
   *
   * Pushes a new location to the browser history.
   *
   * @internal
   * @param urlMatcher
   * @param params
   * @param options
   */
  push(urlMatcher, params, options) {
    const replace = options && !!options.replace;
    this.url(urlMatcher.format(params || {}), replace);
  }

  /**
   * Builds and returns a URL with interpolated parameters
   *
   * #### Example:
   * ```js
   * matcher = $umf.compile("/about/:person");
   * params = { person: "bob" };
   * $bob = $urlService.href(matcher, params);
   * // $bob == "/about/bob";
   * ```
   *
   * @param urlMatcher The [[UrlMatcher]] object which is used as the template of the URL to generate.
   * @param params An object of parameter values to fill the matcher's required parameters.
   * @param options Options object. The options are:
   *
   * - **`absolute`** - {boolean=false},  If true will generate an absolute url, e.g. "http://www.example.com/fullurl".
   *
   * @returns Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher`
   */
  href(urlMatcher, params, options) {
    let url = urlMatcher.format(params);
    if (url == null) return null;
    options = options || { absolute: false };
    const isHtml5 = this.html5Mode();
    if (!isHtml5 && url !== null) {
      url = "#" + this.$locationProvider.hashPrefix() + url;
    }
    url = appendBasePath(url, isHtml5, options.absolute, this.baseHref());
    if (!options.absolute || !url) {
      return url;
    }
    const slash = !isHtml5 && url ? "/" : "";
    const cfgPort = this.$location.port();
    const port = cfgPort === 80 || cfgPort === 443 ? "" : ":" + cfgPort;
    return [
      this.$location.protocol(),
      "://",
      this.$location.host(),
      port,
      slash,
      url,
    ].join("");
  }

  /**
   * Creates a [[UrlMatcher]] for the specified pattern.
   *
   * @param pattern  The URL pattern.
   * @param config  The config object hash.
   * @returns The UrlMatcher.
   */
  compile(pattern, config) {
    const urlConfig = this.config;
    // backward-compatible support for config.params -> config.state.params
    const params = config && !config.state && config.params;
    config = params ? Object.assign({ state: { params } }, config) : config;
    const globalConfig = {
      strict: urlConfig._isStrictMode,
      caseInsensitive: urlConfig._isCaseInsensitive,
    };
    return new UrlMatcher(
      pattern,
      urlConfig.paramTypes,
      this.paramFactory,
      Object.assign(globalConfig, config),
    );
  }

  /**
   * Returns true if the specified object is a [[UrlMatcher]], or false otherwise.
   *
   * @param object  The object to perform the type check against.
   * @returns `true` if the object matches the `UrlMatcher` interface, by
   *          implementing all the same methods.
   */
  isMatcher(object) {
    // TODO: typeof?
    if (!isObject(object)) return false;
    let result = true;
    Object.entries(UrlMatcher.prototype).forEach(([name, val]) => {
      if (isFunction(val))
        result = result && isDefined(object[name]) && isFunction(object[name]);
    });
    return result;
  }
}

function appendBasePath(url, isHtml5, absolute, baseHref) {
  if (baseHref === "/") return url;
  if (isHtml5) return stripLastPathElement(baseHref) + url;
  if (absolute) return baseHref.slice(1) + url;
  return url;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy