git » hoplite.git » master » tree

[master] / el.mjs

import { _ } from './datalog.mjs';
import { Input, Formula, input, formula, database, view, watch, captureCreated } from './reactives.mjs';

function el(tag, attrs, kids) {
  let el = document.createElement(tag);
  for (let [k, v] of Object.entries(attrs)) {
    if (v instanceof Input || v instanceof Formula) {
      watch((oldv, newv) => {
        el[k] = newv;
        // el.setAttribute(k, newv);
      })(v);
    } else if (k.startsWith("on")) {
      el.addEventListener(k.slice(2), v);
    } else {
      el.setAttribute(k, v);
    }
  }
  let offset = 0;
  for (let i = 0; i < kids.length; i++) {
    let kid = kids[i];
    if (kid instanceof Input || kid instanceof Formula) {
      let node,
          placeholder = document.createComment("placeholder");
      watch((oldv, newv) => {
        if (!newv) {
          if (node) el.replaceChild(placeholder, node);
          node = placeholder;
        } else if (typeof newv !== "object") {
          if (node && node.nodeType === Node.TEXT_NODE) {
            node.textContent = newv;
          } else {
            let newNode = document.createTextNode(newv);
            if (node) el.replaceChild(newNode, node);
            node = newNode;
          }
        } else if (newv instanceof Node) {
          if (node) el.replaceChild(newv, node);
          node = newv;
        } else {
          console.log(`Unknown child cell content`);
        }
      })(kid);
      el.appendChild(node);
    } else if (kid instanceof Array) {
      // TODO formulas/etc in array
      kid.forEach(x => el.appendChild(x));
      offset += kid.length;
    } else if (kid instanceof Promise) {
      el.appendChild(placeholder);
      kid.then(x => el.replaceChild(x, placeholder));
    } else if (kid instanceof EntityNodes) {
      // TODO error if any EntityNodes siblings for now
      kid.bind(el, offset);
      if ((kids.length - i) > 1) {
        throw new Error(`entities() must be last child`);
      }
    } else {
      el.appendChild(kid);
    }
    offset++;
  }
  return el;
}

const h = new Proxy({}, {
  get: (obj, name) => (attrs = {}, ...kids) => {
    if (typeof attrs === "string"
        || attrs instanceof Node
        || attrs instanceof Input
        || attrs instanceof Formula
        || attrs instanceof EntityNodes) {
      kids.unshift(attrs);
      attrs = {};
    }
    return el(name, attrs, kids.map(kid => {
      return typeof kid === "string" ? document.createTextNode(kid) : kid;
    }));
  }
});

function makeAttrCells(db, eidCell) {
  let cellCache = new Map();
  return new Proxy({}, {
    get: (obj, attrName) => {
      if (cellCache.has(attrName)) {
        return cellCache.get(attrName);
      }
      if (attrName === "_eid") {
        return eidCell;
      }
      let attrValCell = input(null);
      watch((added, removed) => {
        if (added.size()) {
          let [[newv]] = added;
          attrValCell.set(newv);
        } else if (removed.size()) {
          attrValCell.set(null);
        }
      })(view({
        find: [_.attrVal],
        use: [_.eid],
        where: [[_.eid, attrName, _.attrVal]]
      })(db, eidCell));
      cellCache.set(attrName, attrValCell);
      return attrValCell;
    }
  })
}

function getComparator(name) {
  if (name === "asc") {
    return (a, b) => a === b ? 0 : a < b ? -1 : 1;
  } else if (name === "desc") {
    return (a, b) => a === b ? 0 : b < a ? -1 : 1;
  } else {
    throw new Error(`Unknown comparator: ${name}`);
  }
}

function splice(el, offset, i, node) {
  if (el.children.length === offset || el.children.length === offset + i) {
    el.appendChild(node);
  } else {
    el.insertBefore(node, el.children[offset + i]);
  }
}

function binaryInsert(el, offset, nodeToAttrs, sortAttr, node, cmpFunc) {
  let left = offset,
      right = el.children.length,
      middle,
      cmp;
  while (left < right) {
    middle = (left + right) >> 1;
    cmp = cmpFunc(
      nodeToAttrs.get(node)[sortAttr].value,
      nodeToAttrs.get(el.children[middle])[sortAttr].value
    );
    if (cmp > 0) {
      left = middle + 1;
    } else if (cmp < 0) {
      right = middle;
    } else if (cmp === 0) {
      left++;
      break;
    } else {
      throw new Error(`Bad compare value: ${cmp}`);
    }
  }
  splice(el, offset, left, node);
}

function asCell(val, defaultVal) {
  if (val instanceof Input || val instanceof Formula) {
    return val;
  } else if (val === undefined) {
    return input(defaultVal)
  } else {
    return input(val);
  }
}

class EntityNodes {
  constructor(db, sortAttr, sortDir, makeNode) {
    this.db = db;
    this.sortAttr = asCell(sortAttr, "_eid");
    this.sortDir = asCell(sortDir, "asc");
    this.makeNode = makeNode;
  }
  bind(el, offset = 0) {
    let eidToNode = new Map(),
        nodeToEidCell = new Map(),
        nodeToAttrs = new Map(),
        nodeToReactives = new Map(),
        eidToInserter = new Map(),
        pool = [];
    window.pool = pool;
    watch((added, removed) => {
      let node;
      for (let [eid] of removed) {
        node = eidToNode.get(eid);
        for (let r of nodeToReactives.get(node)) {
          r.pause();
        }
        el.removeChild(node);
        eidToNode.delete(eid);
        eidToInserter.get(eid).detach();
        eidToInserter.delete(eid);
        pool.push(node);
      }
      for (let [eid] of added) {
        if (pool.length) {
          node = pool.pop();
          for (let r of nodeToReactives.get(node)) {
            r.resume();
          }
          nodeToEidCell.get(node).set(eid);
          eidToNode.set(eid, node);
        } else {
          let eidCell = input(eid),
              attrs = makeAttrCells(this.db, eidCell),
              reactives = captureCreated(() => {
                node = this.makeNode(attrs);
              });
          nodeToReactives.set(node, reactives);
          eidToNode.set(eid, node);
          nodeToEidCell.set(node, eidCell);
          nodeToAttrs.set(node, attrs);
        }
        // eidToInserter.set(eid, watch(() => {
        //   binaryInsert(el, nodeToAttrs, this.sortAttr.value, node, getComparator(this.sortDir.value));
        // })(nodeToAttrs.get(node)[this.sortAttr.value]));
        eidToInserter.set(eid, watch(() => {
          binaryInsert(el, offset, nodeToAttrs, this.sortAttr.value, node, getComparator(this.sortDir.value));
        })(nodeToAttrs.get(node)[this.sortAttr.value]));
      }
    })(view({find: [_.eid], where: [[_.eid]]})(this.db));
    formula((sortAttr, sortDir) => {
      while (el.children.length - offset) {
        el.removeChild(el.children[offset]);
      }
      for (let [eid, f] of [...eidToInserter]) {
        let node = eidToNode.get(eid);
        f?.detach();
        eidToInserter.set(eid, watch(() => {
          binaryInsert(el, offset, nodeToAttrs, sortAttr, node, getComparator(sortDir));
        })(nodeToAttrs.get(node)[sortAttr]));
      }
    })(this.sortAttr, this.sortDir);
  }
}

function entities(db, sortAttr, sortDir, makeNode) {
  return new EntityNodes(db, sortAttr, sortDir, makeNode);
}

function entitiesBy(db, sortAttr, makeNode) {
  if (!(sortAttr instanceof Input)
      && !(sortAttr instanceof Formula )) {
    sortAttr = input(sortAttr);
  }
  return new EntityNodes(db, makeNode, sortAttr)
}

let nbsp = "\u00A0";

function bindTo(input, cell, event = "change") {
  input.addEventListener(event, ({target: {value}}) => {
    cell.set(value);
  })
  watch((oldv, newv) => {
    input.value = newv ?? "";
  })(cell);
  return input;
}

export { h, entities, nbsp, entitiesBy, bindTo };