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

package.src.router.directives.state-directives.js Maven / Gradle / Ivy

import { tail, unnestR, uniqR, removeFrom } from "../../shared/common";
import { isString, isObject } from "../../shared/utils";

import { parse } from "../../shared/hof";
/** @ignore */
function parseStateRef(ref) {
  const paramsOnly = ref.match(/^\s*({[^}]*})\s*$/);
  if (paramsOnly) ref = "(" + paramsOnly[1] + ")";
  const parsed = ref
    .replace(/\n/g, " ")
    .match(/^\s*([^(]*?)\s*(\((.*)\))?\s*$/);
  if (!parsed || parsed.length !== 4)
    throw new Error("Invalid state ref '" + ref + "'");
  return { state: parsed[1] || null, paramExpr: parsed[3] || null };
}
/** @ignore */
function stateContext(el) {
  const $ngView = el.parent().inheritedData("$ngView");
  const path = parse("$cfg.path")($ngView);
  return path ? tail(path).state.name : undefined;
}
/** @ignore */
function processedDef($state, $element, def) {
  const ngState = def.ngState || $state.current.name;
  const ngStateOpts = Object.assign(
    defaultOpts($element, $state),
    def.ngStateOpts || {},
  );
  const href = $state.href(ngState, def.ngStateParams, ngStateOpts);
  return { ngState, ngStateParams: def.ngStateParams, ngStateOpts, href };
}
/** @ignore */
function getTypeInfo(el) {
  // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
  const isSvg =
    Object.prototype.toString.call(el[0].getAttribute("href")) ===
    "[object SVGAnimatedString]";
  const isForm = el[0].nodeName === "FORM";
  return {
    attr: isForm ? "action" : isSvg ? "xlink:href" : "href",
    isAnchor: el[0].nodeName === "A",
    clickable: !isForm,
  };
}
/** @ignore */
function clickHook(el, $state, type, getDef) {
  return function (e) {
    const button = e.which || e.button,
      target = getDef();

    let res =
      button > 1 ||
      e.ctrlKey ||
      e.metaKey ||
      e.shiftKey ||
      e.altKey ||
      el.attr("target");
    if (!res) {
      // HACK: This is to allow ng-clicks to be processed before the transition is initiated:
      const transition = setTimeout(function () {
        if (!el.attr("disabled")) {
          $state.go(target.ngState, target.ngStateParams, target.ngStateOpts);
        }
      });
      e.preventDefault();
      // if the state has no URL, ignore one preventDefault from the  directive.
      let ignorePreventDefaultCount = type.isAnchor && !target.href ? 1 : 0;
      e.preventDefault = function () {
        if (ignorePreventDefaultCount-- <= 0) clearTimeout(transition);
      };
    } else {
      // ignored
      e.preventDefault();
      e.stopImmediatePropagation();
    }
  };
}
/** @ignore */
function defaultOpts(el, $state) {
  return {
    relative: stateContext(el) || $state.$current,
    inherit: true,
    source: "sref",
  };
}
/** @ignore */
function bindEvents(element, scope, hookFn, ngStateOpts) {
  let events;
  if (ngStateOpts) {
    events = ngStateOpts.events;
  }
  if (!Array.isArray(events)) {
    events = ["click"];
  }
  const on = element.on ? "on" : "bind";
  for (const event of events) {
    element[on](event, hookFn);
  }
  scope.$on("$destroy", function () {
    const off = element.off ? "off" : "unbind";
    for (const event of events) {
      element[off](event, hookFn);
    }
  });
}

// // TODO: SEPARATE THESE OUT

$StateRefDirective.$inject = ["$state", "$stateRegistry", "$transitions"];
export function $StateRefDirective(
  $stateService,
  $stateRegistry,
  $transitions,
) {
  const $state = $stateService;
  return {
    restrict: "A",
    require: ["?^ngSrefActive", "?^ngSrefActiveEq"],
    link: (scope, element, attrs, ngSrefActive) => {
      const type = getTypeInfo(element);
      const active = ngSrefActive[1] || ngSrefActive[0];
      let unlinkInfoFn = null;
      const rawDef = {};
      const getDef = () => processedDef($state, element, rawDef);
      const ref = parseStateRef(attrs.ngSref);
      rawDef.ngState = ref.state;
      rawDef.ngStateOpts = attrs.ngSrefOpts
        ? scope.$eval(attrs.ngSrefOpts)
        : {};
      function update() {
        const def = getDef();
        if (unlinkInfoFn) unlinkInfoFn();
        if (active)
          unlinkInfoFn = active.$$addStateInfo(def.ngState, def.ngStateParams);
        if (def.href != null) attrs.$set(type.attr, def.href);
      }
      if (ref.paramExpr) {
        scope.$watch(
          ref.paramExpr,
          function (val) {
            rawDef.ngStateParams = Object.assign({}, val);
            update();
          },
          true,
        );
        rawDef.ngStateParams = Object.assign({}, scope.$eval(ref.paramExpr));
      }
      update();
      scope.$on("$destroy", $stateRegistry.onStatesChanged(update));
      scope.$on("$destroy", $transitions.onSuccess({}, update));
      if (!type.clickable) return;
      const hookFn = clickHook(element, $state, type, getDef);
      bindEvents(element, scope, hookFn, rawDef.ngStateOpts);
    },
  };
}

$StateRefDynamicDirective.$inject = [
  "$state",
  "$stateRegistry",
  "$transitions",
];
export function $StateRefDynamicDirective(
  $state,
  $stateRegistry,
  $transitions,
) {
  return {
    restrict: "A",
    require: ["?^ngSrefActive", "?^ngSrefActiveEq"],
    link: function (scope, element, attrs, ngSrefActive) {
      const type = getTypeInfo(element);
      const active = ngSrefActive[1] || ngSrefActive[0];
      let unlinkInfoFn = null;
      let hookFn;
      const rawDef = {};
      const getDef = () => processedDef($state, element, rawDef);
      const inputAttrs = ["ngState", "ngStateParams", "ngStateOpts"];
      const watchDeregFns = inputAttrs.reduce(
        (acc, attr) => ((acc[attr] = () => {}), acc),
        {},
      );
      function update() {
        const def = getDef();
        if (unlinkInfoFn) unlinkInfoFn();
        if (active)
          unlinkInfoFn = active.$$addStateInfo(def.ngState, def.ngStateParams);
        if (def.href != null) attrs.$set(type.attr, def.href);
      }
      inputAttrs.forEach((field) => {
        rawDef[field] = attrs[field] ? scope.$eval(attrs[field]) : null;
        attrs.$observe(field, (expr) => {
          watchDeregFns[field]();
          watchDeregFns[field] = scope.$watch(
            expr,
            (newval) => {
              rawDef[field] = newval;
              update();
            },
            true,
          );
        });
      });
      update();
      scope.$on("$destroy", $stateRegistry.onStatesChanged(update));
      scope.$on("$destroy", $transitions.onSuccess({}, update));
      if (!type.clickable) return;
      hookFn = clickHook(element, $state, type, getDef);
      bindEvents(element, scope, hookFn, rawDef.ngStateOpts);
    },
  };
}

$StateRefActiveDirective.$inject = [
  "$state",
  "$routerGlobals",
  "$interpolate",
  "$stateRegistry",
  "$transitions",
];

/**
 *
 * @param {*} $state
 * @param {*} $routerGlobals
 * @param {*} $interpolate
 * @param {*} $stateRegistry
 * @param {*} $transitions
 * @returns {import("../../types").Directive}
 */
export function $StateRefActiveDirective(
  $state,
  $routerGlobals,
  $interpolate,
  $stateRegistry,
  $transitions,
) {
  return {
    restrict: "A",
    controller: function ($scope, $element, $attrs) {
      let states = [];
      let activeEqClass;
      let ngSrefActive;
      // There probably isn't much point in $observing this
      // ngSrefActive and ngSrefActiveEq share the same directive object with some
      // slight difference in logic routing
      activeEqClass = $interpolate($attrs.ngSrefActiveEq || "", false)($scope);
      try {
        ngSrefActive = $scope.$eval($attrs.ngSrefActive);
      } catch (e) {
        // Do nothing. ngSrefActive is not a valid expression.
        // Fall back to using $interpolate below
      }
      ngSrefActive =
        ngSrefActive || $interpolate($attrs.ngSrefActive || "", false)($scope);
      setStatesFromDefinitionObject(ngSrefActive);
      // Allow ngSref to communicate with ngSrefActive[Equals]
      this.$$addStateInfo = function (newState, newParams) {
        // we already got an explicit state provided by ui-sref-active, so we
        // shadow the one that comes from ui-sref
        if (isObject(ngSrefActive) && states.length > 0) {
          return;
        }
        const deregister = addState(newState, newParams, ngSrefActive);
        update();
        return deregister;
      };
      function updateAfterTransition(trans) {
        trans.promise.then(update, () => {});
      }
      $scope.$on("$destroy", setupEventListeners());
      if ($routerGlobals.transition) {
        updateAfterTransition($routerGlobals.transition);
      }
      function setupEventListeners() {
        const deregisterStatesChangedListener =
          $stateRegistry.onStatesChanged(handleStatesChanged);
        const deregisterOnStartListener = $transitions.onStart(
          {},
          updateAfterTransition,
        );
        const deregisterStateChangeSuccessListener = $scope.$on(
          "$stateChangeSuccess",
          update,
        );
        return function cleanUp() {
          deregisterStatesChangedListener();
          deregisterOnStartListener();
          deregisterStateChangeSuccessListener();
        };
      }
      function handleStatesChanged() {
        setStatesFromDefinitionObject(ngSrefActive);
      }
      function setStatesFromDefinitionObject(statesDefinition) {
        if (isObject(statesDefinition)) {
          states = [];
          Object.entries(statesDefinition).forEach(
            ([activeClass, stateOrName]) => {
              // Helper function to abstract adding state.
              const addStateForClass = function (stateOrName, activeClass) {
                const ref = parseStateRef(stateOrName);
                addState(ref.state, $scope.$eval(ref.paramExpr), activeClass);
              };
              if (isString(stateOrName)) {
                // If state is string, just add it.
                addStateForClass(stateOrName, activeClass);
              } else if (Array.isArray(stateOrName)) {
                // If state is an array, iterate over it and add each array item individually.
                stateOrName.forEach((stateOrName) => {
                  addStateForClass(stateOrName, activeClass);
                });
              }
            },
          );
        }
      }
      function addState(stateName, stateParams, activeClass) {
        const state = $state.get(stateName, stateContext($element));
        const stateInfo = {
          state: state || { name: stateName },
          params: stateParams,
          activeClass: activeClass,
        };
        states.push(stateInfo);
        return function removeState() {
          removeFrom(states)(stateInfo);
        };
      }
      // Update route state
      function update() {
        const splitClasses = (str) => str.split(/\s/).filter(Boolean);
        const getClasses = (stateList) =>
          stateList
            .map((x) => x.activeClass)
            .map(splitClasses)
            .reduce(unnestR, []);
        const allClasses = getClasses(states)
          .concat(splitClasses(activeEqClass))
          .reduce(uniqR, []);
        const fuzzyClasses = getClasses(
          states.filter((x) => $state.includes(x.state.name, x.params)),
        );
        const exactlyMatchesAny = !!states.filter((x) =>
          $state.is(x.state.name, x.params),
        ).length;
        const exactClasses = exactlyMatchesAny
          ? splitClasses(activeEqClass)
          : [];
        const addClasses = fuzzyClasses.concat(exactClasses).reduce(uniqR, []);
        const removeClasses = allClasses.filter(
          (cls) => !addClasses.includes(cls),
        );
        $scope.$evalAsync(() => {
          addClasses.forEach((className) =>
            $element[0].classList.add(className),
          );
          removeClasses.forEach((className) =>
            $element[0].classList.remove(className),
          );
        });
      }
      update();
    },
  };
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy