Created
June 10, 2021 08:00
-
-
Save intrnl/e0503cf600b4db0085ed70e0c35f7176 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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