Observer

Advanced DOM mutations observer

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://greasyfork.org/scripts/415669-observer/code/Observer.js?version=866817

// ==UserScript==
// @name         Observer
// @description  Advanced DOM mutations observer
// @license      MIT
// @namespace    https://greasyfork.org/users/424058
// @version      1.0.0
// ==/UserScript==

/* jshint esversion: 6 */
 
class Observer {
  constructor(_observedNode, _dynEntities) { // updates of _dynEntities will be detected
    this.entities = _dynEntities;

    this.subs = {
      find(soughtForSubName) {
        for (const entityName in this.entities) {
          const entity = this.entities[entityName];
    
          for (const subName in entity.subs) {
            const sub = entity.subs[subName];
            if (soughtForSubName === subName) { return sub }        
          }
        }
        
        return false;
      }
    }
    
    const observer = new MutationObserver((mutationsList, observer) => {
      if (!Observer.isThereEnabledSubs(_dynEntities)) { return }

      const addedNodes = {
        text: [],
        DOM: []
      };

      for (const mutation of mutationsList) {
        for (const node of mutation.addedNodes) {
          // node.nodeType = 3 means text changes type
          if (node.nodeType === 3) {
            addedNodes.text.push(node);
            continue;
          }

          // node.nodeType = 1 means DOM write changes
          if (node.nodeType === 1) { addedNodes.DOM.push(node) }
        }
      }
      
      for (const entityName in _dynEntities) {
        const entity = _dynEntities[entityName];
        const enabledSubs = Observer.getEnabledSubsFromEntity(entity);

        // simplify subs
        const simpleSubs = {
          textChanges: [],
          DOMchanges: []
        }
        
        for (const subName in enabledSubs) {
          const sub = enabledSubs[subName];

          if (sub.scanTypes.textChanges) { simpleSubs.textChanges.push(sub.cb) }
          if (sub.scanTypes.DOMchanges) { simpleSubs.DOMchanges.push(sub.cb) }
        }

        if (simpleSubs.textChanges.length) {
          const passedNodes = [];

          for (const node of addedNodes.text) {
            // check and pass parent node of the new text node
            if (node.parentNode && Observer.queryNode(entity.query, node.parentNode)) {
              passedNodes.push(node.parentNode);
            }
          }

          if (passedNodes.length) {
            for (const cb of simpleSubs.textChanges) {
              try { cb(passedNodes) } catch(e) { console.error(e) }

              // drop mutations caused by cb (avoids infinite loop if the nodes were changed by cb)
              // cb must not be asynchronous
              observer.takeRecords();
            }
          }
        }

        if (simpleSubs.DOMchanges.length) {

          let duplicatedNodes = [];
          // this check will provide list with duplicates so we need to clean up the list before cb
          for (const node of addedNodes.DOM) {
            
            // the sought-for node can be inside the current, as a child node
            let chunk = Array.from(node.querySelectorAll(entity.query));
            duplicatedNodes = duplicatedNodes.concat(chunk);
            // but also the current node can be the sought-for too
            if (Observer.queryNode(entity.query, node)) { duplicatedNodes.push(node) }
          }

          let passedNodes = [];
          if (duplicatedNodes.length) {

            const arr = duplicatedNodes;

            // remove duplicates
            passedNodes = arr.filter((a, b) => arr.indexOf(a) === b);
          }

          if (passedNodes.length) {
            for (const cb of simpleSubs.DOMchanges) {
              try { cb(passedNodes) } catch(e) { console.error(e) }
              observer.takeRecords();
            }
          }
        }
      }
    });

    // if the sought-for node is already in the DOM
    if (Observer.isThereEnabledSubs(_dynEntities)) {
      for (const entityName in _dynEntities) {
        const entity = _dynEntities[entityName];
        const enabledSubs = Observer.getEnabledSubsFromEntity(entity);
        const callbacks = [];
        
        for (const subName in enabledSubs) {
          const sub = enabledSubs[subName];
          if (sub.scanTypes.DOMfirstScan) { callbacks.push(sub.cb) }
        }
  
        if (callbacks.length) {
          const nodes = _observedNode.querySelectorAll(entity.query);

          if (nodes.length) {
            const nodesArr = Array.prototype.slice.call(nodes);

            for (const cb of callbacks) {
              try { cb(nodesArr) } catch(e) { console.error(e) }
              observer.takeRecords(); 
            }
          }
        }
      }
    }

    observer.observe(_observedNode, { childList: true, subtree: true });
  }

  static getEnabledSubsFromEntity(entity) {
    const result = {};
  
    for (const subName in entity.subs) {
      const sub = entity.subs[subName];
      if (sub.enabled) { result[subName] = sub }
    }
    
    return result;
  }

  static isThereEnabledSubs(entities) {
    for (const entityName in entities) {
      const entity = entities[entityName];
      const enabledSubs = Observer.getEnabledSubsFromEntity(entity);

      if (Object.keys(enabledSubs).length) return true;
    }

    return false;
  }

  static queryNode(query, node) {
    // parseable query example: 'span.claSs.name.te-_st.123#i_-d'
  
    query = query.trim();
  
    let queryObj = {
      tag: (query.match(/^[a-z]+/ig) || [])[0],
      id:  (query.match(/(?:#)(([a-z]|_|-)+)/i) || [])[1],
      classList: []
    }
    
    const classListRegexp = /(?:\.)((\w+|-+|_+)+)/ig;
    let classListMatch = classListRegexp.exec(query);
  
    while (classListMatch != null) {
      queryObj.classList.push(classListMatch[1]);
      classListMatch = classListRegexp.exec(query);
    }
  
    let queryTagCheck = false, queryIdCheck = false, queryClassesCheck = false;
  
    queryTagCheck = queryObj.tag ? (node.localName === queryObj.tag.toLowerCase()) : true;
    queryIdCheck = queryObj.id ? (node.id === queryObj.id) : true;
  
    queryClassesCheck = (function() {
      for (let i = 0; i < queryObj.classList.length; i++) {
        const className = queryObj.classList[i];
  
        if (className.length === 0) { continue }
  
        if (Array.prototype.indexOf.call(node.classList, className) == -1) {
          return false;
        }
      }
  
      return true;
    }());
  
    if (queryTagCheck && queryIdCheck && queryClassesCheck) { return true }
    return false;
  }
}