Skip to content

Instantly share code, notes, and snippets.

@intrnl
Created June 10, 2021 08:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save intrnl/e0503cf600b4db0085ed70e0c35f7176 to your computer and use it in GitHub Desktop.
Save intrnl/e0503cf600b4db0085ed70e0c35f7176 to your computer and use it in GitHub Desktop.
let curr_document = null;
let curr_parent = null;
let curr_node = null;
let curr_focus_path = null;
function assert (statement, message) {
if (!statement) throw new Error(message);
}
function get_object_keys (...objects) {
return new Set(objects.flatMap((object) => Object.keys(object)));
}
/// Node traversal
function get_next_node () {
return curr_node ? curr_node.nextSibling : curr_parent.firstChild;
}
function clear_node (parent, start, end) {
let child = start;
while (child !== end) {
let next = child.nextSibling;
parent.removeChild(child);
child = next;
}
}
function enter_node () {
curr_parent = curr_node;
curr_node = null;
}
function leave_node () {
clear_node(curr_parent, get_next_node(), null);
curr_node = curr_parent;
curr_parent = curr_parent.parentNode;
return curr_node;
}
function next_node () {
curr_node = get_next_node();
}
/// Focus path
function get_node_ancestry (node, parent) {
let ancestry = [];
let curr = node;
while (curr !== parent) {
ancestry.push(curr);
curr = curr.parentNode;
}
return ancestry;
}
function get_focused_nodes (parent) {
let active_node = parent.getRootNode().activeElement;
return !active_node || !parent.contains(active_node)
? []
: get_node_ancestry(active_node, parent);
}
/// Node record
function get_node_record (node) {
if (node.$$v) return node.$$v;
return node.$$v = {
tag: node.nodeType === 1 ? node.localName : node.nodeName,
key: undefined,
text: null,
props: {},
};
}
/// Node mutation
function get_parent_namespace (tag, parent) {
if (tag === 'svg') {
return 'http://www.w3.org/2000/svg';
}
else if (tag === 'math') {
return 'http://www.w3.org/1998/Math/MathML';
}
else if (parent == null || get_node_record(parent).tag === 'foreignObject') {
return null;
}
return parent.namespaceURI;
}
function create_node (tag, key) {
let node;
if (tag === '#text') {
node = curr_document.createTextNode('');
} else {
let namespace = get_parent_namespace(tag, curr_parent);
if (namespace) {
node = curr_document.createElementNS(namespace, tag);
} else {
node = curr_document.createElement(tag);
}
}
let record = get_node_record(node);
record.key = key;
return node;
}
function find_node (start, tag, key) {
if (!start) return null;
let curr = start;
do {
let record = get_node_record(curr);
if (record && record.tag === tag && record.key === key) {
return curr;
}
} while (key && (curr = curr.nextSibling))
}
function move_node_before (parent, node, reference) {
let insert_reference = node.nextSibling;
let curr = reference;
while (curr !== null && curr !== node) {
let next = curr.nextSibling;
parent.insertBefore(curr, insert_reference);
curr = next;
}
}
function align_node (tag, key) {
next_node();
let prev = find_node(curr_node, tag, key);
let next = prev || create_node(tag, key);
if (next === curr_node) {
return;
}
// We don't want to lose the focus from this node, so instead of
// moving this node, we'll move the other nodes around
if (curr_focus_path.includes(next)) {
move_node_before(curr_parent, next, curr_node);
} else {
curr_parent.insertBefore(next, curr_node);
}
curr_node = next;
}
export function patch (root, render, data) {
let prev_document = curr_document;
let prev_parent = curr_parent;
let prev_node = curr_node;
let prev_focus_path = curr_focus_path;
curr_document = root.ownerDocument;
curr_parent = root;
curr_node = null;
curr_focus_path = get_focused_nodes(root);
try {
render(data);
} finally {
curr_document = prev_document;
curr_parent = prev_parent;
curr_node = prev_node;
curr_focus_path = prev_focus_path;
}
}
export function element_open (tag, props, key) {
align_node(tag, key);
enter_node();
let record = get_node_record(curr_parent);
props ||= {};
for (let key of get_object_keys(record.props, props)) {
let prev_value = record.props[key];
let next_value = props[key];
if (prev_value == next_value) continue;
if (key.startsWith('on:')) {
let event = key.slice(3);
curr_parent.removeEventListener(event, prev_value);
curr_parent.addEventListener(event, next_value);
}
else if (key.includes(':')) {
continue;
}
else if (key in curr_parent) {
curr_parent[key] = next_value;
}
else if (!next_value) {
curr_parent.removeAttribute(key);
}
else {
curr_parent.setAttribute(key, next_value);
}
}
record.props = props;
return curr_parent;
}
export function element_close (tag) {
let prev_node = leave_node();
let prev_record = get_node_record(prev_node);
assert(prev_record.tag === tag, 'expected closing element to match');
return prev_node;
}
export function element_void (tag, attrs, key) {
element_open(tag, attrs, key);
element_close(tag);
}
export function skip () {
curr_node = curr_parent.lastChild;
}
export function text (contents) {
align_node('#text', undefined);
let record = get_node_record(curr_node);
if (record.text !== contents) {
curr_node.data = contents;
record.text = contents;
}
return curr_node;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment