ks-fwork 4.1.1 → 4.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ks-fwork",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "Demo Frontend Framework",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -9,7 +9,7 @@
9
9
  "module": "dist/ks-fwork.js",
10
10
  "exports": "./dist/ks-fwork.js",
11
11
  "files": [
12
- "src"
12
+ "dist"
13
13
  ],
14
14
  "scripts": {
15
15
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -19,7 +19,6 @@
19
19
  "fast-deep-equal": "^3.1.3"
20
20
  },
21
21
  "devDependencies": {
22
- "rollup-plugin-cleanup": "^3.2.1",
23
22
  "vite": "^8.1.3"
24
23
  }
25
24
  }
package/src/app.js DELETED
@@ -1,36 +0,0 @@
1
- import { destroyDOM } from "./destroy-dom.js";
2
- import { mountDOM } from "./mount-dom.js";
3
- import { h } from "./h.js";
4
-
5
- export function createApp(RootComponent, props = {}) {
6
- let parentEl = null;
7
- let rootNode = null;
8
- let isMounted = false;
9
-
10
- function reset() {
11
- parentEl = null;
12
- rootNode = null;
13
- isMounted = false;
14
- }
15
-
16
- return {
17
- // Attach the app to a real DOM element and do the first render.
18
- mount(_parentEl) {
19
- if (isMounted) throw new Error('The application is already mounted');
20
-
21
- parentEl = _parentEl;
22
- rootNode = h(RootComponent, props);
23
- mountDOM(rootNode, parentEl);
24
-
25
- isMounted = true;
26
- },
27
-
28
- // Detach the app: remove its DOM and cancel every subscription.
29
- unmount() {
30
- if (!isMounted) throw new Error('The application is not mounted');
31
-
32
- destroyDOM(rootNode);
33
- reset();
34
- },
35
- }
36
- }
package/src/attributes.js DELETED
@@ -1,62 +0,0 @@
1
- // Set attributes to an element
2
- export function setAttributes(el, attrs) {
3
- const { class: className, style, ...otherAttrs } = attrs;
4
-
5
- if (className) {
6
- setClass(el, className);
7
- }
8
-
9
- if (style) {
10
- Object.entries(style).forEach(([prop, value]) => {
11
- setStyle(el, prop, value);
12
- })
13
- }
14
-
15
- for (const [name, value] of Object.entries(otherAttrs)) {
16
- setAttribute(el, name, value);
17
- }
18
- }
19
-
20
- // Set class or array of classes to an element
21
- function setClass(el, className) {
22
- el.className = '';
23
-
24
- // If classes are set as string
25
- if (typeof className === 'string') {
26
- el.className = className;
27
- }
28
-
29
- // If classes are set as an array
30
- if (Array.isArray(className)) {
31
- el.classList.add(...className);
32
- }
33
- }
34
-
35
- // Set style to an element
36
- export function setStyle(el, name, value) {
37
- el.style[name] = value;
38
- }
39
-
40
- // Remove style from an element
41
- export function removeStyle(el, name) {
42
- el.style[name] = null;
43
- }
44
-
45
- // Set attribute to an element
46
- export function setAttribute(el, name, value) {
47
- // Remove attributes with no values
48
- if (value == null) {
49
- removeAttribute(el, name);
50
- // Set 'data-*' attributes separately
51
- } else if (name.startsWith('data-')) {
52
- el.setAttribute(name, value);
53
- } else {
54
- el[name] = value;
55
- }
56
- }
57
-
58
- // Remove attribute from an element
59
- export function removeAttribute(el, name) {
60
- el[name] = null;
61
- el.removeAttribute(name);
62
- }
package/src/component.js DELETED
@@ -1,159 +0,0 @@
1
- import { mountDOM } from "./mount-dom.js";
2
- import { destroyDOM } from "./destroy-dom.js";
3
- import { patchDOM } from "./patch-dom.js";
4
- import { DOM_TYPES, extractChildren } from "./h.js";
5
- import { hasOwnProperty } from "./utils/objects.js";
6
-
7
- import equal from 'fast-deep-equal';
8
- import { Dispatcher } from "./dispatcher.js";
9
-
10
- const emptyFn = () => {};
11
-
12
- export function defineComponent({ render, state, onMounted = emptyFn, onUnmounted = emptyFn, ...methods }) {
13
- class Component {
14
- #isMounted = false;
15
- #vNode = null;
16
- #hostEl = null;
17
- #eventHandlers = null;
18
- #parentComponent = null;
19
- #dispatcher = new Dispatcher();
20
- #subscriptions = [];
21
-
22
- constructor(props = {}, eventHandlers = {}, parentComponent = null) {
23
- this.props = props;
24
- this.state = state ? state(props) : {};
25
- this.#eventHandlers = eventHandlers;
26
- this.#parentComponent = parentComponent;
27
- }
28
-
29
- onMounted() {
30
- return Promise.resolve(onMounted.call(this));
31
- }
32
-
33
- onUnmounted() {
34
- return Promise.resolve(onUnmounted.call(this));
35
- }
36
-
37
- get elements() {
38
- // If vNode is null, return an empty array
39
- if (this.#vNode == null) {
40
- return [];
41
- }
42
-
43
- // If vNode's top element is fragment, return the elements inside it
44
- if (this.#vNode.type === DOM_TYPES.FRAGMENT) {
45
- return extractChildren(this.#vNode).flatMap((child) => {
46
- if (child.type === DOM_TYPES.COMPONENT) {
47
- return child.component.elements;
48
- }
49
-
50
- return [child.el];
51
- });
52
- }
53
-
54
- // If vNode's top element is a single node, return its element
55
- return [this.#vNode.el];
56
- }
57
-
58
- // Return component's first element
59
- get firstElement() {
60
- return this.elements[0];
61
- }
62
-
63
- // Return component's first element offset inside the parent element. If is not fragment, return offset 0
64
- get offset() {
65
- if(this.#vNode.type === DOM_TYPES.FRAGMENT) {
66
- return Array.from(this.#hostEl.children).indexOf(this.firstElement);
67
- }
68
-
69
- return 0;
70
- }
71
-
72
- updateProps(props) {
73
- const newProps = { ...this.props, ...props };
74
-
75
- if (equal(this.props, newProps)) {
76
- return;
77
- }
78
-
79
- this.props = newProps;
80
- this.#patch();
81
- }
82
-
83
- updateState(state) {
84
- this.state = { ...this.state, ...state };
85
- this.#patch();
86
- }
87
-
88
- render() {
89
- return render.call(this);
90
- }
91
-
92
- emit(eventName, payload) {
93
- this.#dispatcher.dispatch(eventName, payload);
94
- }
95
-
96
- mount(hostEl, index = null) {
97
- if (this.#isMounted) {
98
- throw new Error('Component is already mounted');
99
- }
100
-
101
- this.#vNode = this.render();
102
- mountDOM(this.#vNode, hostEl, index, this);
103
- this.#wireEventHandlers();
104
-
105
- this.#hostEl = hostEl;
106
- this.#isMounted = true;
107
- }
108
-
109
- unmount() {
110
- if (!this.#isMounted) {
111
- throw new Error('Component is not mounted');
112
- }
113
-
114
- destroyDOM(this.#vNode);
115
- this.#subscriptions.forEach((unsubscribe) => unsubscribe());
116
-
117
- this.#vNode = null;
118
- this.#hostEl = null;
119
- this.#isMounted = false;
120
- this.#subscriptions = [];
121
- }
122
-
123
- #patch() {
124
- if (!this.#isMounted) {
125
- throw new Error('Component is not mounted');
126
- }
127
-
128
- const vNode = this.render();
129
- this.#vNode = patchDOM(this.#vNode, vNode, this.#hostEl, this);
130
- }
131
-
132
- #wireEventHandlers() {
133
- this.#subscriptions = Object.entries(this.#eventHandlers).map(
134
- ([eventName, handler]) =>
135
- this.#wireEventHandler(eventName, handler)
136
- )
137
- }
138
-
139
- #wireEventHandler(eventName, handler) {
140
- return this.#dispatcher.subscribe(eventName, (payload) => {
141
- if (this.#parentComponent) {
142
- handler.call(this.#parentComponent, payload)
143
- } else {
144
- handler(payload)
145
- }
146
- })
147
- }
148
- }
149
-
150
- for (const methodName in methods) {
151
- if (hasOwnProperty(Component, methodName)) {
152
- throw new Error(`Method "${methodName}()" already exists in the component.`);
153
- }
154
-
155
- Component.prototype[methodName] = methods[methodName];
156
- }
157
-
158
- return Component;
159
- }
@@ -1,64 +0,0 @@
1
- import { removeEventListeners } from './events.js'
2
- import { DOM_TYPES } from "./h.js";
3
- import { enqueueJob } from "./scheduler.js";
4
-
5
- // Recursively destroy a virtual node (and its subtree).
6
- export function destroyDOM(vNode) {
7
- const { type } = vNode;
8
-
9
- switch (type) {
10
- case DOM_TYPES.TEXT: {
11
- removeTextNode(vNode);
12
- break;
13
- }
14
-
15
- case DOM_TYPES.ELEMENT: {
16
- removeElementNode(vNode);
17
- break;
18
- }
19
-
20
- case DOM_TYPES.FRAGMENT: {
21
- removeFragmentNode(vNode);
22
- break;
23
- }
24
-
25
- case DOM_TYPES.COMPONENT: {
26
- vNode.component.unmount();
27
- enqueueJob(() => vNode.component.onUnmounted());
28
- break;
29
- }
30
-
31
- default: {
32
- throw new Error(`Can't destroy virtual node of type: ${vNode.type}`);
33
- }
34
- }
35
-
36
- // Drop the reference to the DOM element
37
- delete vNode.el;
38
- }
39
-
40
- // Detach the text node from the DOM.
41
- function removeTextNode(vNode) {
42
- const { el } = vNode;
43
- el.remove();
44
- }
45
-
46
- // Recursively destroy the element's children, remove its event listeners, then detach the element.
47
- function removeElementNode(vNode) {
48
- const { el, children, listeners } = vNode;
49
-
50
- children.forEach(destroyDOM)
51
-
52
- if (listeners) {
53
- removeEventListeners(listeners, el);
54
- delete vNode.listeners;
55
- }
56
-
57
- el.remove();
58
- }
59
-
60
- // Fragment has no DOM element. Destroy fragment node's children recursively.
61
- function removeFragmentNode(vNode) {
62
- const { children } = vNode;
63
- children.forEach(destroyDOM);
64
- }
package/src/dispatcher.js DELETED
@@ -1,56 +0,0 @@
1
- export class Dispatcher {
2
- // All registered commands mapped to their handlers (callback functions)
3
- #handlersByCommand = new Map();
4
-
5
- // Handlers that run after every dispatch, regardless of command
6
- #afterHandlers = [];
7
-
8
- // Subscribe to a command
9
- subscribe(commandName, handler) {
10
- // Create an empty handler array for this command if none exists yet
11
- if (!this.#handlersByCommand.has(commandName)) {
12
- this.#handlersByCommand.set(commandName, []);
13
- }
14
-
15
- // Get the handler array for this command
16
- const handlers = this.#handlersByCommand.get(commandName);
17
-
18
- // If this exact handler reference is already registered, skip it and return a no-op unsubscribe
19
- if (handlers.includes(handler)) {
20
- return () => {}
21
- }
22
-
23
- // Register the handler
24
- handlers.push(handler);
25
-
26
- // Return unsubscribe function
27
- return () => {
28
- const index = handlers.indexOf(handler);
29
- handlers.splice(index, 1);
30
- }
31
- }
32
-
33
- // Register a handler that will be called after every dispatch
34
- afterEveryCommand(handler) {
35
- this.#afterHandlers.push(handler);
36
-
37
- // Return unsubscribe function
38
- return () => {
39
- const index = this.#afterHandlers.indexOf(handler);
40
- this.#afterHandlers.splice(index, 1);
41
- }
42
- }
43
-
44
- // Dispatch a command: run its handlers with the payload
45
- dispatch(commandName, payload) {
46
- if (this.#handlersByCommand.has(commandName)) {
47
- // Call every handler subscribed to this command
48
- this.#handlersByCommand.get(commandName).forEach((handler) => handler(payload))
49
- } else {
50
- console.warn(`No handlers for command: ${commandName}`);
51
- }
52
-
53
- // Call all 'afterEveryCommand' handlers (no payload)
54
- this.#afterHandlers.forEach((handler) => handler());
55
- }
56
- }
package/src/events.js DELETED
@@ -1,28 +0,0 @@
1
- // Add an object of event listeners to an element
2
- export function addEventListeners(listeners = {}, el, hostComponent = null) {
3
- const addedListeners = {};
4
-
5
- Object.entries(listeners).forEach(([eventName, handler]) => {
6
- addedListeners[eventName] = addEventListener(eventName, handler, el, hostComponent);
7
- });
8
-
9
- return addedListeners;
10
- }
11
-
12
- // Add an event listener to an element
13
- export function addEventListener(eventName, handler, el, hostComponent = null) {
14
- function boundHandler() {
15
- hostComponent ? handler.apply(hostComponent, arguments) : handler(...arguments);
16
- }
17
-
18
- el.addEventListener(eventName, boundHandler);
19
-
20
- return boundHandler;
21
- }
22
-
23
- // Remove an object of event listeners from an element
24
- export function removeEventListeners(listeners = {}, el) {
25
- Object.entries(listeners).forEach(([eventName, handler]) => {
26
- el.removeEventListener(eventName, handler);
27
- })
28
- }
package/src/h.js DELETED
@@ -1,74 +0,0 @@
1
- import { withoutNulls } from './utils/arrays.js';
2
-
3
- export const DOM_TYPES = {
4
- TEXT: 'text',
5
- ELEMENT: 'element',
6
- FRAGMENT: 'fragment',
7
- COMPONENT: 'component'
8
- }
9
-
10
- // Create a virtual DOM element
11
- export function h(tag, props = {}, children = []) {
12
- const type = typeof tag === 'string' ? DOM_TYPES.ELEMENT : DOM_TYPES.COMPONENT;
13
-
14
- return {
15
- tag,
16
- props,
17
- type,
18
- children: mapTextNodes(withoutNulls(children)),
19
- }
20
- }
21
-
22
- // Create a virtual text node
23
- export function hString(str) {
24
- return {
25
- type: DOM_TYPES.TEXT,
26
- value: str
27
- };
28
- }
29
-
30
- // Create a fragment node
31
- export function hFragment(vNodes) {
32
- return {
33
- children: mapTextNodes(withoutNulls(vNodes)),
34
- type: DOM_TYPES.FRAGMENT,
35
- }
36
- }
37
-
38
- // Transform strings into text virtual nodes
39
- function mapTextNodes(children) {
40
- return children.map((child) =>
41
- typeof child === 'string' ? hString(child) : child
42
- )
43
- }
44
-
45
- // Create an array with specified amount of empty elements and fill them with 'p' virtual node that has text as children.
46
- export function lipsum(numberOfParagraphs) {
47
- const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
48
- sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
49
- enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
50
- ut aliquip ex ea commodo consequat.`;
51
-
52
- return hFragment(
53
- Array(numberOfParagraphs).fill(
54
- h('p', {}, [text])
55
- )
56
- );
57
- }
58
-
59
- // Recursively extract a node's children into a flat list, dissolving any nested fragments into their child nodes
60
- export function extractChildren(vNode) {
61
- if (vNode.children == null) return [];
62
-
63
- const children = [];
64
-
65
- for (const child of vNode.children) {
66
- if (child.type === DOM_TYPES.FRAGMENT) {
67
- children.push(...extractChildren(child))
68
- } else {
69
- children.push(child);
70
- }
71
- }
72
-
73
- return children;
74
- }
package/src/index.js DELETED
@@ -1,4 +0,0 @@
1
- export { createApp } from './app.js'
2
- export { defineComponent } from './component.js'
3
- export { h, hFragment, hString } from './h.js'
4
- export { nextTick } from './scheduler.js'
package/src/mount-dom.js DELETED
@@ -1,108 +0,0 @@
1
- import { DOM_TYPES } from "./h.js";
2
- import { setAttributes } from './attributes.js'
3
- import { addEventListeners } from "./events.js";
4
- import { enqueueJob } from "./scheduler.js";
5
- import { extractPropsAndEvents } from "./utils/props.js";
6
-
7
- // Recursively mount a virtual node (and its subtree) into a real parent DOM element.
8
- export function mountDOM(vNode, parentEl, index, hostComponent = null) {
9
- switch (vNode.type) {
10
- case DOM_TYPES.TEXT: {
11
- createTextNode(vNode, parentEl, index);
12
- break;
13
- }
14
-
15
- case DOM_TYPES.ELEMENT: {
16
- createElementNode(vNode, parentEl, index, hostComponent);
17
- break;
18
- }
19
-
20
- case DOM_TYPES.FRAGMENT: {
21
- createFragmentNode(vNode, parentEl, index, hostComponent);
22
- break;
23
- }
24
-
25
- case DOM_TYPES.COMPONENT: {
26
- createComponentNode(vNode, parentEl, index, hostComponent);
27
- enqueueJob(() => vNode.component.onMounted());
28
- break;
29
- }
30
-
31
- default: {
32
- throw new Error(`Can't mount virtual node of type: ${vNode.type}`);
33
- }
34
- }
35
- }
36
-
37
- // Create a real text node, save a reference to it on the vNode, and append it to the parent.
38
- function createTextNode(vNode, parentEl, index) {
39
- const { value } = vNode;
40
-
41
- const textNode = document.createTextNode(value);
42
- vNode.el = textNode;
43
-
44
- insert(textNode, parentEl, index);
45
- }
46
-
47
- // Create a real element, apply its props, recursively mount its children into it, then append the finished element to the parent.
48
- function createElementNode(vNode, parentEl, index, hostComponent) {
49
- const { tag, children } = vNode;
50
-
51
- const el = document.createElement(tag);
52
- addProps(el, vNode, hostComponent);
53
- vNode.el = el;
54
-
55
- children.forEach((child) => mountDOM(child, el, null, hostComponent));
56
- insert(el, parentEl, index);
57
- }
58
-
59
- // Fragment has no DOM element. Mount its children directly into the parent.
60
- function createFragmentNode(vNode, parentEl, index, hostComponent) {
61
- const { children } = vNode;
62
- vNode.el = parentEl;
63
-
64
- children.forEach((child, i) => {
65
- mountDOM(child, parentEl, index ? index + i : null, hostComponent)
66
- });
67
- }
68
-
69
- function createComponentNode(vNode, parentEl, index, hostComponent) {
70
- const Component = vNode.tag;
71
- const { props, events } = extractPropsAndEvents(vNode);
72
- const component = new Component(props, events, hostComponent);
73
-
74
- component.mount(parentEl, index);
75
- vNode.component = component;
76
- vNode.el = component.firstElement;
77
- }
78
-
79
- // Split props into event listeners ('on') and attributes and add them to DOM element.
80
- function addProps(el, vNode, hostComponent) {
81
- const { props: attrs, events} = extractPropsAndEvents(vNode);
82
-
83
- vNode.listeners = addEventListeners(events, el, hostComponent);
84
- setAttributes(el, attrs);
85
- }
86
-
87
- // Insert element into parent element at the specified index (or append if no index given)
88
- function insert(el, parentEl, index) {
89
- // Append if no index specified (null or undefined)
90
- if (index == null) {
91
- parentEl.append(el);
92
- return;
93
- }
94
-
95
- if (index < 0) {
96
- throw new Error(`Index must be a positive number, got ${index}`);
97
- }
98
-
99
- const children = parentEl.childNodes;
100
-
101
- if (index >= children.length) {
102
- // Append if index is greater than or equal to the parent element's length of children
103
- parentEl.append(el);
104
- } else {
105
- // Insert before the element at the specified index
106
- parentEl.insertBefore(el, children[index]);
107
- }
108
- }
@@ -1,23 +0,0 @@
1
- import { DOM_TYPES } from "./h.js";
2
-
3
- export function areNodesEqual(nodeOne, nodeTwo) {
4
- // Nodes are different type
5
- if (nodeOne.type !== nodeTwo.type) return false;
6
-
7
- if (nodeOne.type === DOM_TYPES.ELEMENT) {
8
- const { tag: tagOne, props: { key: keyOne } } = nodeOne;
9
- const { tag: tagTwo, props: { key: keyTwo } } = nodeTwo;
10
-
11
- return tagOne === tagTwo && keyOne === keyTwo;
12
- }
13
-
14
- if (nodeOne.type === DOM_TYPES.COMPONENT) {
15
- const { tag: componentOne, props: { key: keyOne } } = nodeOne;
16
- const { tag: componentTwo, props: { key: keyTwo } } = nodeTwo;
17
-
18
- return componentOne === componentTwo && keyOne === keyTwo;
19
- }
20
-
21
- // Text and fragment nodes are always equal
22
- return true;
23
- }
package/src/patch-dom.js DELETED
@@ -1,212 +0,0 @@
1
- import { areNodesEqual } from "./nodes-equal.js";
2
- import { destroyDOM } from "./destroy-dom.js";
3
- import { mountDOM } from "./mount-dom.js";
4
- import { DOM_TYPES, extractChildren } from "./h.js";
5
- import { objectsDiff } from "./utils/objects.js";
6
- import { removeAttribute, removeStyle, setAttribute, setStyle } from "./attributes.js";
7
- import { ARRAY_DIFF_OP, arraysDiff, arraysDiffSequence } from "./utils/arrays.js";
8
- import { isNotBlankOrEmptyString } from "./utils/strings.js";
9
- import { addEventListener } from "./events.js";
10
- import { extractPropsAndEvents } from "./utils/props.js";
11
-
12
- export function patchDOM(oldVNode, newVNode, parentEl, hostComponent = null) {
13
- // Nodes are too different to reuse/patch.
14
- if (!areNodesEqual(oldVNode, newVNode)) {
15
- // Remember where the old node was located inside the parent so the replacement lands in the same spot.
16
- const index = findIndexInParent(parentEl, oldVNode.el);
17
-
18
- // Destroy the old node and its whole subtree.
19
- destroyDOM(oldVNode);
20
-
21
- // Mount the new node and its whole subtree at the index of the old node.
22
- mountDOM(newVNode, parentEl, index, hostComponent);
23
-
24
- return newVNode;
25
- }
26
-
27
- // Copy over the DOM element reference
28
- newVNode.el = oldVNode.el;
29
-
30
- switch (newVNode.type) {
31
- case DOM_TYPES.TEXT: {
32
- patchText(oldVNode, newVNode);
33
- return newVNode;
34
- }
35
-
36
- case DOM_TYPES.ELEMENT: {
37
- patchElement(oldVNode, newVNode, hostComponent);
38
- break;
39
- }
40
-
41
- case DOM_TYPES.COMPONENT: {
42
- patchComponent(oldVNode, newVNode);
43
- break;
44
- }
45
- }
46
-
47
- patchChildren(oldVNode, newVNode, hostComponent);
48
-
49
- return newVNode;
50
- }
51
-
52
- // Get index of the element inside the parent element
53
- function findIndexInParent(parentEl, el) {
54
- const index = Array.from(parentEl.childNodes).indexOf(el);
55
- if (index < 0) return null;
56
-
57
- return index;
58
- }
59
-
60
- function patchText(oldVNode, newVNode) {
61
- const el = oldVNode.el;
62
- const { value: oldText } = oldVNode;
63
- const { value: newText } = newVNode;
64
-
65
- if (oldText !== newText) {
66
- el.nodeValue = newText;
67
- }
68
- }
69
-
70
- function patchElement(oldVNode, newVNode, hostComponent) {
71
- const el = oldVNode.el;
72
-
73
- const {
74
- class: oldClass,
75
- style: oldStyle,
76
- on: oldEvents,
77
- ...oldAttrs
78
- } = oldVNode.props;
79
-
80
- const {
81
- class: newClass,
82
- style: newStyle,
83
- on: newEvents,
84
- ...newAttrs
85
- } = newVNode.props;
86
-
87
- const { listeners: oldListeners } = oldVNode;
88
-
89
- patchAttrs(el, oldAttrs, newAttrs);
90
- patchClasses(el, oldClass, newClass);
91
- patchStyles(el, oldStyle, newStyle);
92
- newVNode.listeners = patchEvents(el, oldListeners, oldEvents, newEvents, hostComponent);
93
- }
94
-
95
- function patchComponent(oldVNode, newVNode) {
96
- const { component } = oldVNode;
97
- const { props } = extractPropsAndEvents(newVNode);
98
-
99
- component.updateProps(props);
100
-
101
- newVNode.component = component;
102
- newVNode.el = component.firstElement;
103
- }
104
-
105
- function patchAttrs(el, oldAttrs, newAttrs) {
106
- const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
107
-
108
- for (const attr of removed) {
109
- removeAttribute(el, attr);
110
- }
111
-
112
- // Concatenate added and updated attributes and set them to element.
113
- for (const attr of added.concat(updated)) {
114
- setAttribute(el, attr, newAttrs[attr]);
115
- }
116
- }
117
-
118
- function patchClasses(el, oldClass, newClass) {
119
- const oldClasses = toClassList(oldClass);
120
- const newClasses = toClassList(newClass);
121
-
122
- const { added, removed } = arraysDiff(oldClasses, newClasses);
123
-
124
- if (removed.length > 0) {
125
- el.classList.remove(...removed);
126
- }
127
-
128
- if (added.length > 0) {
129
- el.classList.add(...added);
130
- }
131
- }
132
-
133
- function toClassList(classes = '') {
134
- return Array.isArray(classes)
135
- ? classes.filter(isNotBlankOrEmptyString)
136
- : classes.split(/(\s+)/).filter(isNotBlankOrEmptyString);
137
- }
138
-
139
- function patchStyles(el, oldStyle = {}, newStyle = {}) {
140
- const { added, removed, updated} = objectsDiff(oldStyle, newStyle);
141
-
142
- for (const style of removed) {
143
- removeStyle(el, style);
144
- }
145
-
146
- for (const style of added.concat(updated)) {
147
- setStyle(el, style, newStyle[style]);
148
- }
149
- }
150
-
151
- function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}, hostComponent) {
152
- const { removed, added, updated} = objectsDiff(oldEvents, newEvents);
153
-
154
- for (const eventName of removed.concat(updated)) {
155
- el.removeEventListener(eventName, oldListeners[eventName]);
156
- }
157
-
158
- const addedListeners = {};
159
-
160
- for (const eventName of added.concat(updated)) {
161
- addedListeners[eventName] = addEventListener(eventName, newEvents[eventName], el, hostComponent);
162
- }
163
-
164
- return addedListeners;
165
- }
166
-
167
- // Reconcile a node's child lists.
168
- function patchChildren(oldVNode, newVNode, hostComponent) {
169
- const oldChildren = extractChildren(oldVNode);
170
- const newChildren = extractChildren(newVNode);
171
- const parentEl = oldVNode.el;
172
-
173
- // Create the sequence of operations to match the note's child lists.
174
- const diffSeq = arraysDiffSequence(oldChildren, newChildren, areNodesEqual);
175
-
176
- // Loop over the operations and perform the operations on the DOM.
177
- for (const operation of diffSeq) {
178
- const { originalIndex, from, index, item } = operation;
179
- // Get the host component’s offset, if there is one; offset is 0 otherwise
180
- const offset = hostComponent?.offset ?? 0;
181
-
182
- switch (operation.op) {
183
- // New node with no old counterpart: build its subtree and insert it at `index`
184
- case ARRAY_DIFF_OP.ADD: {
185
- mountDOM(item, parentEl, index, hostComponent)
186
- break;
187
- }
188
-
189
- // Old node gone from the new list: destroy it and its subtree
190
- case ARRAY_DIFF_OP.REMOVE: {
191
- destroyDOM(item);
192
- break;
193
- }
194
-
195
- // Node still present but at a different position: move the existing node to its new spot, then patch it in place for any inner changes
196
- case ARRAY_DIFF_OP.MOVE: {
197
- const el = oldChildren[from].el;
198
- const elAtTargetIndex = parentEl.childNodes[index + offset];
199
- parentEl.insertBefore(el, elAtTargetIndex);
200
-
201
- patchDOM(oldChildren[from], newChildren[index], parentEl, hostComponent);
202
- break;
203
- }
204
-
205
- // Same node, same position, but its contents may still differ, so recurse
206
- case ARRAY_DIFF_OP.NOOP: {
207
- patchDOM(oldChildren[originalIndex], newChildren[index], parentEl, hostComponent);
208
- break;
209
- }
210
- }
211
- }
212
- }
package/src/scheduler.js DELETED
@@ -1,37 +0,0 @@
1
- let isScheduled = false;
2
- const jobs = [];
3
-
4
- export function enqueueJob(job) {
5
- jobs.push(job);
6
- scheduleUpdate();
7
- }
8
-
9
- function scheduleUpdate() {
10
- if (isScheduled) return;
11
-
12
- isScheduled = true;
13
- queueMicrotask(processJobs);
14
- }
15
-
16
- function processJobs() {
17
- while (jobs.length > 0) {
18
- const job = jobs.shift();
19
- try {
20
- Promise.resolve(job()).catch((error) => {
21
- console.error(`[scheduler]: ${error}`);
22
- });
23
- } catch (error) {
24
- console.error(`[scheduler]: ${error}`);
25
- }
26
- }
27
- isScheduled = false;
28
- }
29
-
30
- export function nextTick() {
31
- scheduleUpdate();
32
- return flushPromises();
33
- }
34
-
35
- function flushPromises() {
36
- return new Promise((resolve) => setTimeout(resolve));
37
- }
@@ -1,203 +0,0 @@
1
- // Filter out null values from the array.
2
- export function withoutNulls(arr) {
3
- return arr.filter((item) => item != null);
4
- }
5
-
6
- export function arraysDiff(oldArray, newArray) {
7
- return {
8
- added: newArray.filter(
9
- (newItem) => !oldArray.includes(newItem)
10
- ),
11
- removed: oldArray.filter(
12
- (oldItem) => !newArray.includes(oldItem)
13
- ),
14
- }
15
- }
16
-
17
- export const ARRAY_DIFF_OP = {
18
- ADD: 'add',
19
- REMOVE: 'remove',
20
- MOVE: 'move',
21
- NOOP: 'noop'
22
- }
23
-
24
- class ArrayWithOriginalIndices {
25
- #oldArray = [];
26
- #originalIndices = [];
27
- #equalsFn;
28
-
29
- constructor(array, equalsFn) {
30
- this.#oldArray = [...array];
31
- this.#originalIndices = array.map((value, index) => index);
32
- this.#equalsFn = equalsFn;
33
- }
34
-
35
- get length() {
36
- return this.#oldArray.length;
37
- }
38
-
39
- originalIndexAt(index) {
40
- return this.#originalIndices[index];
41
- }
42
-
43
- // ADD
44
- isAddition(item, startIndex) {
45
- return this.findIndexFrom(item, startIndex) === -1;
46
- }
47
-
48
- addItem(item, index) {
49
- const operation = {
50
- op: ARRAY_DIFF_OP.ADD,
51
- index,
52
- item
53
- }
54
-
55
- this.#oldArray.splice(index, 0, item);
56
- this.#originalIndices.splice(index, 0, -1);
57
-
58
- return operation;
59
- }
60
-
61
- // REMOVE
62
- isRemoval(index, newArray) {
63
- // Bail out if index is past the end of the old array.
64
- if (index >= this.length) { return false; }
65
-
66
- const item = this.#oldArray[index];
67
- const indexInNewArray = newArray.findIndex((newArrayItem) => this.#equalsFn(item, newArrayItem))
68
-
69
- return indexInNewArray === -1;
70
- }
71
-
72
- removeItem(index) {
73
- const operation = {
74
- op: ARRAY_DIFF_OP.REMOVE,
75
- index,
76
- item: this.#oldArray[index],
77
- }
78
-
79
- this.#oldArray.splice(index, 1);
80
- this.#originalIndices.splice(index, 1);
81
-
82
- return operation;
83
- }
84
-
85
- // MOVE
86
- moveItem(item, toIndex) {
87
- const fromIndex = this.findIndexFrom(item, toIndex);
88
-
89
- const operation = {
90
- op: ARRAY_DIFF_OP.MOVE,
91
- originalIndex: this.originalIndexAt(fromIndex),
92
- from: fromIndex,
93
- index: toIndex,
94
- item: this.#oldArray[fromIndex],
95
- }
96
-
97
- const [itemToMove] = this.#oldArray.splice(fromIndex, 1);
98
- this.#oldArray.splice(toIndex, 0, itemToMove);
99
-
100
- const [originalIndex] = this.#originalIndices.splice(fromIndex, 1);
101
- this.#originalIndices.splice(toIndex, 0, originalIndex);
102
-
103
- return operation;
104
- }
105
-
106
- // NOOP
107
- isNoop(index, newArray) {
108
- // Bail out if index is past the end of the old array.
109
- if (index >= this.length) {return false; }
110
-
111
- const item = this.#oldArray[index];
112
- const newArrayItem = newArray[index];
113
-
114
- return this.#equalsFn(item, newArrayItem);
115
- }
116
-
117
- noopItem(index) {
118
- return {
119
- op: ARRAY_DIFF_OP.NOOP,
120
- originalIndex: this.originalIndexAt(index),
121
- index,
122
- item: this.#oldArray[index],
123
- }
124
- }
125
-
126
- // Remove all items after specified index
127
- removeItemsAfter(index) {
128
- const operations = [];
129
-
130
- while (this.length > index) {
131
- operations.push(this.removeItem(index))
132
- }
133
-
134
- return operations;
135
- }
136
-
137
- // Return the first index at or after startIndex where the item matches, or -1 if item is not found.
138
- findIndexFrom(item, startIndex) {
139
- for (let i = startIndex; i < this.length; i++) {
140
- if (this.#equalsFn(item, this.#oldArray[i])) {
141
- return i;
142
- }
143
- }
144
-
145
- return -1;
146
- }
147
- }
148
-
149
- export function arraysDiffSequence(oldArray, newArray, equalsFn = (a, b) => a === b) {
150
- const sequence = [];
151
-
152
- // Store old array with original indices preserved.
153
- const array = new ArrayWithOriginalIndices(oldArray, equalsFn);
154
-
155
- for (let index = 0; index < newArray.length; index++) {
156
- if (array.isRemoval(index, newArray)) {
157
- sequence.push(array.removeItem(index));
158
- index--
159
- continue;
160
- }
161
-
162
- if (array.isNoop(index, newArray)) {
163
- sequence.push(array.noopItem(index));
164
- continue;
165
- }
166
-
167
- const item = newArray[index];
168
-
169
- if (array.isAddition(item, index)) {
170
- sequence.push(array.addItem(item, index));
171
- continue;
172
- }
173
-
174
- sequence.push(array.moveItem(item, index));
175
- }
176
-
177
- sequence.push(...array.removeItemsAfter(newArray.length));
178
-
179
- return sequence;
180
- }
181
-
182
- export function applyArraysDiffSequence(oldArray, diffSequence) {
183
- return diffSequence.reduce((array, { op, item, index, from }) => {
184
- switch (op) {
185
- // Insert an item at the specified index.
186
- case 'add':
187
- array.splice(index, 0, item)
188
- break
189
-
190
- // Remove an element at the specified index.
191
- case 'remove':
192
- array.splice(index, 1)
193
- break
194
-
195
- // Remove element at index 'from' and move it to specified index.
196
- case 'move':
197
- array.splice(index, 0, array.splice(from, 1)[0])
198
- break
199
- }
200
-
201
- return array
202
- }, oldArray)
203
- }
@@ -1,16 +0,0 @@
1
- export function objectsDiff(oldObj, newObj) {
2
- const oldKeys = Object.keys(oldObj);
3
- const newKeys = Object.keys(newObj);
4
-
5
- return {
6
- added: newKeys.filter((newKey) => !(newKey in oldObj)),
7
- removed: oldKeys.filter((oldKey) => !(oldKey in newObj)),
8
- updated: newKeys.filter(
9
- (key) => key in oldObj && oldObj[key] !== newObj[key]
10
- ),
11
- }
12
- }
13
-
14
- export function hasOwnProperty(obj, prop) {
15
- return Object.prototype.hasOwnProperty.call(obj, prop);
16
- }
@@ -1,6 +0,0 @@
1
- export function extractPropsAndEvents(vNode) {
2
- const { on: events = {}, ...props } = vNode.props;
3
- delete props.key;
4
-
5
- return { props, events }
6
- }
@@ -1,7 +0,0 @@
1
- export function isNotEmptyString(str) {
2
- return str !== '';
3
- }
4
-
5
- export function isNotBlankOrEmptyString(str) {
6
- return isNotEmptyString(str.trim());
7
- }