MutationObserver-impl.js 3.51 KB
"use strict";

const { wrapperForImpl } = require("../generated/utils");

// If we were to implement the MutationObserver by spec, the MutationObservers will not be collected by the GC because
// all the MO are kept in a mutation observer list (https://github.com/jsdom/jsdom/pull/2398/files#r238123889). The
// mutation observer list is primarily used to invoke the mutation observer callback in the same order than the
// mutation observer creation.
// In order to get around this issue, we will assign an increasing id for each mutation observer, this way we would be
// able to invoke the callback in the creation order without having to keep a list of all the mutation observers.
let mutationObserverId = 0;

// https://dom.spec.whatwg.org/#mutationobserver
class MutationObserverImpl {
  // https://dom.spec.whatwg.org/#dom-mutationobserver-mutationobserver
  constructor(globalObject, args) {
    const [callback] = args;

    this._callback = callback;
    this._nodeList = [];
    this._recordQueue = [];

    this._id = ++mutationObserverId;
  }

  // https://dom.spec.whatwg.org/#dom-mutationobserver-observe
  observe(target, options) {
    if (("attributeOldValue" in options || "attributeFilter" in options) && !("attributes" in options)) {
      options.attributes = true;
    }

    if ("characterDataOldValue" in options & !("characterData" in options)) {
      options.characterData = true;
    }

    if (!options.childList && !options.attributes && !options.characterData) {
      throw new TypeError("The options object must set at least one of 'attributes', 'characterData', or 'childList' " +
        "to true.");
    } else if (options.attributeOldValue && !options.attributes) {
      throw new TypeError("The options object may only set 'attributeOldValue' to true when 'attributes' is true or " +
        "not present.");
    } else if (("attributeFilter" in options) && !options.attributes) {
      throw new TypeError("The options object may only set 'attributeFilter' when 'attributes' is true or not " +
        "present.");
    } else if (options.characterDataOldValue && !options.characterData) {
      throw new TypeError("The options object may only set 'characterDataOldValue' to true when 'characterData' is " +
        "true or not present.");
    }

    const existingRegisteredObserver = target._registeredObserverList.find(registeredObserver => {
      return registeredObserver.observer === this;
    });

    if (existingRegisteredObserver) {
      for (const node of this._nodeList) {
        node._registeredObserverList = node._registeredObserverList.filter(registeredObserver => {
          return registeredObserver.source !== existingRegisteredObserver;
        });
      }

      existingRegisteredObserver.options = options;
    } else {
      target._registeredObserverList.push({
        observer: this,
        options
      });

      this._nodeList.push(target);
    }
  }

  // https://dom.spec.whatwg.org/#dom-mutationobserver-disconnect
  disconnect() {
    for (const node of this._nodeList) {
      node._registeredObserverList = node._registeredObserverList.filter(registeredObserver => {
        return registeredObserver.observer !== this;
      });
    }

    this._recordQueue = [];
  }

  // https://dom.spec.whatwg.org/#dom-mutationobserver-takerecords
  takeRecords() {
    // TODO: revisit if https://github.com/jsdom/webidl2js/pull/108 gets fixed.
    const records = this._recordQueue.map(wrapperForImpl);
    this._recordQueue = [];

    return records;
  }
}

module.exports = {
  implementation: MutationObserverImpl
};