Greasy Fork is available in English.

Observer

Advanced DOM mutations observer

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyfork.org/scripts/415669/866817/Observer.js

// ==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;
  }
}