<commit_before>import {
  defaultState,
  firstRender,
  raiseChangeEvents,
  render,
  renderChanges,
  rendered,
  rendering,
  setState,
  state,
  stateEffects,
} from "./internal.js";

/** @type {any} */
const stateKey = Symbol("state");
/** @type {any} */
const raiseChangeEventsInNextRenderKey = Symbol(
  "raiseChangeEventsInNextRender"
);
// Tracks total set of changes made to elements since their last render.
/** @type {any} */
const changedSinceLastRenderKey = Symbol("changedSinceLastRender");

/**
 * Manages component state and renders changes in state
 *
 * This is modeled after React/Preact's state management, and is adapted for
 * use with web components. Applying this mixin to a component will give it
 * FRP behavior comparable to React's.
 *
 * This model is very basic. It's key aspects are:
 * * an immutable `state` property updated via `setState` calls.
 * * a `render` method that will be invoked asynchronously when state changes.
 *
 * @module ReactiveMixin
 * @param {Constructor<CustomElement>} Base
 */
export default function ReactiveMixin(Base) {
  class Reactive extends Base {
    constructor() {
      super();

      // Components can inspect `firstRender` during rendering to do special
      // work the first time (like wire up event handlers). Until the first
      // render actually happens, we set that flag to be undefined so we have a
      // way of distinguishing between a component that has never rendered and
      // one that is being rendered for the nth time.
      this[firstRender] = undefined;

      // We want to support the standard HTML pattern of only raising events in
      // response to direct user interactions. For a detailed discussion of this
      // point, see the Gold Standard checklist item for [Propery Change
      // Events](https://github.com/webcomponents/gold-standard/wiki/Property%20Change%20Events).
      //
      // To support this pattern, we define a flag indicating whether change
      // events should be raised. By default, we want the flag to be false. In
      // UI event handlers, a component can temporarily set the flag to true. If
      // a setState call is made while the flag is true, then that fact will be
      // remembered and passed the subsequent render/rendered methods. That will
      // let the methods know whether they should raise property change events.
      this[raiseChangeEvents] = false;

      // Maintain a change log of all fields which have changed since the
      // component was last rendered.
      this[changedSinceLastRenderKey] = null;

      // Set the initial state from the default state defined by the component
      // and its mixins/base classes.
      this[setState](this[defaultState]);
    }

    // When the component is attached to the document (or upgraded), we will
    // generally render the component for the first time. That operation will
    // include rendering of the default state and any state changes that
    // happened between constructor time and this connectedCallback.
    connectedCallback() {
      if (super.connectedCallback) {
        super.connectedCallback();
      }

      // Render the component.
      //
      // If the component was forced to render before this point, and the state
      // hasn't changed, this call will be a no-op.
      this[renderChanges]();
    }

    /**
     * The default state for the component. This can be extended by mixins and
     * classes to provide additional default state.
     *
     * @type {PlainObject}
     */
    get [defaultState]() {
      // Defer to base implementation if defined.
      return super[defaultState] || {};
    }

    /**
     * Render the indicated changes in state to the DOM.
     *
     * The default implementation of this method does nothing. Override this
     * method in your component to update your component's host element and
     * any shadow elements to reflect the component's new state. See the
     * [rendering example](ReactiveMixin#rendering).
     *
     * Be sure to call `super` in your method implementation so that your
     * component's base classes and mixins have a chance to perform their own
     * render work.
     *
     * @param {ChangedFlags} changed - dictionary of flags indicating which state
     * members have changed since the last render
     */
    [render](changed) {
      if (super[render]) {
        super[render](changed);
      }
    }

    /**
     * Render any pending component changes to the DOM.
     *
     * This method does nothing if the state has not changed since the last
     * render call.
     *
     * ReactiveMixin will invoke this method following a `setState` call;
     * you should not need to invoke this method yourself.
     *
     * This method invokes the internal `render` method, then invokes the
     * `rendered` method.
     */
    [renderChanges]() {
      if (this[firstRender] === undefined) {
        // First render.
        this[firstRender] = true;
      }

      // Get the log of which fields have changed since the last render. As a
      // special case, the initial value of this set will be the empty set, so
      // we'll always render the first time.
      const changed = this[changedSinceLastRenderKey];

      // We only render if this is the first render, or state has has changed
      // since the last render.
      if (this[firstRender] || changed) {
        // If at least one of the[setState] calls was made in response
        // to user interaction or some other component-internal event, set the
        // raiseChangeEvents flag so that render/rendered methods know whether
        // to raise property change events. See the comments in the component
        // constructor where we initialize this flag for details.
        const saveRaiseChangeEvents = this[raiseChangeEvents];
        this[raiseChangeEvents] = this[raiseChangeEventsInNextRenderKey];

        // We set a flag to indicate that rendering is happening. The component
        // may use this to avoid triggering other updates during the render.
        this[rendering] = true;

        // Invoke any internal render implementations.
        this[render](changed);

        this[rendering] = false;

        // Since we've now rendered all changes, clear the change log. If other
        // async render calls are queued up behind this call, they'll see an
        // empty change log, and so skip unnecessary render work.
        this[changedSinceLastRenderKey] = null;

        // Let the component know it was rendered.
        this[rendered](changed);

        // We've now rendered for the first time.
        this[firstRender] = false;

        // Restore state of event flags.
        this[raiseChangeEvents] = saveRaiseChangeEvents;
        this[raiseChangeEventsInNextRenderKey] = saveRaiseChangeEvents;
      }
    }

    /**
     * Perform any work that must happen after state changes have been rendered
     * to the DOM.
     *
     * The default implementation of this method does nothing. Override this
     * method in your component to perform work that requires the component to
     * be fully rendered, such as setting focus on a shadow element or
     * inspecting the computed style of an element. If such work should result
     * in a change in component state, you can safely call `setState` during the
     * `rendered` method.
     *
     * Be sure to call `super` in your method implementation so that your
     * component's base classes and mixins have a chance to perform their own
     * post-render work.
     *
     * @param {ChangedFlags} changed
     */
    [rendered](changed) {
      if (super[rendered]) {
        super[rendered](changed);
      }
    }

    /**
     * Update the component's state by merging the specified changes on
     * top of the existing state. If the component is connected to the document,
     * and the new state has changed, this returns a promise to asynchronously
     * render the component. Otherwise, this returns a resolved promise.
     *
     * @param {PlainObject} changes - the changes to apply to the element's state
     * @returns {Promise} - resolves when the new state has been rendered
     */
    async [setState](changes) {
      // There's no good reason to have a render method update state.
      if (this[rendering]) {
        /* eslint-disable no-console */
        console.warn(
          `${this.constructor.name} called [setState] during rendering, which you should avoid.\nSee https://elix.org/documentation/ReactiveMixin.`
        );
      }

      // Apply the changes to a copy of the component's current state to produce
      // a new, updated state and a dictionary of flags indicating which fields
      // actually changed.
      const { state, changed } = copyStateWithChanges(this, changes);

      // We only need to apply the changes to the component state if: a) this is
      // the first time setState has been called, or b) the supplied changes
      // parameter actually contains substantive changes.
      const firstSetState = this[stateKey] === undefined;
      const substantiveChanges = Object.keys(changed).length > 0;
      if (!(firstSetState || substantiveChanges)) {
        // No need to update state.
        return;
      }

      // Freeze the new state so it's immutable. This prevents accidental
      // attempts to set state without going through [setState].
      Object.freeze(state);

      // Set this as the component's new state.
      this[stateKey] = state;

      // If setState was called with the raiseChangeEvents flag set, record that
      // fact for use in rendering. See the comments in the component
      // constructor for details.
      if (this[raiseChangeEvents]) {
        this[raiseChangeEventsInNextRenderKey] = true;
      }

      // Look to see whether the component is already set up to render.
      const willRender =
        this[firstRender] === undefined ||
        this[changedSinceLastRenderKey] !== null;

      // Add this round of changed fields to the complete log of fields that
      // have changed since the component was last rendered.
      this[changedSinceLastRenderKey] = Object.assign(
        this[changedSinceLastRenderKey] || {},
        changed
      );

      // We only need to queue a render if we're in the document and a render
      // operation hasn't already been queued for this component. If we're not
      // in the document yet, when the component is eventually added to the
      // document, the connectedCallback will ensure we render at that point.
      const needsRender = this.isConnected && !willRender;
      if (needsRender) {
        // Yield with promise timing. This lets any *synchronous* setState calls
        // that happen after this current setState call complete first. Their
        // effects on the state will be batched up, and accumulate in the change
        // log stored under this[changedSinceLastRenderKey].
        await Promise.resolve();

        // Now that the above promise has resolved, render the component. By the
        // time this line is reached, the complete log of batched changes can be
        // applied in a single render call.
        this[renderChanges]();
      }
    }

    /**
     * The component's current state.
     *
     * The returned state object is immutable. To update it, invoke
     * `internal.setState`.
     *
     * It's extremely useful to be able to inspect component state while
     * debugging. If you append `?elixdebug=true` to a page's URL, then
     * ReactiveMixin will conditionally expose a public `state` property that
     * returns the component's state. You can then access the state in your
     * browser's debug console.
     *
     * @type {PlainObject}
     */
    get [state]() {
      return this[stateKey];
    }

    /**
     * Ask the component whether a state with a set of recently-changed fields
     * implies that additional second-order changes should be applied to that
     * state to make it consistent.
     *
     * This method is invoked during a call to `internal.setState` to give all
     * of a component's mixins and classes a chance to respond to changes in
     * state. If one mixin/class updates state that it controls, another
     * mixin/class may want to respond by updating some other state member that
     * *it* controls.
     *
     * This method should return a dictionary of changes that should be applied
     * to the state. If the dictionary object is not empty, the
     * `internal.setState` method will apply the changes to the state, and
     * invoke this `stateEffects` method again to determine whether there are
     * any third-order effects that should be applied. This process repeats
     * until all mixins/classes report that they have no additional changes to
     * make.
     *
     * See an example of how `ReactiveMixin` invokes the `stateEffects` to
     * [ensure state consistency](ReactiveMixin#ensuring-state-consistency).
     *
     * @param {PlainObject} state - a proposal for a new state
     * @param {ChangedFlags} changed - the set of fields changed in this
     * latest proposal for the new state
     * @returns {PlainObject}
     */
    [stateEffects](state, changed) {
      return super[stateEffects] ? super[stateEffects](state, changed) : {};
    }
  }

  // Expose state when debugging; see note for `[state]` getter.
  const elixdebug = new URLSearchParams(location.search).get("elixdebug");
  if (elixdebug === "true") {
    Object.defineProperty(Reactive.prototype, "state", {
      get() {
        return this[state];
      },
    });
  }

  return Reactive;
}

/**
 * Create a copy of the component's state with the indicated changes applied.
 * Ask the component whether the new state implies any second-order effects. If
 * so, apply those and loop again until the state has stabilized. Return the new
 * state and a dictionary of flags indicating which fields were actually
 * changed.
 *
 * @private
 * @param {Element} element
 * @param {PlainObject} changes
 */
export function copyStateWithChanges(element, changes) {
  // Start with a copy of the current state.
  /** @type {PlainObject} */
  const state = Object.assign({}, element[stateKey]);
  /** @type {ChangedFlags} */
  const changed = {};
  // Take the supplied changes as the first round of effects.
  let effects = changes;
  // Loop until there are no effects to apply.
  /* eslint-disable no-constant-condition */
  while (true) {
    // See whether the effects actually changed anything in state.
    const changedByEffects = fieldsChanged(state, effects);
    if (Object.keys(changedByEffects).length === 0) {
      // No more effects to apply; we're done.
      break;
    }
    // Apply the effects.
    Object.assign(state, effects);
    Object.assign(changed, changedByEffects);
    // Ask the component if there are any second- (or third-, etc.) order
    // effects that should be applied.
    effects = element[stateEffects](state, changedByEffects);
  }
  return { state, changed };
}

/**
 * Return true if the two values are equal.
 *
 * @private
 * @param {any} value1
 * @param {any} value2
 * @returns {boolean}
 */
function equal(value1, value2) {
  if (value1 instanceof Date && value2 instanceof Date) {
    return value1.getTime() === value2.getTime();
  }
  return value1 === value2;
}

/**
 * Return a dictionary of flags indicating which of the indicated changes to the
 * state are actually substantive changes.
 *
 * @private
 * @param {PlainObject} state
 * @param {PlainObject} changes
 */
function fieldsChanged(state, changes) {
  /** @type {ChangedFlags} */
  const changed = {};
  for (const field in changes) {
    if (!equal(changes[field], state[field])) {
      changed[field] = true;
    }
  }
  return changed;
}
<commit_msg>Comments<commit_after>import {
  defaultState,
  firstRender,
  raiseChangeEvents,
  render,
  renderChanges,
  rendered,
  rendering,
  setState,
  state,
  stateEffects,
} from "./internal.js";

/** @type {any} */
const stateKey = Symbol("state");
/** @type {any} */
const raiseChangeEventsInNextRenderKey = Symbol(
  "raiseChangeEventsInNextRender"
);
// Tracks total set of changes made to elements since their last render.
/** @type {any} */
const changedSinceLastRenderKey = Symbol("changedSinceLastRender");

/**
 * Manages component state and renders changes in state
 *
 * This is modeled after React/Preact's state management, and is adapted for
 * use with web components. Applying this mixin to a component will give it
 * FRP behavior comparable to React's.
 *
 * This model is very basic. It's key aspects are:
 * * an immutable `state` property updated via `setState` calls.
 * * a `render` method that will be invoked asynchronously when state changes.
 *
 * @module ReactiveMixin
 * @param {Constructor<CustomElement>} Base
 */
export default function ReactiveMixin(Base) {
  class Reactive extends Base {
    constructor() {
      super();

      // Components can inspect `firstRender` during rendering to do special
      // work the first time (like wire up event handlers). Until the first
      // render actually happens, we set that flag to be undefined so we have a
      // way of distinguishing between a component that has never rendered and
      // one that is being rendered for the nth time.
      this[firstRender] = undefined;

      // We want to support the standard HTML pattern of only raising events in
      // response to direct user interactions. For a detailed discussion of this
      // point, see the Gold Standard checklist item for [Propery Change
      // Events](https://github.com/webcomponents/gold-standard/wiki/Property%20Change%20Events).
      //
      // To support this pattern, we define a flag indicating whether change
      // events should be raised. By default, we want the flag to be false. In
      // UI event handlers, a component can temporarily set the flag to true. If
      // a setState call is made while the flag is true, then that fact will be
      // remembered and passed the subsequent render/rendered methods. That will
      // let the methods know whether they should raise property change events.
      this[raiseChangeEvents] = false;

      // Maintain a change log of all fields which have changed since the
      // component was last rendered.
      this[changedSinceLastRenderKey] = null;

      // Set the initial state from the default state defined by the component
      // and its mixins/base classes.
      this[setState](this[defaultState]);
    }

    // When the component is attached to the document (or upgraded), we will
    // generally render the component for the first time. That operation will
    // include rendering of the default state and any state changes that
    // happened between constructor time and this connectedCallback.
    connectedCallback() {
      if (super.connectedCallback) {
        super.connectedCallback();
      }

      // Render the component.
      //
      // If the component was forced to render before this point, and the state
      // hasn't changed, this call will be a no-op.
      this[renderChanges]();
    }

    /**
     * The default state for the component. This can be extended by mixins and
     * classes to provide additional default state.
     *
     * @type {PlainObject}
     */
    get [defaultState]() {
      // Defer to base implementation if defined.
      return super[defaultState] || {};
    }

    /**
     * Render the indicated changes in state to the DOM.
     *
     * The default implementation of this method does nothing. Override this
     * method in your component to update your component's host element and
     * any shadow elements to reflect the component's new state. See the
     * [rendering example](ReactiveMixin#rendering).
     *
     * Be sure to call `super` in your method implementation so that your
     * component's base classes and mixins have a chance to perform their own
     * render work.
     *
     * @param {ChangedFlags} changed - dictionary of flags indicating which state
     * members have changed since the last render
     */
    [render](changed) {
      if (super[render]) {
        super[render](changed);
      }
    }

    /**
     * Render any pending component changes to the DOM.
     *
     * This method does nothing if the state has not changed since the last
     * render call.
     *
     * ReactiveMixin will invoke this method following a `setState` call;
     * you should not need to invoke this method yourself.
     *
     * This method invokes the internal `render` method, then invokes the
     * `rendered` method.
     */
    [renderChanges]() {
      if (this[firstRender] === undefined) {
        // First render.
        this[firstRender] = true;
      }

      // Get the log of which fields have changed since the last render.
      const changed = this[changedSinceLastRenderKey];

      // We only render if this is the first render, or state has changed since
      // the last render.
      if (this[firstRender] || changed) {
        // If at least one of the[setState] calls was made in response
        // to user interaction or some other component-internal event, set the
        // raiseChangeEvents flag so that render/rendered methods know whether
        // to raise property change events. See the comments in the component
        // constructor where we initialize this flag for details.
        const saveRaiseChangeEvents = this[raiseChangeEvents];
        this[raiseChangeEvents] = this[raiseChangeEventsInNextRenderKey];

        // We set a flag to indicate that rendering is happening. The component
        // may use this to avoid triggering other updates during the render.
        this[rendering] = true;

        // Invoke any internal render implementations.
        this[render](changed);

        this[rendering] = false;

        // Since we've now rendered all changes, clear the change log. If other
        // async render calls are queued up behind this call, they'll see an
        // empty change log, and so skip unnecessary render work.
        this[changedSinceLastRenderKey] = null;

        // Let the component know it was rendered.
        this[rendered](changed);

        // We've now rendered for the first time.
        this[firstRender] = false;

        // Restore state of event flags.
        this[raiseChangeEvents] = saveRaiseChangeEvents;
        this[raiseChangeEventsInNextRenderKey] = saveRaiseChangeEvents;
      }
    }

    /**
     * Perform any work that must happen after state changes have been rendered
     * to the DOM.
     *
     * The default implementation of this method does nothing. Override this
     * method in your component to perform work that requires the component to
     * be fully rendered, such as setting focus on a shadow element or
     * inspecting the computed style of an element. If such work should result
     * in a change in component state, you can safely call `setState` during the
     * `rendered` method.
     *
     * Be sure to call `super` in your method implementation so that your
     * component's base classes and mixins have a chance to perform their own
     * post-render work.
     *
     * @param {ChangedFlags} changed
     */
    [rendered](changed) {
      if (super[rendered]) {
        super[rendered](changed);
      }
    }

    /**
     * Update the component's state by merging the specified changes on
     * top of the existing state. If the component is connected to the document,
     * and the new state has changed, this returns a promise to asynchronously
     * render the component. Otherwise, this returns a resolved promise.
     *
     * @param {PlainObject} changes - the changes to apply to the element's state
     * @returns {Promise} - resolves when the new state has been rendered
     */
    async [setState](changes) {
      // There's no good reason to have a render method update state.
      if (this[rendering]) {
        /* eslint-disable no-console */
        console.warn(
          `${this.constructor.name} called [setState] during rendering, which you should avoid.\nSee https://elix.org/documentation/ReactiveMixin.`
        );
      }

      // Apply the changes to a copy of the component's current state to produce
      // a new, updated state and a dictionary of flags indicating which fields
      // actually changed.
      const { state, changed } = copyStateWithChanges(this, changes);

      // We only need to apply the changes to the component state if: a) this is
      // the first time setState has been called, or b) the supplied changes
      // parameter actually contains substantive changes.
      const firstSetState = this[stateKey] === undefined;
      const substantiveChanges = Object.keys(changed).length > 0;
      if (!(firstSetState || substantiveChanges)) {
        // No need to update state.
        return;
      }

      // Freeze the new state so it's immutable. This prevents accidental
      // attempts to set state without going through [setState].
      Object.freeze(state);

      // Set this as the component's new state.
      this[stateKey] = state;

      // If setState was called with the raiseChangeEvents flag set, record that
      // fact for use in rendering. See the comments in the component
      // constructor for details.
      if (this[raiseChangeEvents]) {
        this[raiseChangeEventsInNextRenderKey] = true;
      }

      // Look to see whether the component is already set up to render.
      const willRender =
        this[firstRender] === undefined ||
        this[changedSinceLastRenderKey] !== null;

      // Add this round of changed fields to the complete log of fields that
      // have changed since the component was last rendered.
      this[changedSinceLastRenderKey] = Object.assign(
        this[changedSinceLastRenderKey] || {},
        changed
      );

      // We only need to queue a render if we're in the document and a render
      // operation hasn't already been queued for this component. If we're not
      // in the document yet, when the component is eventually added to the
      // document, the connectedCallback will ensure we render at that point.
      const needsRender = this.isConnected && !willRender;
      if (needsRender) {
        // Yield with promise timing. This lets any *synchronous* setState calls
        // that happen after this current setState call complete first. Their
        // effects on the state will be batched up, and accumulate in the change
        // log stored under this[changedSinceLastRenderKey].
        await Promise.resolve();

        // Now that the above promise has resolved, render the component. By the
        // time this line is reached, the complete log of batched changes can be
        // applied in a single render call.
        this[renderChanges]();
      }
    }

    /**
     * The component's current state.
     *
     * The returned state object is immutable. To update it, invoke
     * `internal.setState`.
     *
     * It's extremely useful to be able to inspect component state while
     * debugging. If you append `?elixdebug=true` to a page's URL, then
     * ReactiveMixin will conditionally expose a public `state` property that
     * returns the component's state. You can then access the state in your
     * browser's debug console.
     *
     * @type {PlainObject}
     */
    get [state]() {
      return this[stateKey];
    }

    /**
     * Ask the component whether a state with a set of recently-changed fields
     * implies that additional second-order changes should be applied to that
     * state to make it consistent.
     *
     * This method is invoked during a call to `internal.setState` to give all
     * of a component's mixins and classes a chance to respond to changes in
     * state. If one mixin/class updates state that it controls, another
     * mixin/class may want to respond by updating some other state member that
     * *it* controls.
     *
     * This method should return a dictionary of changes that should be applied
     * to the state. If the dictionary object is not empty, the
     * `internal.setState` method will apply the changes to the state, and
     * invoke this `stateEffects` method again to determine whether there are
     * any third-order effects that should be applied. This process repeats
     * until all mixins/classes report that they have no additional changes to
     * make.
     *
     * See an example of how `ReactiveMixin` invokes the `stateEffects` to
     * [ensure state consistency](ReactiveMixin#ensuring-state-consistency).
     *
     * @param {PlainObject} state - a proposal for a new state
     * @param {ChangedFlags} changed - the set of fields changed in this
     * latest proposal for the new state
     * @returns {PlainObject}
     */
    [stateEffects](state, changed) {
      return super[stateEffects] ? super[stateEffects](state, changed) : {};
    }
  }

  // Expose state when debugging; see note for `[state]` getter.
  const elixdebug = new URLSearchParams(location.search).get("elixdebug");
  if (elixdebug === "true") {
    Object.defineProperty(Reactive.prototype, "state", {
      get() {
        return this[state];
      },
    });
  }

  return Reactive;
}

/**
 * Create a copy of the component's state with the indicated changes applied.
 * Ask the component whether the new state implies any second-order effects. If
 * so, apply those and loop again until the state has stabilized. Return the new
 * state and a dictionary of flags indicating which fields were actually
 * changed.
 *
 * @private
 * @param {Element} element
 * @param {PlainObject} changes
 */
export function copyStateWithChanges(element, changes) {
  // Start with a copy of the current state.
  /** @type {PlainObject} */
  const state = Object.assign({}, element[stateKey]);
  /** @type {ChangedFlags} */
  const changed = {};
  // Take the supplied changes as the first round of effects.
  let effects = changes;
  // Loop until there are no effects to apply.
  /* eslint-disable no-constant-condition */
  while (true) {
    // See whether the effects actually changed anything in state.
    const changedByEffects = fieldsChanged(state, effects);
    if (Object.keys(changedByEffects).length === 0) {
      // No more effects to apply; we're done.
      break;
    }
    // Apply the effects.
    Object.assign(state, effects);
    Object.assign(changed, changedByEffects);
    // Ask the component if there are any second- (or third-, etc.) order
    // effects that should be applied.
    effects = element[stateEffects](state, changedByEffects);
  }
  return { state, changed };
}

/**
 * Return true if the two values are equal.
 *
 * @private
 * @param {any} value1
 * @param {any} value2
 * @returns {boolean}
 */
function equal(value1, value2) {
  if (value1 instanceof Date && value2 instanceof Date) {
    return value1.getTime() === value2.getTime();
  }
  return value1 === value2;
}

/**
 * Return a dictionary of flags indicating which of the indicated changes to the
 * state are actually substantive changes.
 *
 * @private
 * @param {PlainObject} state
 * @param {PlainObject} changes
 */
function fieldsChanged(state, changes) {
  /** @type {ChangedFlags} */
  const changed = {};
  for (const field in changes) {
    if (!equal(changes[field], state[field])) {
      changed[field] = true;
    }
  }
  return changed;
}
<|endoftext|>


<commit_before>/**
 * SVGInjector v1.1.3 - Fast, caching, dynamic inline SVG DOM injection library
 * https://github.com/iconic/SVGInjector
 *
 * Copyright (c) 2014-2015 Waybury <hello@waybury.com>
 * @license MIT
 */

(function (window, document) {

  'use strict';

  // Environment
  var isLocal = window.location.protocol === 'file:';
  var hasSvgSupport = document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');

  function uniqueClasses(list) {
    list = list.split(' ');

    var hash = {};
    var i = list.length;
    var out = [];

    while (i--) {
      if (!hash.hasOwnProperty(list[i])) {
        hash[list[i]] = 1;
        out.unshift(list[i]);
      }
    }

    return out.join(' ');
  }

  /**
   * cache (or polyfill for <= IE8) Array.forEach()
   * source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
   */
  var forEach = Array.prototype.forEach || function (fn, scope) {
    if (this === void 0 || this === null || typeof fn !== 'function') {
      throw new TypeError();
    }

    /* jshint bitwise: false */
    var i, len = this.length >>> 0;
    /* jshint bitwise: true */

    for (i = 0; i < len; ++i) {
      if (i in this) {
        fn.call(scope, this[i], i, this);
      }
    }
  };

  // SVG Cache
  var svgCache = {};

  var injectCount = 0;
  var injectedElements = [];

  // Request Queue
  var requestQueue = [];

  // Script running status
  var ranScripts = {};

  var cloneSvg = function (sourceSvg) {
    return sourceSvg.cloneNode(true);
  };

  var queueRequest = function (url, callback) {
    requestQueue[url] = requestQueue[url] || [];
    requestQueue[url].push(callback);
  };

  var processRequestQueue = function (url) {
    for (var i = 0, len = requestQueue[url].length; i < len; i++) {
      // Make these calls async so we avoid blocking the page/renderer
      /* jshint loopfunc: true */
      (function (index) {
        setTimeout(function () {
          requestQueue[url][index](cloneSvg(svgCache[url]));
        }, 0);
      })(i);
      /* jshint loopfunc: false */
    }
  };

  var loadSvg = function (url, callback) {
    if (svgCache[url] !== undefined) {
      if (svgCache[url] instanceof SVGSVGElement) {
        // We already have it in cache, so use it
        callback(cloneSvg(svgCache[url]));
      }
      else {
        // We don't have it in cache yet, but we are loading it, so queue this request
        queueRequest(url, callback);
      }
    }
    else {

      if (!window.XMLHttpRequest) {
        callback('Browser does not support XMLHttpRequest');
        return false;
      }

      // Seed the cache to indicate we are loading this URL already
      svgCache[url] = {};
      queueRequest(url, callback);

      var httpRequest = new XMLHttpRequest();

      httpRequest.onreadystatechange = function () {
        // readyState 4 = complete
        if (httpRequest.readyState === 4) {

          // Handle status
          if (httpRequest.status === 404 || httpRequest.responseXML === null) {
            callback('Unable to load SVG file: ' + url);

            if (isLocal) callback('Note: SVG injection ajax calls do not work locally without adjusting security setting in your browser. Or consider using a local webserver.');

            callback();
            return false;
          }

          // 200 success from server, or 0 when using file:// protocol locally
          if (httpRequest.status === 200 || (isLocal && httpRequest.status === 0)) {

            /* globals Document */
            if (httpRequest.responseXML instanceof Document) {
              // Cache it
              svgCache[url] = httpRequest.responseXML.documentElement;
            }
            /* globals -Document */

            // IE9 doesn't create a responseXML Document object from loaded SVG,
            // and throws a "DOM Exception: HIERARCHY_REQUEST_ERR (3)" error when injected.
            //
            // So, we'll just create our own manually via the DOMParser using
            // the the raw XML responseText.
            //
            // :NOTE: IE8 and older doesn't have DOMParser, but they can't do SVG either, so...
            else if (DOMParser && (DOMParser instanceof Function)) {
              var xmlDoc;
              try {
                var parser = new DOMParser();
                xmlDoc = parser.parseFromString(httpRequest.responseText, 'text/xml');
              }
              catch (e) {
                xmlDoc = undefined;
              }

              if (!xmlDoc || xmlDoc.getElementsByTagName('parsererror').length) {
                callback('Unable to parse SVG file: ' + url);
                return false;
              }
              else {
                // Cache it
                svgCache[url] = xmlDoc.documentElement;
              }
            }

            // We've loaded a new asset, so process any requests waiting for it
            processRequestQueue(url);
          }
          else {
            callback('There was a problem injecting the SVG: ' + httpRequest.status + ' ' + httpRequest.statusText);
            return false;
          }
        }
      };

      httpRequest.open('GET', url);

      // Treat and parse the response as XML, even if the
      // server sends us a different mimetype
      if (httpRequest.overrideMimeType) httpRequest.overrideMimeType('text/xml');

      httpRequest.send();
    }
  };

  // Inject a single element
  var injectElement = function (el, evalScripts, pngFallback, callback) {

    // Grab the src or data-src attribute
    var imgUrl = el.getAttribute('data-src') || el.getAttribute('src');

    // We can only inject SVG
    if (!(/\.svg/i).test(imgUrl)) {
      callback('Attempted to inject a file with a non-svg extension: ' + imgUrl);
      return;
    }

    // If we don't have SVG support try to fall back to a png,
    // either defined per-element via data-fallback or data-png,
    // or globally via the pngFallback directory setting
    if (!hasSvgSupport) {
      var perElementFallback = el.getAttribute('data-fallback') || el.getAttribute('data-png');

      // Per-element specific PNG fallback defined, so use that
      if (perElementFallback) {
        el.setAttribute('src', perElementFallback);
        callback(null);
      }
      // Global PNG fallback directoriy defined, use the same-named PNG
      else if (pngFallback) {
        el.setAttribute('src', pngFallback + '/' + imgUrl.split('/').pop().replace('.svg', '.png'));
        callback(null);
      }
      // um...
      else {
        callback('This browser does not support SVG and no PNG fallback was defined.');
      }

      return;
    }

    // Make sure we aren't already in the process of injecting this element to
    // avoid a race condition if multiple injections for the same element are run.
    // :NOTE: Using indexOf() only _after_ we check for SVG support and bail,
    // so no need for IE8 indexOf() polyfill
    if (injectedElements.indexOf(el) !== -1) {
      return;
    }

    // Remember the request to inject this element, in case other injection
    // calls are also trying to replace this element before we finish
    injectedElements.push(el);

    // Try to avoid loading the orginal image src if possible.
    el.setAttribute('src', '');

    // Load it up
    loadSvg(imgUrl, function (svg) {

      if (typeof svg === 'undefined' || typeof svg === 'string') {
        callback(svg);
        return false;
      }

      var imgId = el.getAttribute('id');
      if (imgId) {
        svg.setAttribute('id', imgId);
      }

      var imgTitle = el.getAttribute('title');
      if (imgTitle) {
        svg.setAttribute('title', imgTitle);
      }

      // Concat the SVG classes + 'injected-svg' + the img classes
      var classMerge = [].concat(svg.getAttribute('class') || [], 'injected-svg', el.getAttribute('class') || []).join(' ');
      svg.setAttribute('class', uniqueClasses(classMerge));

      var imgStyle = el.getAttribute('style');
      if (imgStyle) {
        svg.setAttribute('style', imgStyle);
      }

      // Copy all the data elements to the svg
      var imgData = [].filter.call(el.attributes, function (at) {
        return (/^data-\w[\w\-]*$/).test(at.name);
      });
      forEach.call(imgData, function (dataAttr) {
        if (dataAttr.name && dataAttr.value) {
          svg.setAttribute(dataAttr.name, dataAttr.value);
        }
      });

      // Make sure any internally referenced clipPath ids and their
      // clip-path references are unique.
      //
      // This addresses the issue of having multiple instances of the
      // same SVG on a page and only the first clipPath id is referenced.
      //
      // Browsers often shortcut the SVG Spec and don't use clipPaths
      // contained in parent elements that are hidden, so if you hide the first
      // SVG instance on the page, then all other instances lose their clipping.
      // Reference: https://bugzilla.mozilla.org/show_bug.cgi?id=376027

      // Handle all defs elements that have iri capable attributes as defined by w3c: http://www.w3.org/TR/SVG/linking.html#processingIRI
      // Mapping IRI addressable elements to the properties that can reference them:
      var iriElementsAndProperties = {
        'clipPath': ['clip-path'],
        'color-profile': ['color-profile'],
        'cursor': ['cursor'],
        'filter': ['filter'],
        'linearGradient': ['fill', 'stroke'],
        'marker': ['marker', 'marker-start', 'marker-mid', 'marker-end'],
        'mask': ['mask'],
        'pattern': ['fill', 'stroke'],
        'radialGradient': ['fill', 'stroke']
      };

      var element, elementDefs, properties, currentId, newId;
      Object.keys(iriElementsAndProperties).forEach(function (key) {
        element = key;
        properties = iriElementsAndProperties[key];

        elementDefs = svg.querySelectorAll('defs ' + element + '[id]');
        for (var i = 0, elementsLen = elementDefs.length; i < elementsLen; i++) {
          currentId = elementDefs[i].id;
          newId = currentId + '-' + injectCount;

          // All of the properties that can reference this element type
          var referencingElements;
          forEach.call(properties, function (property) {
            // :NOTE: using a substring match attr selector here to deal with IE "adding extra quotes in url() attrs"
            referencingElements = svg.querySelectorAll('[' + property + '*="' + currentId + '"]');
            for (var j = 0, referencingElementLen = referencingElements.length; j < referencingElementLen; j++) {
              referencingElements[j].setAttribute(property, 'url(#' + newId + ')');
            }
          });

          elementDefs[i].id = newId;
        }
      });

      // Remove any unwanted/invalid namespaces that might have been added by SVG editing tools
      svg.removeAttribute('xmlns:a');

      // Post page load injected SVGs don't automatically have their script
      // elements run, so we'll need to make that happen, if requested

      // Find then prune the scripts
      var scripts = svg.querySelectorAll('script');
      var scriptsToEval = [];
      var script, scriptType;

      for (var k = 0, scriptsLen = scripts.length; k < scriptsLen; k++) {
        scriptType = scripts[k].getAttribute('type');

        // Only process javascript types.
        // SVG defaults to 'application/ecmascript' for unset types
        if (!scriptType || scriptType === 'application/ecmascript' || scriptType === 'application/javascript') {

          // innerText for IE, textContent for other browsers
          script = scripts[k].innerText || scripts[k].textContent;

          // Stash
          scriptsToEval.push(script);

          // Tidy up and remove the script element since we don't need it anymore
          svg.removeChild(scripts[k]);
        }
      }

      // Run/Eval the scripts if needed
      if (scriptsToEval.length > 0 && (evalScripts === 'always' || (evalScripts === 'once' && !ranScripts[imgUrl]))) {
        for (var l = 0, scriptsToEvalLen = scriptsToEval.length; l < scriptsToEvalLen; l++) {

          // :NOTE: Yup, this is a form of eval, but it is being used to eval code
          // the caller has explictely asked to be loaded, and the code is in a caller
          // defined SVG file... not raw user input.
          //
          // Also, the code is evaluated in a closure and not in the global scope.
          // If you need to put something in global scope, use 'window'
          new Function(scriptsToEval[l])(window); // jshint ignore:line
        }

        // Remember we already ran scripts for this svg
        ranScripts[imgUrl] = true;
      }

      // :WORKAROUND:
      // IE doesn't evaluate <style> tags in SVGs that are dynamically added to the page.
      // This trick will trigger IE to read and use any existing SVG <style> tags.
      //
      // Reference: https://github.com/iconic/SVGInjector/issues/23
      var styleTags = svg.querySelectorAll('style');
      forEach.call(styleTags, function (styleTag) {
        styleTag.textContent += '';
      });

      // Replace the image with the svg
      el.parentNode.replaceChild(svg, el);

      // Now that we no longer need it, drop references
      // to the original element so it can be GC'd
      delete injectedElements[injectedElements.indexOf(el)];
      el = null;

      // Increment the injected count
      injectCount++;

      callback(svg);
    });
  };

  /**
   * SVGInjector
   *
   * Replace the given elements with their full inline SVG DOM elements.
   *
   * :NOTE: We are using get/setAttribute with SVG because the SVG DOM spec differs from HTML DOM and
   * can return other unexpected object types when trying to directly access svg properties.
   * ex: "className" returns a SVGAnimatedString with the class value found in the "baseVal" property,
   * instead of simple string like with HTML Elements.
   *
   * @param {mixes} Array of or single DOM element
   * @param {object} options
   * @param {function} callback
   * @return {object} Instance of SVGInjector
   */
  var SVGInjector = function (elements, options, done) {

    // Options & defaults
    options = options || {};

    // Should we run the scripts blocks found in the SVG
    // 'always' - Run them every time
    // 'once' - Only run scripts once for each SVG
    // [false|'never'] - Ignore scripts
    var evalScripts = options.evalScripts || 'always';

    // Location of fallback pngs, if desired
    var pngFallback = options.pngFallback || false;

    // Callback to run during each SVG injection, returning the SVG injected
    var eachCallback = options.each;

    // Do the injection...
    if (elements.length !== undefined) {
      var elementsLoaded = 0;
      forEach.call(elements, function (element) {
        injectElement(element, evalScripts, pngFallback, function (svg) {
          if (eachCallback && typeof eachCallback === 'function') eachCallback(svg);
          if (done && elements.length === ++elementsLoaded) done(elementsLoaded);
        });
      });
    }
    else {
      if (elements) {
        injectElement(elements, evalScripts, pngFallback, function (svg) {
          if (eachCallback && typeof eachCallback === 'function') eachCallback(svg);
          if (done) done(1);
          elements = null;
        });
      }
      else {
        if (done) done(0);
      }
    }
  };

  /* global module, exports: true, define */
  // Node.js or CommonJS
  if (typeof module === 'object' && typeof module.exports === 'object') {
    module.exports = exports = SVGInjector;
  }
  // AMD support
  else if (typeof define === 'function' && define.amd) {
    define(function () {
      return SVGInjector;
    });
  }
  // Otherwise, attach to window as global
  else if (typeof window === 'object') {
    window.SVGInjector = SVGInjector;
  }
  /* global -module, -exports, -define */

}(window, document));
<commit_msg>Fix compatibility with prototype.js 1.7.0.0<commit_after>/**
 * SVGInjector v1.1.3 - Fast, caching, dynamic inline SVG DOM injection library
 * https://github.com/iconic/SVGInjector
 *
 * Copyright (c) 2014-2015 Waybury <hello@waybury.com>
 * @license MIT
 */

(function (window, document) {

  'use strict';

  // Environment
  var isLocal = window.location.protocol === 'file:';
  var hasSvgSupport = document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');

  function uniqueClasses(list) {
    list = list.split(' ');

    var hash = {};
    var i = list.length;
    var out = [];

    while (i--) {
      if (!hash.hasOwnProperty(list[i])) {
        hash[list[i]] = 1;
        out.unshift(list[i]);
      }
    }

    return out.join(' ');
  }

  /**
   * cache (or polyfill for <= IE8) Array.forEach()
   * source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
   */
  var forEach = Array.prototype.forEach || function (fn, scope) {
    if (this === void 0 || this === null || typeof fn !== 'function') {
      throw new TypeError();
    }

    /* jshint bitwise: false */
    var i, len = this.length >>> 0;
    /* jshint bitwise: true */

    for (i = 0; i < len; ++i) {
      if (i in this) {
        fn.call(scope, this[i], i, this);
      }
    }
  };

  var arrayFilter = function(fun) {
    'use strict';

    if (this === void 0 || this === null) {
      throw new TypeError();
    }

    var t = Object(this);
    var len = t.length >>> 0;
    if (typeof fun !== 'function') {
      throw new TypeError();
    }

    var res = [];
    var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
    for (var i = 0; i < len; i++) {
      if (i in t) {
        var val = t[i];
        if (fun.call(thisArg, val, i, t)) {
          res.push(val);
        }
      }
    }

    return res;
  };

  // SVG Cache
  var svgCache = {};

  var injectCount = 0;
  var injectedElements = [];

  // Request Queue
  var requestQueue = [];

  // Script running status
  var ranScripts = {};

  var cloneSvg = function (sourceSvg) {
    return sourceSvg.cloneNode(true);
  };

  var queueRequest = function (url, callback) {
    requestQueue[url] = requestQueue[url] || [];
    requestQueue[url].push(callback);
  };

  var processRequestQueue = function (url) {
    for (var i = 0, len = requestQueue[url].length; i < len; i++) {
      // Make these calls async so we avoid blocking the page/renderer
      /* jshint loopfunc: true */
      (function (index) {
        setTimeout(function () {
          requestQueue[url][index](cloneSvg(svgCache[url]));
        }, 0);
      })(i);
      /* jshint loopfunc: false */
    }
  };

  var loadSvg = function (url, callback) {
    if (svgCache[url] !== undefined) {
      if (svgCache[url] instanceof SVGSVGElement) {
        // We already have it in cache, so use it
        callback(cloneSvg(svgCache[url]));
      }
      else {
        // We don't have it in cache yet, but we are loading it, so queue this request
        queueRequest(url, callback);
      }
    }
    else {

      if (!window.XMLHttpRequest) {
        callback('Browser does not support XMLHttpRequest');
        return false;
      }

      // Seed the cache to indicate we are loading this URL already
      svgCache[url] = {};
      queueRequest(url, callback);

      var httpRequest = new XMLHttpRequest();

      httpRequest.onreadystatechange = function () {
        // readyState 4 = complete
        if (httpRequest.readyState === 4) {

          // Handle status
          if (httpRequest.status === 404 || httpRequest.responseXML === null) {
            callback('Unable to load SVG file: ' + url);

            if (isLocal) callback('Note: SVG injection ajax calls do not work locally without adjusting security setting in your browser. Or consider using a local webserver.');

            callback();
            return false;
          }

          // 200 success from server, or 0 when using file:// protocol locally
          if (httpRequest.status === 200 || (isLocal && httpRequest.status === 0)) {

            /* globals Document */
            if (httpRequest.responseXML instanceof Document) {
              // Cache it
              svgCache[url] = httpRequest.responseXML.documentElement;
            }
            /* globals -Document */

            // IE9 doesn't create a responseXML Document object from loaded SVG,
            // and throws a "DOM Exception: HIERARCHY_REQUEST_ERR (3)" error when injected.
            //
            // So, we'll just create our own manually via the DOMParser using
            // the the raw XML responseText.
            //
            // :NOTE: IE8 and older doesn't have DOMParser, but they can't do SVG either, so...
            else if (DOMParser && (DOMParser instanceof Function)) {
              var xmlDoc;
              try {
                var parser = new DOMParser();
                xmlDoc = parser.parseFromString(httpRequest.responseText, 'text/xml');
              }
              catch (e) {
                xmlDoc = undefined;
              }

              if (!xmlDoc || xmlDoc.getElementsByTagName('parsererror').length) {
                callback('Unable to parse SVG file: ' + url);
                return false;
              }
              else {
                // Cache it
                svgCache[url] = xmlDoc.documentElement;
              }
            }

            // We've loaded a new asset, so process any requests waiting for it
            processRequestQueue(url);
          }
          else {
            callback('There was a problem injecting the SVG: ' + httpRequest.status + ' ' + httpRequest.statusText);
            return false;
          }
        }
      };

      httpRequest.open('GET', url);

      // Treat and parse the response as XML, even if the
      // server sends us a different mimetype
      if (httpRequest.overrideMimeType) httpRequest.overrideMimeType('text/xml');

      httpRequest.send();
    }
  };

  // Inject a single element
  var injectElement = function (el, evalScripts, pngFallback, callback) {

    // Grab the src or data-src attribute
    var imgUrl = el.getAttribute('data-src') || el.getAttribute('src');

    // We can only inject SVG
    if (!(/\.svg/i).test(imgUrl)) {
      callback('Attempted to inject a file with a non-svg extension: ' + imgUrl);
      return;
    }

    // If we don't have SVG support try to fall back to a png,
    // either defined per-element via data-fallback or data-png,
    // or globally via the pngFallback directory setting
    if (!hasSvgSupport) {
      var perElementFallback = el.getAttribute('data-fallback') || el.getAttribute('data-png');

      // Per-element specific PNG fallback defined, so use that
      if (perElementFallback) {
        el.setAttribute('src', perElementFallback);
        callback(null);
      }
      // Global PNG fallback directoriy defined, use the same-named PNG
      else if (pngFallback) {
        el.setAttribute('src', pngFallback + '/' + imgUrl.split('/').pop().replace('.svg', '.png'));
        callback(null);
      }
      // um...
      else {
        callback('This browser does not support SVG and no PNG fallback was defined.');
      }

      return;
    }

    // Make sure we aren't already in the process of injecting this element to
    // avoid a race condition if multiple injections for the same element are run.
    // :NOTE: Using indexOf() only _after_ we check for SVG support and bail,
    // so no need for IE8 indexOf() polyfill
    if (injectedElements.indexOf(el) !== -1) {
      return;
    }

    // Remember the request to inject this element, in case other injection
    // calls are also trying to replace this element before we finish
    injectedElements.push(el);

    // Try to avoid loading the orginal image src if possible.
    el.setAttribute('src', '');

    // Load it up
    loadSvg(imgUrl, function (svg) {

      if (typeof svg === 'undefined' || typeof svg === 'string') {
        callback(svg);
        return false;
      }

      var imgId = el.getAttribute('id');
      if (imgId) {
        svg.setAttribute('id', imgId);
      }

      var imgTitle = el.getAttribute('title');
      if (imgTitle) {
        svg.setAttribute('title', imgTitle);
      }

      // Concat the SVG classes + 'injected-svg' + the img classes
      var classMerge = [].concat(svg.getAttribute('class') || [], 'injected-svg', el.getAttribute('class') || []).join(' ');
      svg.setAttribute('class', uniqueClasses(classMerge));

      var imgStyle = el.getAttribute('style');
      if (imgStyle) {
        svg.setAttribute('style', imgStyle);
      }

      // Copy all the data elements to the svg
      var imgData = arrayFilter.call(el.attributes, function (at) {
        return (/^data-\w[\w\-]*$/).test(at.name);
      });
      forEach.call(imgData, function (dataAttr) {
        if (dataAttr.name && dataAttr.value) {
          svg.setAttribute(dataAttr.name, dataAttr.value);
        }
      });

      // Make sure any internally referenced clipPath ids and their
      // clip-path references are unique.
      //
      // This addresses the issue of having multiple instances of the
      // same SVG on a page and only the first clipPath id is referenced.
      //
      // Browsers often shortcut the SVG Spec and don't use clipPaths
      // contained in parent elements that are hidden, so if you hide the first
      // SVG instance on the page, then all other instances lose their clipping.
      // Reference: https://bugzilla.mozilla.org/show_bug.cgi?id=376027

      // Handle all defs elements that have iri capable attributes as defined by w3c: http://www.w3.org/TR/SVG/linking.html#processingIRI
      // Mapping IRI addressable elements to the properties that can reference them:
      var iriElementsAndProperties = {
        'clipPath': ['clip-path'],
        'color-profile': ['color-profile'],
        'cursor': ['cursor'],
        'filter': ['filter'],
        'linearGradient': ['fill', 'stroke'],
        'marker': ['marker', 'marker-start', 'marker-mid', 'marker-end'],
        'mask': ['mask'],
        'pattern': ['fill', 'stroke'],
        'radialGradient': ['fill', 'stroke']
      };

      var element, elementDefs, properties, currentId, newId;
      Object.keys(iriElementsAndProperties).forEach(function (key) {
        element = key;
        properties = iriElementsAndProperties[key];

        elementDefs = svg.querySelectorAll('defs ' + element + '[id]');
        for (var i = 0, elementsLen = elementDefs.length; i < elementsLen; i++) {
          currentId = elementDefs[i].id;
          newId = currentId + '-' + injectCount;

          // All of the properties that can reference this element type
          var referencingElements;
          forEach.call(properties, function (property) {
            // :NOTE: using a substring match attr selector here to deal with IE "adding extra quotes in url() attrs"
            referencingElements = svg.querySelectorAll('[' + property + '*="' + currentId + '"]');
            for (var j = 0, referencingElementLen = referencingElements.length; j < referencingElementLen; j++) {
              referencingElements[j].setAttribute(property, 'url(#' + newId + ')');
            }
          });

          elementDefs[i].id = newId;
        }
      });

      // Remove any unwanted/invalid namespaces that might have been added by SVG editing tools
      svg.removeAttribute('xmlns:a');

      // Post page load injected SVGs don't automatically have their script
      // elements run, so we'll need to make that happen, if requested

      // Find then prune the scripts
      var scripts = svg.querySelectorAll('script');
      var scriptsToEval = [];
      var script, scriptType;

      for (var k = 0, scriptsLen = scripts.length; k < scriptsLen; k++) {
        scriptType = scripts[k].getAttribute('type');

        // Only process javascript types.
        // SVG defaults to 'application/ecmascript' for unset types
        if (!scriptType || scriptType === 'application/ecmascript' || scriptType === 'application/javascript') {

          // innerText for IE, textContent for other browsers
          script = scripts[k].innerText || scripts[k].textContent;

          // Stash
          scriptsToEval.push(script);

          // Tidy up and remove the script element since we don't need it anymore
          svg.removeChild(scripts[k]);
        }
      }

      // Run/Eval the scripts if needed
      if (scriptsToEval.length > 0 && (evalScripts === 'always' || (evalScripts === 'once' && !ranScripts[imgUrl]))) {
        for (var l = 0, scriptsToEvalLen = scriptsToEval.length; l < scriptsToEvalLen; l++) {

          // :NOTE: Yup, this is a form of eval, but it is being used to eval code
          // the caller has explictely asked to be loaded, and the code is in a caller
          // defined SVG file... not raw user input.
          //
          // Also, the code is evaluated in a closure and not in the global scope.
          // If you need to put something in global scope, use 'window'
          new Function(scriptsToEval[l])(window); // jshint ignore:line
        }

        // Remember we already ran scripts for this svg
        ranScripts[imgUrl] = true;
      }

      // :WORKAROUND:
      // IE doesn't evaluate <style> tags in SVGs that are dynamically added to the page.
      // This trick will trigger IE to read and use any existing SVG <style> tags.
      //
      // Reference: https://github.com/iconic/SVGInjector/issues/23
      var styleTags = svg.querySelectorAll('style');
      forEach.call(styleTags, function (styleTag) {
        styleTag.textContent += '';
      });

      // Replace the image with the svg
      el.parentNode.replaceChild(svg, el);

      // Now that we no longer need it, drop references
      // to the original element so it can be GC'd
      delete injectedElements[injectedElements.indexOf(el)];
      el = null;

      // Increment the injected count
      injectCount++;

      callback(svg);
    });
  };

  /**
   * SVGInjector
   *
   * Replace the given elements with their full inline SVG DOM elements.
   *
   * :NOTE: We are using get/setAttribute with SVG because the SVG DOM spec differs from HTML DOM and
   * can return other unexpected object types when trying to directly access svg properties.
   * ex: "className" returns a SVGAnimatedString with the class value found in the "baseVal" property,
   * instead of simple string like with HTML Elements.
   *
   * @param {mixes} Array of or single DOM element
   * @param {object} options
   * @param {function} callback
   * @return {object} Instance of SVGInjector
   */
  var SVGInjector = function (elements, options, done) {

    // Options & defaults
    options = options || {};

    // Should we run the scripts blocks found in the SVG
    // 'always' - Run them every time
    // 'once' - Only run scripts once for each SVG
    // [false|'never'] - Ignore scripts
    var evalScripts = options.evalScripts || 'always';

    // Location of fallback pngs, if desired
    var pngFallback = options.pngFallback || false;

    // Callback to run during each SVG injection, returning the SVG injected
    var eachCallback = options.each;

    // Do the injection...
    if (elements.length !== undefined) {
      var elementsLoaded = 0;
      forEach.call(elements, function (element) {
        injectElement(element, evalScripts, pngFallback, function (svg) {
          if (eachCallback && typeof eachCallback === 'function') eachCallback(svg);
          if (done && elements.length === ++elementsLoaded) done(elementsLoaded);
        });
      });
    }
    else {
      if (elements) {
        injectElement(elements, evalScripts, pngFallback, function (svg) {
          if (eachCallback && typeof eachCallback === 'function') eachCallback(svg);
          if (done) done(1);
          elements = null;
        });
      }
      else {
        if (done) done(0);
      }
    }
  };

  /* global module, exports: true, define */
  // Node.js or CommonJS
  if (typeof module === 'object' && typeof module.exports === 'object') {
    module.exports = exports = SVGInjector;
  }
  // AMD support
  else if (typeof define === 'function' && define.amd) {
    define(function () {
      return SVGInjector;
    });
  }
  // Otherwise, attach to window as global
  else if (typeof window === 'object') {
    window.SVGInjector = SVGInjector;
  }
  /* global -module, -exports, -define */

}(window, document));
<|endoftext|>


<commit_before>define(['dojo/_base/declare',
			'require',
			"dojo/_base/lang",
			'dijit/_WidgetBase',
			'dijit/_TemplatedMixin',
			"dojox/gfx",
			"dojox/gfx/utils",
			'dojo/text!./resources/Icon.html'
], function(declare, require, lang, _WidgetBase, _TemplatedMixin, gfx, gfxUtils, template) {

	return declare(require.module.mid, [_WidgetBase, _TemplatedMixin], {

		templateString: template,

		baseClass: "icon",

		item: null,

		postCreate: function() {
			this.deserialize({
				parentNode: this.gfxSvg,
				height: this.item.cheight,
				width: this.item.cwidth,
				json: this.item.ciconjson
			});
			this.inherited(arguments);
		},

		deserialize: function(args) {
			// summary: creating a gfx surface to place a deserialized group of shapes.
			this._surface = gfx.createSurface(args.parentNode, args.width, args.height);
			this._surface.whenLoaded(lang.hitch(this, function() {
				this._group = gfxUtils.deserialize(this._surface, JSON.parse(args.json));
			}));
		}
	});
});
<commit_msg>Update Icon.js<commit_after>define(['dojo/_base/declare',
	'require',
	"dojo/_base/lang",
	'dijit/_WidgetBase',
	'dijit/_TemplatedMixin',
	"dojox/gfx",
	"dojox/gfx/utils",
	'dojo/text!./resources/Icon.html'
], function(declare, require, lang, _WidgetBase, _TemplatedMixin, gfx, gfxUtils, template) {

	return declare(require.module.mid, [_WidgetBase, _TemplatedMixin], {

		templateString: template,

		baseClass: "icon",

		item: null,

		postCreate: function() {
			this.deserialize({
				parentNode: this.gfxSvg,
				height: this.item.cheight,
				width: this.item.cwidth,
				json: this.item.ciconjson
			});
			this.inherited(arguments);
		},

		deserialize: function(args) {
			// summary: creating a gfx surface to place a deserialized group of shapes.
			this._surface = gfx.createSurface(args.parentNode, args.width, args.height);
			this._surface.whenLoaded(lang.hitch(this, function() {
				this._group = gfxUtils.deserialize(this._surface, JSON.parse(args.json));
			}));
		}
	});
});
<|endoftext|>


<commit_before>gelato = (function() {

    /**
     * @property config
     * @type {Object}
     */
    var config = {
        attributes: {
            name: '@@application-name',
            version: '@@application-version'
        },
        fonts: {},
        modules: [
            {
                name: 'core/modules/GelatoLibraries'
            },
            {
                name: 'core/modules/GelatoTests'
            },
            {
                name: 'modules/Application'
            },
            {
                name: 'modules/Router'
            }
        ],
        paths: {
            //framework libraries
            async: 'core/libraries/async-0.9.2',
            backbone: 'core/libraries/backbone-1.1.2',
            'backbone.routefilter': 'core/libraries/backbone.routefilter-0.2.0',
            bootstrap: 'core/libraries/bootstrap-3.3.4',
            'bootstrap.notify': 'core/libraries/bootstrap.notify-3.1.3',
            'bootstrap.switch': 'core/libraries/bootstrap.switch-3.3.2',
            fastclick: 'core/libraries/fastclick-1.0.6',
            handlebars: 'core/libraries/handlebars-3.0.3',
            jasmine: 'core/libraries/jasmine-2.3.3',
            'jasmine.html': 'core/libraries/jasmine.html-2.3.3',
            jquery: 'core/libraries/jquery-1.11.3',
            'jquery.chosen': 'core/libraries/jquery.chosen-1.4.2',
            'jquery.mobile': 'core/libraries/jquery.mobile.custom-1.4.5',
            'jquery.ui': 'core/libraries/jquery.ui.custom-1.11.4',
            'lzstring': 'core/libraries/lzstring-1.4.3',
            modernizr: 'core/libraries/modernizr.custom-2.8.3',
            moment: 'core/libraries/moment-2.10.3',
            'moment.timezone': 'core/libraries/moment.timezone-0.3.1',
            react: 'core/libraries/react-0.13.3',
            'require.i18n': 'core/libraries/require.i18n-2.0.5',
            'require.text': 'core/libraries/require.text-2.0.12',
            underscore: 'core/libraries/lodash.compat-3.8.0',
            webfont: 'core/libraries/webfontloader-1.5.18'
        },
        plugins: [],
        shim: {
            //framework shims
            backbone: ['jquery', 'underscore'],
            'backbone.routefilter': ['backbone'],
            bootstrap: ['jquery'],
            'bootstrap.notify': ['bootstrap'],
            'bootstrap.switch': ['bootstrap'],
            'jasmine.html': ['jasmine'],
            'jquery.chosen': ['jquery'],
            'jquery.mobile': ['jquery'],
            'jquery.notify': ['jquery'],
            'jquery.ui': ['jquery'],
            'moment.timezones': ['moment']
        }
    };

    /**
     * @property version
     * @type {String}
     */
    var version = '@@framework-version';

    /**
     * @method addCordovaPlugins
     * @param {Array|Object} [plugins]
     */
    function addCordovaPlugins(plugins) {
        config.plugins = mergeArrays(config.plugins, sanitizeArray(plugins));
    }

    /**
     * @method addFonts
     * @param {Object} [fonts]
     */
    function addFonts(fonts) {
        config.fonts = mergeObjects(config.fonts, sanitizeObject(fonts));
    }

    /**
     * @method addModules
     * @param {Array|Object} [modules]
     */
    function addModules(modules) {
        config.modules = mergeArrays(config.modules, sanitizeArray(modules));
    }

    /**
     * @method addPaths
     * @param {Object} [paths]
     */
    function addPaths(paths) {
        config.paths = mergeObjects(config.paths, sanitizeObject(paths));
    }

    /**
     * @method addShim
     * @param {Object} [shim]
     */
    function addShim(shim) {
        config.shim = mergeObjects(config.shim, sanitizeObject(shim));
    }

    /**
     * @method getConfig
     * @param {String} [key]
     * @returns {Array|Object|String}
     */
    function getConfig(key) {
        return key ? config[key] : config;
    }

    /**
     * @method getPushState
     * @returns {Boolean}
     */
    function getPushState() {
        return isCordova() || isLocal() ? false : true;
    }

    /**
     * @method getRoot
     * @returns {String}
     */
    function getRoot() {
        return location.pathname;
    }

    /**
     * @method isCordova
     * @returns {Boolean}
     */
    function isCordova() {
        return location.protocol === 'file:';
    }

    /**
     * @method isLocal
     * @returns {Boolean}
     */
    function isLocal() {
        return location.hostname === 'localhost' || location.port !== '';
    }

    /**
     * @method mergeArrays
     * @param {Array} array1
     * @param {Array} array2
     * @returns {Array}
     */
    function mergeArrays (array1, array2) {
        return array1.concat(array2);
    }

    /**
     * @method mergeObjects
     * @param {Object} object1
     * @param {Object} object2
     * @returns {Object}
     */
    function mergeObjects(object1, object2) {
        for (var key in object2) {
            if (object1[key] && object2[key].constructor === Object) {
                mergeObjects(object1[key], object2[key]);
            } else {
                object1[key] = object2[key];
            }
        }
        return object1;
    }

    /**
     * @method sanitizeArray
     * @param {Array} array
     */
    function sanitizeArray(array) {
        return Array.isArray(array) ? array : [];
    }

    /**
     * @method sanitizeObject
     * @param {Object} object
     */
    function sanitizeObject(object) {
        return Object.prototype.toString.call(object) === '[object Object]' ? object : {};
    }

    return {
        addCordovaPlugins: addCordovaPlugins,
        addFonts: addFonts,
        addModules: addModules,
        addPaths: addPaths,
        addShim: addShim,
        getConfig: getConfig,
        getPlatform: getPlatform,
        getPushState: getPushState,
        getRoot: getRoot,
        isCordova: isCordova,
        isLocal: isLocal,
        version: version
    };

})();<commit_msg>Removed gelato return for the get platform function.<commit_after>gelato = (function() {

    /**
     * @property config
     * @type {Object}
     */
    var config = {
        attributes: {
            name: '@@application-name',
            version: '@@application-version'
        },
        fonts: {},
        modules: [
            {
                name: 'core/modules/GelatoLibraries'
            },
            {
                name: 'core/modules/GelatoTests'
            },
            {
                name: 'modules/Application'
            },
            {
                name: 'modules/Router'
            }
        ],
        paths: {
            //framework libraries
            async: 'core/libraries/async-0.9.2',
            backbone: 'core/libraries/backbone-1.1.2',
            'backbone.routefilter': 'core/libraries/backbone.routefilter-0.2.0',
            bootstrap: 'core/libraries/bootstrap-3.3.4',
            'bootstrap.notify': 'core/libraries/bootstrap.notify-3.1.3',
            'bootstrap.switch': 'core/libraries/bootstrap.switch-3.3.2',
            fastclick: 'core/libraries/fastclick-1.0.6',
            handlebars: 'core/libraries/handlebars-3.0.3',
            jasmine: 'core/libraries/jasmine-2.3.3',
            'jasmine.html': 'core/libraries/jasmine.html-2.3.3',
            jquery: 'core/libraries/jquery-1.11.3',
            'jquery.chosen': 'core/libraries/jquery.chosen-1.4.2',
            'jquery.mobile': 'core/libraries/jquery.mobile.custom-1.4.5',
            'jquery.ui': 'core/libraries/jquery.ui.custom-1.11.4',
            'lzstring': 'core/libraries/lzstring-1.4.3',
            modernizr: 'core/libraries/modernizr.custom-2.8.3',
            moment: 'core/libraries/moment-2.10.3',
            'moment.timezone': 'core/libraries/moment.timezone-0.3.1',
            react: 'core/libraries/react-0.13.3',
            'require.i18n': 'core/libraries/require.i18n-2.0.5',
            'require.text': 'core/libraries/require.text-2.0.12',
            underscore: 'core/libraries/lodash.compat-3.8.0',
            webfont: 'core/libraries/webfontloader-1.5.18'
        },
        plugins: [],
        shim: {
            //framework shims
            backbone: ['jquery', 'underscore'],
            'backbone.routefilter': ['backbone'],
            bootstrap: ['jquery'],
            'bootstrap.notify': ['bootstrap'],
            'bootstrap.switch': ['bootstrap'],
            'jasmine.html': ['jasmine'],
            'jquery.chosen': ['jquery'],
            'jquery.mobile': ['jquery'],
            'jquery.notify': ['jquery'],
            'jquery.ui': ['jquery'],
            'moment.timezones': ['moment']
        }
    };

    /**
     * @property version
     * @type {String}
     */
    var version = '@@framework-version';

    /**
     * @method addCordovaPlugins
     * @param {Array|Object} [plugins]
     */
    function addCordovaPlugins(plugins) {
        config.plugins = mergeArrays(config.plugins, sanitizeArray(plugins));
    }

    /**
     * @method addFonts
     * @param {Object} [fonts]
     */
    function addFonts(fonts) {
        config.fonts = mergeObjects(config.fonts, sanitizeObject(fonts));
    }

    /**
     * @method addModules
     * @param {Array|Object} [modules]
     */
    function addModules(modules) {
        config.modules = mergeArrays(config.modules, sanitizeArray(modules));
    }

    /**
     * @method addPaths
     * @param {Object} [paths]
     */
    function addPaths(paths) {
        config.paths = mergeObjects(config.paths, sanitizeObject(paths));
    }

    /**
     * @method addShim
     * @param {Object} [shim]
     */
    function addShim(shim) {
        config.shim = mergeObjects(config.shim, sanitizeObject(shim));
    }

    /**
     * @method getConfig
     * @param {String} [key]
     * @returns {Array|Object|String}
     */
    function getConfig(key) {
        return key ? config[key] : config;
    }

    /**
     * @method getPushState
     * @returns {Boolean}
     */
    function getPushState() {
        return isCordova() || isLocal() ? false : true;
    }

    /**
     * @method getRoot
     * @returns {String}
     */
    function getRoot() {
        return location.pathname;
    }

    /**
     * @method isCordova
     * @returns {Boolean}
     */
    function isCordova() {
        return location.protocol === 'file:';
    }

    /**
     * @method isLocal
     * @returns {Boolean}
     */
    function isLocal() {
        return location.hostname === 'localhost' || location.port !== '';
    }

    /**
     * @method mergeArrays
     * @param {Array} array1
     * @param {Array} array2
     * @returns {Array}
     */
    function mergeArrays (array1, array2) {
        return array1.concat(array2);
    }

    /**
     * @method mergeObjects
     * @param {Object} object1
     * @param {Object} object2
     * @returns {Object}
     */
    function mergeObjects(object1, object2) {
        for (var key in object2) {
            if (object1[key] && object2[key].constructor === Object) {
                mergeObjects(object1[key], object2[key]);
            } else {
                object1[key] = object2[key];
            }
        }
        return object1;
    }

    /**
     * @method sanitizeArray
     * @param {Array} array
     */
    function sanitizeArray(array) {
        return Array.isArray(array) ? array : [];
    }

    /**
     * @method sanitizeObject
     * @param {Object} object
     */
    function sanitizeObject(object) {
        return Object.prototype.toString.call(object) === '[object Object]' ? object : {};
    }

    return {
        addCordovaPlugins: addCordovaPlugins,
        addFonts: addFonts,
        addModules: addModules,
        addPaths: addPaths,
        addShim: addShim,
        getConfig: getConfig,
        getPushState: getPushState,
        getRoot: getRoot,
        isCordova: isCordova,
        isLocal: isLocal,
        version: version
    };

})();<|endoftext|>


<commit_before>/*===========================
  Swipe-it v1.1.1 / https://github.com/tri613/swipe-it
  ===========================*/
!function(t,n,e){"use strict";function o(t){function e(){i("touchstart",h,E),i("touchmove",l,E),i("touchend",p,E),i("mousedown",f,E)}function o(){m=!1,w=!1,d=!1,g=!1,s=!1}function f(t){m=t.clientX,w=t.clientY,s=t.target,i("mousemove",a,r),i("mouseup",v,r)}function a(t){t.preventDefault(),m&&w&&(d=t.clientX,g=t.clientY)}function v(t){c("mousemove",a,r),c("mouseup",v,r),p()}function h(t){s=t.target,m=t.touches[0].clientX,w=t.touches[0].clientY}function l(t){d=t.touches[0].clientX,g=t.touches[0].clientY}function p(t){if(m&&w&&d&&g){if(Math.abs(m-d)>30){var n=m<d?"swipeRight":"swipeLeft";u(n,s)}if(Math.abs(w-g)>30){var n=w>g?"swipeUp":"swipeDown";u(n,s)}}o()}var m,w,d,g,E=n.querySelectorAll(t);o(),e(),this.on=function(t,n){return i(t,n,E),this}}function i(t,n,e){for(var o=0;o<e.length;o++)e[o].addEventListener(t,n)}function c(t,n,e){for(var o=0;o<e.length;o++)e[o].removeEventListener(t,n)}function u(t,n){var e=new Event(t);if(n.constructor!==Array)n.dispatchEvent(e);else for(var o=0;o<n.length;o++)n[o].dispatchEvent(e)}var r=[t],s=!1;t[e]=o}(window,document,"SwipeIt");
<commit_msg>minified<commit_after>/*===========================
  Swipe-it v1.1.1 / https://github.com/tri613/swipe-it
  ===========================*/
!function(t,e,n){"use strict";function i(t){function n(){o("touchstart",h,E),o("touchmove",l,E),o("touchend",p,E),o("mousedown",f,E)}function i(){w=!1,m=!1,d=!1,g=!1,s=!1}function f(t){w=t.clientX,m=t.clientY,s=t.target,o("mousemove",a,r),o("mouseup",v,r)}function a(t){t.preventDefault(),w&&m&&(d=t.clientX,g=t.clientY)}function v(t){c("mousemove",a,r),c("mouseup",v,r),p()}function h(t){s=t.target,w=t.touches[0].clientX,m=t.touches[0].clientY}function l(t){d=t.touches[0].clientX,g=t.touches[0].clientY}function p(t){if(w&&m&&d&&g){if(Math.abs(w-d)>30){var e=w<d?"swipeRight":"swipeLeft";u(e,s)}if(Math.abs(m-g)>30){var e=m>g?"swipeUp":"swipeDown";u(e,s)}u("swipe",s)}i()}var w,m,d,g,E=e.querySelectorAll(t);i(),n(),this.on=function(t,e){return o(t,e,E),this}}function o(t,e,n){for(var i=0;i<n.length;i++)n[i].addEventListener(t,e)}function c(t,e,n){for(var i=0;i<n.length;i++)n[i].removeEventListener(t,e)}function u(t,e){var n=new Event(t);if(e.constructor!==Array)e.dispatchEvent(n);else for(var i=0;i<e.length;i++)e[i].dispatchEvent(n)}var r=[t],s=!1;t[n]=i}(window,document,"SwipeIt");<|endoftext|>