solverforge-ui 0.4.1

Frontend component library for SolverForge constraint-optimization applications
Documentation
class FakeNode {}

class FakeTextNode extends FakeNode {
  constructor(text) {
    super();
    this.parentNode = null;
    this.ownerDocument = null;
    this.nodeType = 3;
    this._text = String(text);
  }

  get textContent() {
    return this._text;
  }

  set textContent(value) {
    this._text = String(value);
  }
}

class FakeClassList {
  constructor(owner) {
    this.owner = owner;
    this.values = new Set();
  }

  add(...tokens) {
    tokens.forEach((token) => {
      if (token) this.values.add(token);
    });
    this.owner._syncClassName();
  }

  remove(...tokens) {
    tokens.forEach((token) => this.values.delete(token));
    this.owner._syncClassName();
  }

  contains(token) {
    return this.values.has(token);
  }

  toggle(token, force) {
    if (force === true) {
      this.values.add(token);
      this.owner._syncClassName();
      return true;
    }
    if (force === false) {
      this.values.delete(token);
      this.owner._syncClassName();
      return false;
    }
    if (this.values.has(token)) {
      this.values.delete(token);
      this.owner._syncClassName();
      return false;
    }
    this.values.add(token);
    this.owner._syncClassName();
    return true;
  }

  setFromString(value) {
    this.values = new Set(String(value || '').split(/\s+/).filter(Boolean));
    this.owner._syncClassName();
  }
}

class FakeElement extends FakeNode {
  constructor(tagName, namespaceURI) {
    super();
    this.tagName = String(tagName).toUpperCase();
    this.namespaceURI = namespaceURI || null;
    this.childNodes = [];
    this.parentNode = null;
    this.ownerDocument = null;
    this.attributes = {};
    this.style = {};
    this.dataset = {};
    this.eventListeners = {};
    this.classList = new FakeClassList(this);
    this._className = '';
    this._id = '';
    this._innerHTML = '';
    this.clientWidth = 1024;
    this.clientHeight = 768;
    this.offsetWidth = 1024;
    this.offsetHeight = 768;
  }

  _syncClassName() {
    this._className = Array.from(this.classList.values).join(' ');
  }

  get className() {
    return this._className;
  }

  set className(value) {
    this.classList.setFromString(value);
  }

  get id() {
    return this._id;
  }

  set id(value) {
    this._id = String(value);
    this.attributes.id = this._id;
  }

  get innerHTML() {
    return this._innerHTML;
  }

  set innerHTML(value) {
    this._innerHTML = String(value);
    this.childNodes = [];
  }

  get children() {
    return this.childNodes.filter((child) => child instanceof FakeElement);
  }

  get firstChild() {
    return this.childNodes[0] || null;
  }

  get lastChild() {
    return this.childNodes[this.childNodes.length - 1] || null;
  }

  get textContent() {
    if (this.childNodes.length === 0) return '';
    return this.childNodes.map((child) => child.textContent).join('');
  }

  set textContent(value) {
    this._innerHTML = '';
    this.childNodes = [new FakeTextNode(value)];
    this.childNodes[0].parentNode = this;
  }

  appendChild(child) {
    if (child == null) return child;
    child.parentNode = this;
    child.ownerDocument = this.ownerDocument;
    this.childNodes.push(child);
    this._innerHTML = '';
    return child;
  }

  removeChild(child) {
    this.childNodes = this.childNodes.filter((candidate) => candidate !== child);
    child.parentNode = null;
    return child;
  }

  setAttribute(name, value) {
    if (name === 'class') {
      this.className = value;
      return;
    }
    if (name === 'id') {
      this.id = value;
      return;
    }
    this.attributes[name] = String(value);
  }

  addEventListener(type, handler) {
    if (!this.eventListeners[type]) this.eventListeners[type] = [];
    this.eventListeners[type].push(handler);
  }

  dispatchEvent(event) {
    var type = typeof event === 'string' ? event : event.type;
    (this.eventListeners[type] || []).forEach((handler) => handler({ target: this, type: type }));
  }

  click() {
    this.dispatchEvent('click');
  }

  querySelector(selector) {
    return this.querySelectorAll(selector)[0] || null;
  }

  querySelectorAll(selector) {
    var matches = [];
    walk(this, function (node) {
      if (node instanceof FakeElement && matchesSelector(node, selector)) matches.push(node);
    });
    return matches;
  }

  getBoundingClientRect() {
    return {
      width: this.clientWidth,
      height: this.clientHeight,
      top: 0,
      left: 0,
      right: this.clientWidth,
      bottom: this.clientHeight,
    };
  }
}

class FakeDocument {
  constructor() {
    this.body = this.createElement('body');
  }

  createElement(tagName) {
    const element = new FakeElement(tagName);
    element.ownerDocument = this;
    return element;
  }

  createElementNS(namespaceURI, tagName) {
    const element = new FakeElement(tagName, namespaceURI);
    element.ownerDocument = this;
    return element;
  }

  createTextNode(text) {
    return new FakeTextNode(text);
  }

  getElementById(id) {
    return this.body.querySelector('#' + id);
  }

  querySelector(selector) {
    return this.body.querySelector(selector);
  }

  querySelectorAll(selector) {
    return this.body.querySelectorAll(selector);
  }
}

function walk(node, visit) {
  node.childNodes.forEach((child) => {
    visit(child);
    if (child instanceof FakeElement) walk(child, visit);
  });
}

function matchesSelector(node, selector) {
  var dataMatch = selector.match(/^\[data-([a-z0-9-]+)=\"([^\"]+)\"\]$/i);
  if (dataMatch) {
    return node.dataset[toCamel(dataMatch[1])] === dataMatch[2];
  }
  if (selector.startsWith('.')) return node.classList.contains(selector.slice(1));
  if (selector.startsWith('#')) return node.id === selector.slice(1);
  return node.tagName.toLowerCase() === selector.toLowerCase();
}

function toCamel(value) {
  return value.replace(/-([a-z])/g, function (_, letter) {
    return letter.toUpperCase();
  });
}

function createDom() {
  const document = new FakeDocument();
  return { document, window: { document }, Node: FakeNode };
}

module.exports = { createDom, FakeElement, FakeNode, FakeTextNode };