UNPKG

9.03 kBJavaScriptView Raw
1/**
2 * Field on DOM node that stores the previous VNode
3 */
4const OLD_VNODE_FIELD = '__m_old_vnode';
5
6/**
7 * Creates an element from a VNode
8 * @param {VNode} vnode - VNode to convert to HTMLElement or Text
9 * @param {boolean} attachField - Attach OLD_VNODE_FIELD
10 * @returns {HTMLElement|Text}
11 */
12const createElement = (vnode, attachField = true) => {
13 if (typeof vnode === 'string')
14 return document.createTextNode(vnode);
15 const el = document.createElement(vnode.tag);
16 if (vnode.props) {
17 for (const name of Object.keys(vnode.props)) {
18 el[name] = vnode.props[name];
19 }
20 }
21 if (vnode.children) {
22 for (let i = 0; i < vnode.children.length; ++i) {
23 el.appendChild(createElement(vnode.children[i]));
24 }
25 }
26 if (attachField)
27 el[OLD_VNODE_FIELD] = vnode;
28 return el;
29};
30
31/**
32 * Attaches ns props to svg element
33 * @param {VElement} vnode - SVG VNode
34 * @returns {VElement}
35 */
36const svg = (vnode) => {
37 if (!vnode.props)
38 vnode.props = {};
39 ns(vnode.tag, vnode.props, vnode.children);
40 return vnode;
41};
42const ns = (tag, props, children) => {
43 props.ns = 'http://www.w3.org/2000/svg';
44 if (children && tag !== 'foreignObject') {
45 children.forEach((child) => {
46 if (typeof child === 'string')
47 return;
48 if (child.props)
49 ns(child.tag, child.props, child.children);
50 });
51 }
52};
53/**
54 * Generates a style string based on a styleObject
55 * @param {object} styleObject - Object with styles
56 * @returns
57 */
58const style = (styleObject) => {
59 return Object.entries(styleObject)
60 .map((style) => style.join(':'))
61 .join(';');
62};
63/**
64 * Generates a className string based on a classObject
65 * @param {object} classObject - Object with classes paired with boolean values to toggle
66 * @returns {string}
67 */
68const className = (classObject) => {
69 return Object.keys(classObject)
70 .filter((className) => classObject[className])
71 .join(' ');
72};
73/**
74 * Helper method for creating a VNode
75 * @param {string} tag - The tagName of an HTMLElement
76 * @param {VProps=} props - DOM properties and attributes of an HTMLElement
77 * @param {VNode[]=} children - Children of an HTMLElement
78 * @param {VFlags=} flag - Compiler flag for VNode
79 * @returns {VElement}
80 */
81const m = (tag, props, children, flag) => {
82 let key;
83 if (props?.key) {
84 key = props.key;
85 delete props.key;
86 }
87 return {
88 tag,
89 props,
90 children,
91 key,
92 flag,
93 };
94};
95
96var VFlags;
97(function (VFlags) {
98 VFlags[VFlags["NO_CHILDREN"] = 0] = "NO_CHILDREN";
99 VFlags[VFlags["ONLY_TEXT_CHILDREN"] = 1] = "ONLY_TEXT_CHILDREN";
100 VFlags[VFlags["ANY_CHILDREN"] = 2] = "ANY_CHILDREN";
101})(VFlags || (VFlags = {}));
102var VActions;
103(function (VActions) {
104 VActions[VActions["INSERT_TOP"] = 0] = "INSERT_TOP";
105 VActions[VActions["INSERT_BOTTOM"] = 1] = "INSERT_BOTTOM";
106 VActions[VActions["DELETE_TOP"] = 2] = "DELETE_TOP";
107 VActions[VActions["DELETE_BOTTOM"] = 3] = "DELETE_BOTTOM";
108 VActions[VActions["ANY_ACTION"] = 4] = "ANY_ACTION";
109})(VActions || (VActions = {}));
110
111/**
112 * Diffs two VNode props and modifies the DOM node based on the necessary changes
113 * @param {HTMLElement} el - Target element to be modified
114 * @param {VProps} oldProps - Old VNode props
115 * @param {VProps} newProps - New VNode props
116 */
117/* istanbul ignore next */
118const patchProps = (el, oldProps, newProps) => {
119 const cache = [];
120 for (const oldPropName of Object.keys(oldProps)) {
121 const newPropValue = newProps[oldPropName];
122 if (newPropValue) {
123 el[oldPropName] = newPropValue;
124 cache.push(oldPropName);
125 }
126 else {
127 el.removeAttribute(oldPropName);
128 delete el[oldPropName];
129 }
130 }
131 for (const newPropName of Object.keys(newProps)) {
132 if (!cache.includes(newPropName)) {
133 el[newPropName] = newProps[newPropName];
134 }
135 }
136};
137/**
138 * Diffs two VNode children and modifies the DOM node based on the necessary changes
139 * @param {HTMLElement} el - Target element to be modified
140 * @param {VNode[]} oldVNodeChildren - Old VNode children
141 * @param {VNode[]} newVNodeChildren - New VNode children
142 */
143const patchChildren = (el, oldVNodeChildren, newVNodeChildren) => {
144 const childNodes = [...el.childNodes];
145 /* istanbul ignore next */
146 if (oldVNodeChildren) {
147 for (let i = 0; i < oldVNodeChildren.length; ++i) {
148 patch(childNodes[i], newVNodeChildren[i], oldVNodeChildren[i]);
149 }
150 }
151 /* istanbul ignore next */
152 const slicedNewVNodeChildren = newVNodeChildren.slice(oldVNodeChildren?.length ?? 0);
153 for (let i = 0; i < slicedNewVNodeChildren.length; ++i) {
154 el.appendChild(createElement(slicedNewVNodeChildren[i], false));
155 }
156};
157const replaceElementWithVNode = (el, newVNode) => {
158 if (typeof newVNode === 'string') {
159 el.textContent = newVNode;
160 return el;
161 }
162 else {
163 const newElement = createElement(newVNode);
164 el.replaceWith(newElement);
165 return newElement;
166 }
167};
168/**
169 * Diffs two VNodes and modifies the DOM node based on the necessary changes
170 * @param {HTMLElement|Text} el - Target element to be modified
171 * @param {VNode} newVNode - New VNode
172 * @param {VNode=} prevVNode - Previous VNode
173 * @returns {HTMLElement|Text}
174 */
175const patch = (el, newVNode, prevVNode) => {
176 if (!newVNode) {
177 el.remove();
178 return el;
179 }
180 const oldVNode = prevVNode ?? el[OLD_VNODE_FIELD];
181 const hasString = typeof oldVNode === 'string' || typeof newVNode === 'string';
182 if (hasString && oldVNode !== newVNode)
183 return replaceElementWithVNode(el, newVNode);
184 if (!hasString) {
185 if ((!oldVNode?.key && !newVNode?.key) ||
186 oldVNode?.key !== newVNode?.key) {
187 if (oldVNode?.tag !== newVNode?.tag &&
188 !newVNode.children &&
189 !newVNode.props) {
190 // newVNode has no props/children is replaced because it is generally
191 // faster to create a empty HTMLElement rather than iteratively/recursively
192 // remove props/children
193 return replaceElementWithVNode(el, newVNode);
194 }
195 if (oldVNode && !(el instanceof Text)) {
196 patchProps(el, oldVNode.props || {}, newVNode.props || {});
197 switch (newVNode.flag) {
198 case VFlags.NO_CHILDREN: {
199 el.textContent = '';
200 break;
201 }
202 case VFlags.ONLY_TEXT_CHILDREN: {
203 el.textContent = newVNode.children.join('');
204 break;
205 }
206 default: {
207 const [action, numberOfNodes] = newVNode.action ?? [VActions.ANY_ACTION, 0];
208 switch (action) {
209 case VActions.INSERT_TOP: {
210 for (let i = numberOfNodes - 1; i >= 0; --i) {
211 el.insertBefore(createElement(newVNode.children[i]), el.firstChild);
212 }
213 break;
214 }
215 case VActions.INSERT_BOTTOM: {
216 for (let i = 0; i < numberOfNodes; i++) {
217 el.appendChild(createElement(newVNode.children[i]));
218 }
219 break;
220 }
221 case VActions.DELETE_TOP: {
222 for (let i = numberOfNodes - 1; i >= 0; --i) {
223 el.removeChild(el.firstChild);
224 }
225 break;
226 }
227 case VActions.DELETE_BOTTOM: {
228 for (let i = 0; i < numberOfNodes; i++) {
229 el.removeChild(el.lastChild);
230 }
231 break;
232 }
233 default: {
234 patchChildren(el, oldVNode.children || [], newVNode.children);
235 break;
236 }
237 }
238 break;
239 }
240 }
241 }
242 }
243 }
244 if (!prevVNode)
245 el[OLD_VNODE_FIELD] = newVNode;
246 return el;
247};
248
249export { OLD_VNODE_FIELD, className, createElement, m, patch, patchChildren, patchProps, style, svg };