Created
March 9, 2019 01:50
-
-
Save mizchi/fabd48a51aed78f81ecef77d44047537 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| // Browser sync program | |
| import { uniqueId } from "lodash"; | |
| import React, { useState } from "react"; | |
| import ReactDOM from "react-dom"; | |
| type Cursor = number[]; | |
| type DeserializedMutationRecord = | |
| | { | |
| type: "childList"; | |
| target: Node; | |
| addedNodes: Node[]; | |
| removedNodes: Node[]; | |
| previousSibling: Node | void; | |
| nextSibling: Node | void; | |
| } | |
| | { | |
| type: "characterData"; | |
| target: Node; | |
| value: string; | |
| } | |
| | { | |
| type: "attributes"; | |
| target: Node; | |
| attributeName: string; | |
| oldValue: string; | |
| value: string; | |
| }; | |
| type SerializedMutationRecord = | |
| | { | |
| type: "childList"; | |
| targetCursor: Cursor; | |
| removedNodesCursors: Cursor[]; | |
| addedNodesData: { | |
| tag: string; | |
| attributes: object; | |
| html: string; | |
| }[]; | |
| previousSiblingCursor: Cursor | void; | |
| nextSiblingCursor: Cursor | void; | |
| } | |
| | { | |
| type: "characterData"; | |
| targetCursor: Cursor; | |
| value: string; | |
| } | |
| | { | |
| type: "attributes"; | |
| targetCursor: Cursor; | |
| attributeName: string; | |
| oldValue: string; | |
| value: string; | |
| }; | |
| function getNodeByCursor(cursor: Cursor, root: Node): Node { | |
| let cur: Node = root; | |
| for (const idx of cursor) { | |
| cur = cur.childNodes[idx]; | |
| } | |
| return cur; | |
| } | |
| function getCursorFromRoot(target: Node, root: Node): Cursor { | |
| if (target === root) { | |
| return []; | |
| } | |
| // if (!isChildOfRoot(target, root)) { | |
| // throw new Error('Is not child of root') | |
| // } | |
| if (target == null) { | |
| throw new Error("not valid root"); | |
| } | |
| if (target.parentElement === root) { | |
| return [0]; | |
| } | |
| const indexFromParent = Array.from(target.parentElement.childNodes).indexOf( | |
| target as ChildNode | |
| ); | |
| return [...getCursorFromRoot(target.parentElement, root), indexFromParent]; | |
| } | |
| function deserializeMutationRecord( | |
| record: SerializedMutationRecord, | |
| host: HTMLElement | |
| ): DeserializedMutationRecord { | |
| switch (record.type) { | |
| case "characterData": { | |
| return { | |
| type: record.type, | |
| target: getNodeByCursor(record.targetCursor, host), | |
| value: record.value | |
| }; | |
| } | |
| case "attributes": { | |
| return { | |
| type: record.type, | |
| target: getNodeByCursor(record.targetCursor, host) as any, | |
| attributeName: record.attributeName, | |
| oldValue: record.oldValue, | |
| value: record.value | |
| }; | |
| } | |
| case "childList": { | |
| return { | |
| type: record.type, | |
| addedNodes: record.addedNodesData.map(data => { | |
| const el = document.createElement(data.tag); | |
| Object.entries(data.attributes).forEach(([k, v]) => | |
| el.setAttribute(k, v) | |
| ); | |
| el.innerHTML = data.html; | |
| return el; | |
| }), | |
| removedNodes: record.removedNodesCursors.map(n => | |
| getNodeByCursor(n, host) | |
| ), | |
| target: getNodeByCursor(record.targetCursor, host), | |
| previousSibling: | |
| record.previousSiblingCursor && | |
| getNodeByCursor(record.previousSiblingCursor, host), | |
| nextSibling: | |
| record.nextSiblingCursor && | |
| getNodeByCursor(record.nextSiblingCursor, host) | |
| }; | |
| } | |
| } | |
| } | |
| function serializeMutationRecord( | |
| record: MutationRecord, | |
| host: HTMLElement, | |
| cursorMap: Map<Node, Cursor> | |
| ): SerializedMutationRecord { | |
| switch (record.type) { | |
| case "characterData": { | |
| const text = record.target as Text; | |
| // console.log("char", record.target.data); | |
| return { | |
| type: record.type, | |
| targetCursor: getCursorFromRoot(record.target, host), | |
| value: text.data | |
| }; | |
| } | |
| case "attributes": { | |
| return { | |
| type: record.type, | |
| targetCursor: getCursorFromRoot(record.target, host), | |
| attributeName: record.attributeName, | |
| oldValue: record.oldValue, | |
| value: host.getAttribute(record.attributeName) | |
| }; | |
| } | |
| case "childList": { | |
| return { | |
| type: record.type, | |
| targetCursor: getCursorFromRoot(record.target, host), | |
| removedNodesCursors: | |
| record.removedNodes && | |
| Array.from(record.removedNodes).map(removedNode => | |
| cursorMap.get(removedNode) | |
| ), | |
| addedNodesData: | |
| record.addedNodes && | |
| Array.from(record.addedNodes).map((addedNode: Element) => ({ | |
| tag: addedNode.tagName.toLowerCase(), | |
| attributes: Object.entries(addedNode.attributes).reduce( | |
| (acc, [k, v]) => ({ ...acc, [k]: v }), | |
| {} | |
| ), | |
| html: addedNode.innerHTML | |
| })), | |
| previousSiblingCursor: cursorMap.get(record.previousSibling), | |
| nextSiblingCursor: cursorMap.get(record.nextSibling) | |
| }; | |
| } | |
| } | |
| } | |
| function isChildOfRoot(node: Node, root: Node) { | |
| if (node === root) { | |
| return true; | |
| } | |
| let cur = node; | |
| while ((cur = cur.parentElement)) { | |
| if (cur === document.body) { | |
| return false; | |
| } | |
| if (cur === root) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function handleMutation(mut: DeserializedMutationRecord, root: HTMLElement) { | |
| if (isChildOfRoot(mut.target, root)) { | |
| switch (mut.type) { | |
| case "attributes": { | |
| const targetOfOutput = mut.target; | |
| const newValue = (mut.target as Element).getAttribute( | |
| mut.attributeName | |
| ); | |
| (targetOfOutput as Element).setAttribute(mut.attributeName, newValue); | |
| break; | |
| } | |
| case "characterData": { | |
| // const mapped = this.getMappedNode(mut.target); | |
| // const mapped = mut.target; | |
| if (mut.target) { | |
| mut.target.nodeValue = mut.value; | |
| } | |
| break; | |
| } | |
| case "childList": { | |
| // added | |
| if (mut.addedNodes.length > 0) { | |
| for (const addedNode of Array.from(mut.addedNodes)) { | |
| const newNode = addedNode.cloneNode(true); | |
| if (mut.nextSibling) { | |
| mut.target.insertBefore(newNode, mut.nextSibling); | |
| } else if (mut.previousSibling) { | |
| if (mut.previousSibling) { | |
| mut.target.insertBefore( | |
| newNode, | |
| mut.previousSibling.nextSibling | |
| ); | |
| } | |
| mut.target.appendChild(newNode); | |
| } else { | |
| mut.target.appendChild(newNode); | |
| } | |
| } | |
| } | |
| // removed | |
| if (mut.removedNodes.length > 0) { | |
| for (const removedNode of Array.from(mut.removedNodes)) { | |
| const removedOnOut = removedNode; | |
| const targetOnOutput = mut.target; | |
| targetOnOutput.removeChild(removedOnOut); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| class SyncSource { | |
| private lastCursorMap: Map<Node, Cursor> = new Map(); | |
| constructor(private root: HTMLElement) {} | |
| startSync(onUpdate: (serialized: SerializedMutationRecord[]) => void) { | |
| new MutationObserver((mutations: MutationRecord[]) => { | |
| const serialized = Array.from(mutations) | |
| .filter(mut => { | |
| return isChildOfRoot(mut.target, this.root); | |
| }) | |
| .map(mut => | |
| serializeMutationRecord(mut, this.root, this.lastCursorMap) | |
| ); | |
| this.rebuildCursorMap(); | |
| onUpdate(JSON.parse(JSON.stringify(serialized))); | |
| }).observe(document.body, { | |
| attributes: true, | |
| characterData: true, | |
| childList: true, | |
| subtree: true, | |
| attributeOldValue: true, | |
| characterDataOldValue: true | |
| }); | |
| } | |
| private rebuildCursorMap() { | |
| this.lastCursorMap.clear(); | |
| const walk = (node: Node) => { | |
| const cursor = getCursorFromRoot(node, this.root); | |
| this.lastCursorMap.set(node, cursor); | |
| Array.from(node.childNodes).forEach(child => walk(child)); | |
| }; | |
| walk(this.root); | |
| } | |
| } | |
| function sync(serialized: SerializedMutationRecord[], root: HTMLElement) { | |
| // run | |
| const deserialized: DeserializedMutationRecord[] = serialized.map( | |
| (mut: SerializedMutationRecord) => deserializeMutationRecord(mut, root) | |
| ); | |
| for (const mut of Array.from(deserialized)) { | |
| handleMutation(mut, root); | |
| } | |
| } | |
| // React Component | |
| function NodeList(props: { value: number; onClickSelf: () => void }) { | |
| const [children, setChildren] = useState([]); | |
| const [counter, setCounter] = useState(0); | |
| return ( | |
| <div> | |
| <button onClick={() => setCounter(s => s + 1)}>{counter}</button> | |
| <button | |
| onClick={() => { | |
| setChildren([...children, uniqueId()]); | |
| }} | |
| > | |
| add | |
| </button> | |
| <button | |
| onClick={() => { | |
| setChildren([]); | |
| }} | |
| > | |
| clear | |
| </button> | |
| <button onClick={props.onClickSelf}>remove self</button> | |
| <span>{props.value}</span> | |
| <ul> | |
| {children.map((i, index) => { | |
| return ( | |
| <NodeList | |
| key={i} | |
| value={i} | |
| onClickSelf={() => { | |
| const newChildren = children.filter( | |
| (_, cidx) => cidx !== index | |
| ); | |
| setChildren(newChildren); | |
| }} | |
| /> | |
| ); | |
| })} | |
| </ul> | |
| </div> | |
| ); | |
| } | |
| function App() { | |
| return <NodeList value={0} onClickSelf={() => {}} />; | |
| } | |
| // run | |
| const observedNode = document.querySelector(".region-a") as HTMLElement; | |
| const outputNode = document.querySelector(".region-b") as HTMLElement; | |
| const source = new SyncSource(observedNode); | |
| source.startSync(serialized => { | |
| sync(serialized, outputNode); | |
| }); | |
| ReactDOM.render(<App />, observedNode); |
Author
mizchi
commented
Mar 9, 2019
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
