pulse-js-framework 1.7.4 → 1.7.6

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.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Pulse DOM Conditional Module
3
+ * Conditional rendering primitives (when, match, show)
4
+ *
5
+ * @module dom-conditional
6
+ */
7
+
8
+ import { effect } from './pulse.js';
9
+ import { getAdapter } from './dom-adapter.js';
10
+
11
+ // =============================================================================
12
+ // CONDITIONAL RENDERING
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Conditional rendering - renders content based on condition
17
+ *
18
+ * @param {Function|Pulse} condition - Condition source (reactive)
19
+ * @param {Function|Node} thenTemplate - Template to render when true
20
+ * @param {Function|Node|null} elseTemplate - Template to render when false
21
+ * @returns {DocumentFragment} Container fragment with conditional content
22
+ */
23
+ export function when(condition, thenTemplate, elseTemplate = null) {
24
+ const dom = getAdapter();
25
+ const container = dom.createDocumentFragment();
26
+ const marker = dom.createComment('when');
27
+ dom.appendChild(container, marker);
28
+
29
+ let currentNodes = [];
30
+ let currentCleanup = null;
31
+
32
+ effect(() => {
33
+ const show = typeof condition === 'function' ? condition() : condition.get();
34
+
35
+ // Cleanup previous
36
+ for (const node of currentNodes) {
37
+ dom.removeNode(node);
38
+ }
39
+ if (currentCleanup) currentCleanup();
40
+ currentNodes = [];
41
+ currentCleanup = null;
42
+
43
+ // Render new
44
+ const template = show ? thenTemplate : elseTemplate;
45
+ if (template) {
46
+ const result = typeof template === 'function' ? template() : template;
47
+ if (result) {
48
+ const nodes = Array.isArray(result) ? result : [result];
49
+ const fragment = dom.createDocumentFragment();
50
+ for (const node of nodes) {
51
+ if (dom.isNode(node)) {
52
+ dom.appendChild(fragment, node);
53
+ currentNodes.push(node);
54
+ }
55
+ }
56
+ const markerParent = dom.getParentNode(marker);
57
+ if (markerParent) {
58
+ dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
59
+ }
60
+ }
61
+ }
62
+ });
63
+
64
+ return container;
65
+ }
66
+
67
+ /**
68
+ * Switch/case rendering - renders content based on matching value
69
+ *
70
+ * @param {Function|Pulse} getValue - Value source (reactive)
71
+ * @param {Object} cases - Map of value -> template, with optional 'default' key
72
+ * @returns {Comment} Marker node for position tracking
73
+ */
74
+ export function match(getValue, cases) {
75
+ const dom = getAdapter();
76
+ const marker = dom.createComment('match');
77
+ let currentNodes = [];
78
+
79
+ effect(() => {
80
+ const value = typeof getValue === 'function' ? getValue() : getValue.get();
81
+
82
+ // Remove old nodes
83
+ for (const node of currentNodes) {
84
+ dom.removeNode(node);
85
+ }
86
+ currentNodes = [];
87
+
88
+ // Find matching case
89
+ const template = cases[value] ?? cases.default;
90
+ if (template) {
91
+ const result = typeof template === 'function' ? template() : template;
92
+ if (result) {
93
+ const nodes = Array.isArray(result) ? result : [result];
94
+ const fragment = dom.createDocumentFragment();
95
+ for (const node of nodes) {
96
+ if (dom.isNode(node)) {
97
+ dom.appendChild(fragment, node);
98
+ currentNodes.push(node);
99
+ }
100
+ }
101
+ const markerParent = dom.getParentNode(marker);
102
+ if (markerParent) {
103
+ dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
104
+ }
105
+ }
106
+ }
107
+ });
108
+
109
+ return marker;
110
+ }
111
+
112
+ /**
113
+ * Toggle element visibility without removing from DOM
114
+ * Unlike when(), this keeps the element in the DOM but hides it
115
+ *
116
+ * @param {Function|Pulse} condition - Condition source (reactive)
117
+ * @param {HTMLElement} element - Element to show/hide
118
+ * @returns {HTMLElement} The element for chaining
119
+ */
120
+ export function show(condition, element) {
121
+ const dom = getAdapter();
122
+ effect(() => {
123
+ const shouldShow = typeof condition === 'function' ? condition() : condition.get();
124
+ dom.setStyle(element, 'display', shouldShow ? '' : 'none');
125
+ });
126
+ return element;
127
+ }
128
+
129
+ export default {
130
+ when,
131
+ match,
132
+ show
133
+ };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Pulse DOM Element Module
3
+ * Core element creation and text node utilities
4
+ *
5
+ * @module dom-element
6
+ */
7
+
8
+ import { effect } from './pulse.js';
9
+ import { loggers } from './logger.js';
10
+ import { safeSetAttribute } from './utils.js';
11
+ import { getAdapter } from './dom-adapter.js';
12
+ import { parseSelector } from './dom-selector.js';
13
+
14
+ const log = loggers.dom;
15
+
16
+ // =============================================================================
17
+ // ELEMENT CREATION
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Create a DOM element from a CSS selector-like string
22
+ *
23
+ * @param {string} selector - CSS selector-like string (e.g., 'div.container#main')
24
+ * @param {...*} children - Child elements, text, or reactive functions
25
+ * @returns {HTMLElement} Created DOM element
26
+ */
27
+ export function el(selector, ...children) {
28
+ const dom = getAdapter();
29
+ const config = parseSelector(selector);
30
+ const element = dom.createElement(config.tag);
31
+
32
+ if (config.id) {
33
+ dom.setProperty(element, 'id', config.id);
34
+ }
35
+
36
+ if (config.classes.length > 0) {
37
+ dom.setProperty(element, 'className', config.classes.join(' '));
38
+ }
39
+
40
+ for (const [key, value] of Object.entries(config.attrs)) {
41
+ // Use safeSetAttribute to prevent XSS via event handlers and javascript: URLs
42
+ safeSetAttribute(element, key, value, {}, dom);
43
+ }
44
+
45
+ // Process children
46
+ for (const child of children) {
47
+ appendChild(element, child);
48
+ }
49
+
50
+ return element;
51
+ }
52
+
53
+ /**
54
+ * Append a child to an element, handling various types
55
+ *
56
+ * @private
57
+ * @param {HTMLElement} parent - Parent element
58
+ * @param {*} child - Child to append (string, number, Node, array, or function)
59
+ */
60
+ function appendChild(parent, child) {
61
+ const dom = getAdapter();
62
+
63
+ if (child == null || child === false) return;
64
+
65
+ if (typeof child === 'string' || typeof child === 'number') {
66
+ dom.appendChild(parent, dom.createTextNode(String(child)));
67
+ } else if (dom.isNode(child)) {
68
+ dom.appendChild(parent, child);
69
+ } else if (Array.isArray(child)) {
70
+ for (const c of child) {
71
+ appendChild(parent, c);
72
+ }
73
+ } else if (typeof child === 'function') {
74
+ // Reactive child - create a placeholder and update it
75
+ const placeholder = dom.createComment('pulse');
76
+ dom.appendChild(parent, placeholder);
77
+ let currentNodes = [];
78
+
79
+ effect(() => {
80
+ const result = child();
81
+
82
+ // Remove old nodes
83
+ for (const node of currentNodes) {
84
+ dom.removeNode(node);
85
+ }
86
+ currentNodes = [];
87
+
88
+ // Add new nodes
89
+ if (result != null && result !== false) {
90
+ const fragment = dom.createDocumentFragment();
91
+ if (typeof result === 'string' || typeof result === 'number') {
92
+ const textNode = dom.createTextNode(String(result));
93
+ dom.appendChild(fragment, textNode);
94
+ currentNodes.push(textNode);
95
+ } else if (dom.isNode(result)) {
96
+ dom.appendChild(fragment, result);
97
+ currentNodes.push(result);
98
+ } else if (Array.isArray(result)) {
99
+ for (const r of result) {
100
+ if (dom.isNode(r)) {
101
+ dom.appendChild(fragment, r);
102
+ currentNodes.push(r);
103
+ } else if (r != null && r !== false) {
104
+ const textNode = dom.createTextNode(String(r));
105
+ dom.appendChild(fragment, textNode);
106
+ currentNodes.push(textNode);
107
+ }
108
+ }
109
+ }
110
+ const placeholderParent = dom.getParentNode(placeholder);
111
+ if (placeholderParent) {
112
+ dom.insertBefore(placeholderParent, fragment, dom.getNextSibling(placeholder));
113
+ } else {
114
+ log.warn('Cannot insert reactive children: placeholder has no parent node');
115
+ }
116
+ }
117
+ });
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Create a reactive text node
123
+ *
124
+ * @param {*|Function} getValue - Text value or function returning text
125
+ * @returns {Text} Text node
126
+ */
127
+ export function text(getValue) {
128
+ const dom = getAdapter();
129
+ if (typeof getValue === 'function') {
130
+ const node = dom.createTextNode('');
131
+ effect(() => {
132
+ dom.setTextContent(node, String(getValue()));
133
+ });
134
+ return node;
135
+ }
136
+ return dom.createTextNode(String(getValue));
137
+ }
138
+
139
+ export default {
140
+ el,
141
+ text
142
+ };
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Pulse DOM Lifecycle Module
3
+ * Component lifecycle hooks and mounting utilities
4
+ *
5
+ * @module dom-lifecycle
6
+ */
7
+
8
+ import { pulse, onCleanup } from './pulse.js';
9
+ import { loggers } from './logger.js';
10
+ import { getAdapter } from './dom-adapter.js';
11
+ import { Errors } from './errors.js';
12
+ import { resolveSelector } from './dom-selector.js';
13
+
14
+ const log = loggers.dom;
15
+
16
+ // Context utilities are injected by dom.js to avoid circular dependencies
17
+ let _contextUtils = null;
18
+
19
+ /**
20
+ * Set context utilities for component factory (called by dom.js)
21
+ * @private
22
+ */
23
+ export function _setContextUtils(utils) {
24
+ _contextUtils = utils;
25
+ }
26
+
27
+ // =============================================================================
28
+ // LIFECYCLE TRACKING
29
+ // =============================================================================
30
+
31
+ let currentMountContext = null;
32
+
33
+ /**
34
+ * Register a callback to run when component mounts
35
+ *
36
+ * @param {Function} fn - Callback to run on mount
37
+ */
38
+ export function onMount(fn) {
39
+ if (currentMountContext) {
40
+ currentMountContext.mountCallbacks.push(fn);
41
+ } else {
42
+ // Defer to next microtask if no context
43
+ const dom = getAdapter();
44
+ dom.queueMicrotask(fn);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Register a callback to run when component unmounts
50
+ *
51
+ * @param {Function} fn - Callback to run on unmount
52
+ */
53
+ export function onUnmount(fn) {
54
+ if (currentMountContext) {
55
+ currentMountContext.unmountCallbacks.push(fn);
56
+ }
57
+ // Also register with effect cleanup if in an effect
58
+ onCleanup(fn);
59
+ }
60
+
61
+ /**
62
+ * Get current mount context (for internal use)
63
+ * @returns {Object|null} Current mount context
64
+ */
65
+ export function getMountContext() {
66
+ return currentMountContext;
67
+ }
68
+
69
+ /**
70
+ * Set current mount context (for internal use)
71
+ * @param {Object|null} context - Mount context to set
72
+ * @returns {Object|null} Previous mount context
73
+ */
74
+ export function setMountContext(context) {
75
+ const prev = currentMountContext;
76
+ currentMountContext = context;
77
+ return prev;
78
+ }
79
+
80
+ // =============================================================================
81
+ // MOUNTING
82
+ // =============================================================================
83
+
84
+ /**
85
+ * Mount an element to a target
86
+ *
87
+ * @param {string|HTMLElement} target - CSS selector or DOM element
88
+ * @param {Node} element - Element to mount
89
+ * @returns {Function} Unmount function
90
+ * @throws {Error} If target element is not found
91
+ */
92
+ export function mount(target, element) {
93
+ const dom = getAdapter();
94
+ const { element: resolved, selector } = resolveSelector(target, 'mount');
95
+ if (!resolved) {
96
+ throw Errors.mountNotFound(selector);
97
+ }
98
+ dom.appendChild(resolved, element);
99
+ return () => {
100
+ dom.removeNode(element);
101
+ };
102
+ }
103
+
104
+ // =============================================================================
105
+ // COMPONENT FACTORY
106
+ // =============================================================================
107
+
108
+ /**
109
+ * Create a component factory with lifecycle support
110
+ *
111
+ * @param {Function} setup - Setup function that receives context
112
+ * @returns {Function} Component factory function
113
+ */
114
+ export function component(setup) {
115
+ return (props = {}) => {
116
+ const dom = getAdapter();
117
+ const state = {};
118
+ const methods = {};
119
+
120
+ // Create mount context for lifecycle hooks
121
+ const mountContext = {
122
+ mountCallbacks: [],
123
+ unmountCallbacks: []
124
+ };
125
+
126
+ const prevContext = currentMountContext;
127
+ currentMountContext = mountContext;
128
+
129
+ // Build context with injected utilities
130
+ const ctx = {
131
+ state,
132
+ methods,
133
+ props,
134
+ pulse,
135
+ onMount,
136
+ onUnmount,
137
+ // Spread utilities injected from dom.js
138
+ ...(_contextUtils || {})
139
+ };
140
+
141
+ let result;
142
+ try {
143
+ result = setup(ctx);
144
+ } finally {
145
+ currentMountContext = prevContext;
146
+ }
147
+
148
+ // Schedule mount callbacks after DOM insertion
149
+ if (mountContext.mountCallbacks.length > 0) {
150
+ dom.queueMicrotask(() => {
151
+ for (const cb of mountContext.mountCallbacks) {
152
+ try {
153
+ cb();
154
+ } catch (e) {
155
+ log.error('Mount callback error:', e);
156
+ }
157
+ }
158
+ });
159
+ }
160
+
161
+ // Store unmount callbacks on the element for later cleanup
162
+ if (dom.isNode(result) && mountContext.unmountCallbacks.length > 0) {
163
+ result._pulseUnmount = mountContext.unmountCallbacks;
164
+ }
165
+
166
+ return result;
167
+ };
168
+ }
169
+
170
+ export default {
171
+ onMount,
172
+ onUnmount,
173
+ mount,
174
+ component,
175
+ getMountContext,
176
+ setMountContext,
177
+ _setContextUtils
178
+ };