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

package.src.core.compile.attributes.js Maven / Gradle / Ivy

import { getBooleanAttrName } from "../../shared/jqlite/jqlite";
import {
  isString,
  snakeCase,
  isUndefined,
  arrayRemove,
  minErr,
  trim,
  directiveNormalize,
} from "../../shared/utils";
import { ALIASED_ATTR } from "../../shared/constants";

const $compileMinErr = minErr("$compile");
const SIMPLE_ATTR_NAME = /^\w/;
const specialAttrHolder = document.createElement("div");

/**
 * @typedef {Object} AttributeLike
 * @property {Object} $attr
 */

/**
 * @extends {AttributeLike}
 */
export class Attributes {
  /**
   * @param {import('../scope/scope').Scope} $rootScope
   * @param {*} $animate
   * @param {import("../exception-handler").ErrorHandler} $exceptionHandler
   * @param {*} $sce
   * @param {import('../../shared/jqlite/jqlite').JQLite} [element]
   * @param {*} [attributesToCopy]
   */
  constructor(
    $rootScope,
    $animate,
    $exceptionHandler,
    $sce,
    element,
    attributesToCopy,
  ) {
    this.$rootScope = $rootScope;
    this.$animate = $animate;
    this.$exceptionHandler = $exceptionHandler;
    this.$sce = $sce;
    if (attributesToCopy) {
      const keys = Object.keys(attributesToCopy);
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i];
        this[key] = attributesToCopy[key];
      }
    } else {
      this.$attr = {};
    }
    this.$$element = element;
  }

  /**
   * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or
   * `data-`) to its normalized, camelCase form.
   *
   * Also there is special case for Moz prefix starting with upper case letter.
   *
   * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
   *
   * @param {string} name Name to normalize
   */
  $normalize = directiveNormalize;

  /**
   * Adds the CSS class value specified by the classVal parameter to the element. If animations
   * are enabled then an animation will be triggered for the class addition.
   *
   * @param {string} classVal The className value that will be added to the element
   */
  $addClass(classVal) {
    if (classVal && classVal.length > 0) {
      this.$animate.addClass(this.$$element, classVal);
    }
  }

  /**
   * Removes the CSS class value specified by the classVal parameter from the element. If
   * animations are enabled then an animation will be triggered for the class removal.
   *
   * @param {string} classVal The className value that will be removed from the element
   */
  $removeClass(classVal) {
    if (classVal && classVal.length > 0) {
      this.$animate.removeClass(this.$$element, classVal);
    }
  }

  /**
   * Adds and removes the appropriate CSS class values to the element based on the difference
   * between the new and old CSS class values (specified as newClasses and oldClasses).
   *
   * @param {string} newClasses The current CSS className value
   * @param {string} oldClasses The former CSS className value
   */
  $updateClass(newClasses, oldClasses) {
    const toAdd = tokenDifference(newClasses, oldClasses);
    if (toAdd && toAdd.length) {
      this.$animate.addClass(this.$$element, toAdd);
    }

    const toRemove = tokenDifference(oldClasses, newClasses);
    if (toRemove && toRemove.length) {
      this.$animate.removeClass(this.$$element, toRemove);
    }
  }

  /**
   * Set a normalized attribute on the element in a way such that all directives
   * can share the attribute. This function properly handles boolean attributes.
   * @param {string} key Normalized key. (ie ngAttribute)
   * @param {string|boolean} value The value to set. If `null` attribute will be deleted.
   * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute.
   *     Defaults to true.
   * @param {string=} attrName Optional none normalized name. Defaults to key.
   */
  $set(key, value, writeAttr, attrName) {
    // TODO: decide whether or not to throw an error if "class"
    // is set through this function since it may cause $updateClass to
    // become unstable.

    const node = this.$$element[0];
    const booleanKey = getBooleanAttrName(node, key);
    const aliasedKey = ALIASED_ATTR[key];
    let observer = key;

    if (booleanKey) {
      this.$$element[0][key] = value;
      attrName = booleanKey;
    } else if (aliasedKey) {
      this[aliasedKey] = value;
      observer = aliasedKey;
    }

    this[key] = value;

    // translate normalized key to actual key
    if (attrName) {
      this.$attr[key] = attrName;
    } else {
      attrName = this.$attr[key];
      if (!attrName) {
        this.$attr[key] = attrName = snakeCase(key, "-");
      }
    }

    let nodeName = this.$$element[0].nodeName.toLowerCase();

    // Sanitize img[srcset] values.
    if (nodeName === "img" && key === "srcset") {
      this[key] = value = this.sanitizeSrcset(value, "$set('srcset', value)");
    }

    if (writeAttr !== false) {
      if (value === null || isUndefined(value)) {
        this.$$element[0].removeAttribute(attrName);
        //
      } else if (SIMPLE_ATTR_NAME.test(attrName)) {
        // jQuery skips special boolean attrs treatment in XML nodes for
        // historical reasons and hence AngularJS cannot freely call
        // `.attr(attrName, false) with such attributes. To avoid issues
        // in XHTML, call `removeAttr` in such cases instead.
        // See https://github.com/jquery/jquery/issues/4249
        if (booleanKey && value === false) {
          this.$$element[0].removeAttribute(attrName);
        } else {
          this.$$element.attr(attrName, value);
        }
      } else {
        this.setSpecialAttr(this.$$element[0], attrName, value);
      }
    }

    // fire observers
    const { $$observers } = this;
    if ($$observers && $$observers[observer]) {
      $$observers[observer].forEach((fn) => {
        try {
          fn(value);
        } catch (e) {
          this.$exceptionHandler(e);
        }
      });
    }
  }

  /**
 * Observes an interpolated attribute.
 *
 * The observer function will be invoked once during the next `$digest` following
 * compilation. The observer is then invoked whenever the interpolated value
 * changes.
 *
 * @param {string} key Normalized key. (ie ngAttribute) .
 * @param {any} fn Function that will be called whenever
          the interpolated value of the attribute changes.
 *        See the {@link guide/interpolation#how-text-and-attribute-bindings-work Interpolation
 *        guide} for more info.
 * @returns {function()} Returns a deregistration function for this observer.
 */
  $observe(key, fn) {
    const $$observers =
      this.$$observers || (this.$$observers = Object.create(null));
    const listeners = $$observers[key] || ($$observers[key] = []);

    listeners.push(fn);
    this.$rootScope.$evalAsync(() => {
      if (
        !listeners.$$inter &&
        Object.prototype.hasOwnProperty.call(this, key) &&
        !isUndefined(this[key])
      ) {
        // no one registered attribute interpolation function, so lets call it manually
        fn(this[key]);
      }
    });

    return function () {
      arrayRemove(listeners, fn);
    };
  }

  setSpecialAttr(element, attrName, value) {
    // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute`
    // so we have to jump through some hoops to get such an attribute
    // https://github.com/angular/angular.js/pull/13318
    specialAttrHolder.innerHTML = ``;
    const { attributes } = /** @type {Element} */ (
      specialAttrHolder.firstChild
    );
    const attribute = attributes[0];
    // We have to remove the attribute from its container element before we can add it to the destination element
    attributes.removeNamedItem(attribute.name);
    attribute.value = value;
    element.attributes.setNamedItem(attribute);
  }

  sanitizeSrcset(value, invokeType) {
    if (!value) {
      return value;
    }
    if (!isString(value)) {
      throw $compileMinErr(
        "srcset",
        'Can\'t pass trusted values to `{0}`: "{1}"',
        invokeType,
        value.toString(),
      );
    }

    // Such values are a bit too complex to handle automatically inside $sce.
    // Instead, we sanitize each of the URIs individually, which works, even dynamically.

    // It's not possible to work around this using `$sce.trustAsMediaUrl`.
    // If you want to programmatically set explicitly trusted unsafe URLs, you should use
    // `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the
    // `ng-bind-html` directive.

    var result = "";

    // first check if there are spaces because it's not the same pattern
    var trimmedSrcset = trim(value);
    //                (   999x   ,|   999w   ,|   ,|,   )
    var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
    var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;

    // split srcset into tuple of uri and descriptor except for the last item
    var rawUris = trimmedSrcset.split(pattern);

    // for each tuples
    var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
    for (var i = 0; i < nbrUrisWith2parts; i++) {
      var innerIdx = i * 2;
      // sanitize the uri
      result += this.$sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
      // add the descriptor
      result += " " + trim(rawUris[innerIdx + 1]);
    }

    // split the last item into uri and descriptor
    var lastTuple = trim(rawUris[i * 2]).split(/\s/);

    // sanitize the last uri
    result += this.$sce.getTrustedMediaUrl(trim(lastTuple[0]));

    // and add the last descriptor if any
    if (lastTuple.length === 2) {
      result += " " + trim(lastTuple[1]);
    }
    return result;
  }
}

function tokenDifference(str1, str2) {
  let values = "";
  const tokens1 = str1.split(/\s+/);
  const tokens2 = str2.split(/\s+/);

  outer: for (let i = 0; i < tokens1.length; i++) {
    const token = tokens1[i];
    for (let j = 0; j < tokens2.length; j++) {
      if (token === tokens2[j]) continue outer;
    }
    values += (values.length > 0 ? " " : "") + token;
  }
  return values;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy