@panphora/clayjs 0.1.0 → 0.2.1
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/NOTICE +19 -0
- package/README.md +2 -0
- package/_headers +2 -0
- package/all.js +24 -0
- package/clay-data.js +16 -0
- package/clay-dom.js +18 -0
- package/clay-events.js +28 -0
- package/clay-options.js +27 -0
- package/clay-ui.js +29 -0
- package/clay-utils.js +18 -0
- package/clay.js +17 -5
- package/package.json +25 -2
- package/sap.js +16 -0
- package/src/core/save.js +5 -4
- package/src/core/snapshot.js +12 -9
- package/src/dom/all.js +440 -0
- package/src/dom/dom-helpers.js +218 -0
- package/src/dom/form-data.js +62 -0
- package/src/dom/insert-style-tag.js +109 -0
- package/src/dom/nearest.js +97 -0
- package/src/dom/pipe.js +1 -0
- package/src/events/index.js +22 -0
- package/src/events/onclickaway.js +27 -0
- package/src/events/onclickchildren.js +12 -0
- package/src/events/onclone.js +54 -0
- package/src/events/onmutation.js +54 -0
- package/src/events/onpagemutation.js +37 -0
- package/src/events/onrender.js +32 -0
- package/src/lib/mutation.js +26 -9
- package/src/lib/on-load.js +9 -0
- package/src/loader-logic.js +2 -1
- package/src/loader.js +2 -0
- package/src/options/options.js +253 -0
- package/src/plugins/demo.js +124 -0
- package/src/sync/live-sync.js +61 -19
- package/src/sync/merge-tags.js +36 -0
- package/src/ui/dialogs.js +206 -0
- package/src/ui/index.js +38 -0
- package/src/ui/modal.js +884 -0
- package/src/ui/toast.js +434 -0
- package/src/utils/copy-to-clipboard.js +27 -0
- package/src/utils/debounce.js +23 -0
- package/src/utils/index.js +10 -0
- package/src/utils/slugify.js +20 -0
- package/src/vendor/hyper-morph.vendor.js +2 -1
package/src/dom/all.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// Helper to check if an array contains DOM nodes
|
|
2
|
+
const isElementArray = (arr) => {
|
|
3
|
+
if (!Array.isArray(arr)) return false;
|
|
4
|
+
// Empty arrays from element operations should stay wrapped for chainability
|
|
5
|
+
if (arr.length === 0) return true;
|
|
6
|
+
return arr.every(item => item instanceof Element || item instanceof Node || item instanceof Document);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const createMethodHandler = (elements, plugins, methods) => ({
|
|
10
|
+
get(target, prop) {
|
|
11
|
+
// Handle array-like numeric access and length
|
|
12
|
+
if (prop === 'length') return elements.length;
|
|
13
|
+
|
|
14
|
+
// Handle Symbol.iterator BEFORE number check
|
|
15
|
+
if (prop === Symbol.iterator) {
|
|
16
|
+
return elements[Symbol.iterator].bind(elements);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Handle other Symbols (toStringTag, etc.) - return undefined to avoid Symbol-to-number conversion
|
|
20
|
+
if (typeof prop === 'symbol') {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle numeric indices
|
|
25
|
+
if (!isNaN(prop)) return elements[prop];
|
|
26
|
+
|
|
27
|
+
// Handle array methods
|
|
28
|
+
if (prop in Array.prototype) {
|
|
29
|
+
const arrayMethod = Array.prototype[prop];
|
|
30
|
+
return (...args) => {
|
|
31
|
+
const result = arrayMethod.apply(elements, args);
|
|
32
|
+
// For methods that return arrays, only wrap if they contain DOM nodes
|
|
33
|
+
// Otherwise return plain array (jQuery-like behavior for primitives)
|
|
34
|
+
if (Array.isArray(result)) {
|
|
35
|
+
return isElementArray(result)
|
|
36
|
+
? createElementProxy(result, plugins, methods)
|
|
37
|
+
: result; // Return plain array for primitives
|
|
38
|
+
}
|
|
39
|
+
// For single element returns (like find), return raw value
|
|
40
|
+
if (result instanceof Element) {
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
// For boolean/primitive returns (some, every, etc), return raw value
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Handle event listeners and support event delegation
|
|
49
|
+
if (prop.startsWith('on')) {
|
|
50
|
+
return (selectorOrHandlerOrObject, optionalHandler) => {
|
|
51
|
+
const eventName = prop.slice(2);
|
|
52
|
+
|
|
53
|
+
// Handle object of bindings
|
|
54
|
+
if (selectorOrHandlerOrObject && typeof selectorOrHandlerOrObject === 'object') {
|
|
55
|
+
Object.entries(selectorOrHandlerOrObject).forEach(([selector, handler]) => {
|
|
56
|
+
elements.forEach(el => {
|
|
57
|
+
el.addEventListener(eventName, (e) => {
|
|
58
|
+
const closest = e.target.closest(selector);
|
|
59
|
+
if (closest && el.contains(closest)) {
|
|
60
|
+
handler.call(closest, e);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return createElementProxy(elements, plugins, methods);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle single binding (existing delegation code)
|
|
69
|
+
if (optionalHandler) {
|
|
70
|
+
const selector = selectorOrHandlerOrObject;
|
|
71
|
+
const handler = optionalHandler;
|
|
72
|
+
|
|
73
|
+
elements.forEach(el => {
|
|
74
|
+
el.addEventListener(eventName, (e) => {
|
|
75
|
+
const closest = e.target.closest(selector);
|
|
76
|
+
if (closest && el.contains(closest)) {
|
|
77
|
+
handler.call(closest, e);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
const handler = selectorOrHandlerOrObject;
|
|
83
|
+
elements.forEach(el => el.addEventListener(eventName, handler));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return createElementProxy(elements, plugins, methods);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check plugins first
|
|
91
|
+
if (plugins[prop]) {
|
|
92
|
+
const plugin = plugins[prop];
|
|
93
|
+
if (typeof plugin === 'function') {
|
|
94
|
+
return createElementProxy(plugin(elements), plugins, methods);
|
|
95
|
+
}
|
|
96
|
+
if (typeof plugin === 'object') {
|
|
97
|
+
if (plugin.value) {
|
|
98
|
+
return createElementProxy(plugin.value(elements), plugins, methods);
|
|
99
|
+
}
|
|
100
|
+
if (plugin.properties) {
|
|
101
|
+
return createNestedPluginProxy(elements, plugin.properties, plugins, methods);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return plugin;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (methods[prop]) {
|
|
108
|
+
const method = methods[prop];
|
|
109
|
+
return (...args) => {
|
|
110
|
+
const result = method.apply(createElementProxy(elements, plugins, methods), args);
|
|
111
|
+
// Only wrap arrays that contain DOM nodes, return plain arrays for primitives
|
|
112
|
+
return (result instanceof Array && isElementArray(result))
|
|
113
|
+
? createElementProxy(result, plugins, methods)
|
|
114
|
+
: result;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check if we have any elements to work with
|
|
119
|
+
// Example: All('.missing') returns [] so firstEl is null
|
|
120
|
+
const firstEl = elements[0];
|
|
121
|
+
if (!firstEl) {
|
|
122
|
+
// No elements found - need to handle property access gracefully
|
|
123
|
+
// so chains like All.missing.classList.add('hidden') don't break
|
|
124
|
+
|
|
125
|
+
// These properties return objects with their own methods (classList.add, style.color, etc)
|
|
126
|
+
// We know these are safe to proxy even when empty
|
|
127
|
+
if (['style', 'classList', 'dataset'].includes(prop)) {
|
|
128
|
+
return createIntermediateProxy([], prop, plugins, methods);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// For unknown properties, we need to check what they are on the DOM
|
|
132
|
+
// BUT we can't do Element.prototype.classList - that throws "Illegal invocation"
|
|
133
|
+
// Instead we use getOwnPropertyDescriptor which safely tells us ABOUT the property
|
|
134
|
+
const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, prop) ||
|
|
135
|
+
Object.getOwnPropertyDescriptor(Node.prototype, prop);
|
|
136
|
+
|
|
137
|
+
if (descriptor?.get) {
|
|
138
|
+
// Properties like 'children', 'parentElement' have getters
|
|
139
|
+
// Treat them like classList - return a proxy that won't break chains
|
|
140
|
+
return createIntermediateProxy([], prop, plugins, methods);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof descriptor?.value === 'function') {
|
|
144
|
+
// Regular methods like 'appendChild', 'remove'
|
|
145
|
+
// Return a no-op function: All.missing.remove() does nothing, returns proxy
|
|
146
|
+
return (...args) => createElementProxy([], plugins, methods);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Property doesn't exist on DOM elements (like All.missing.foobar)
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const value = firstEl[prop];
|
|
154
|
+
|
|
155
|
+
// The library is designed to handle chaining correctly
|
|
156
|
+
// - When a method returns an Element (like cloneNode), it wraps it in a proxy
|
|
157
|
+
// - When a method returns undefined (like removeAttribute), it chains on the original elements
|
|
158
|
+
// - For other return values, like strings, it returns the results in an array
|
|
159
|
+
// We also handle passing in an all-wrapped proxy object as an argument and loop over all elements in it
|
|
160
|
+
// In the method handler's get function, replace the function call handling with:
|
|
161
|
+
if (typeof value === 'function') {
|
|
162
|
+
return (...args) => {
|
|
163
|
+
// Unwrap any proxy arguments
|
|
164
|
+
const unwrappedArgs = args.map(arg => {
|
|
165
|
+
if (arg && arg.constructor === Proxy) {
|
|
166
|
+
return Array.from(arg);
|
|
167
|
+
}
|
|
168
|
+
return arg;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const results = elements.map(el => {
|
|
172
|
+
// Check if any of the unwrapped arguments are arrays (from proxies)
|
|
173
|
+
const hasProxyArgs = unwrappedArgs.some(Array.isArray);
|
|
174
|
+
|
|
175
|
+
if (hasProxyArgs) {
|
|
176
|
+
// Handle proxy arguments case
|
|
177
|
+
const elementResults = unwrappedArgs.map(arg => {
|
|
178
|
+
if (Array.isArray(arg)) {
|
|
179
|
+
return arg.map(proxyEl => el[prop](proxyEl));
|
|
180
|
+
}
|
|
181
|
+
return [el[prop](...args)];
|
|
182
|
+
}).flat();
|
|
183
|
+
return elementResults[elementResults.length - 1];
|
|
184
|
+
} else {
|
|
185
|
+
// Simple case - just call the method once with original arguments
|
|
186
|
+
return el[prop](...args);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (results[0] instanceof Element) {
|
|
191
|
+
return createElementProxy(results.filter(Boolean), plugins, methods);
|
|
192
|
+
}
|
|
193
|
+
if (results[0] === undefined) {
|
|
194
|
+
return createElementProxy(elements, plugins, methods);
|
|
195
|
+
}
|
|
196
|
+
return results;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Handle intermediate objects (style, classList, dataset)
|
|
201
|
+
if (['style', 'classList', 'dataset'].includes(prop)) {
|
|
202
|
+
return createIntermediateProxy(elements, prop, plugins, methods);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Return array of values for leaf properties
|
|
206
|
+
return elements.map(el => el[prop]);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
set(target, prop, value) {
|
|
210
|
+
if (typeof value === 'function') {
|
|
211
|
+
elements.forEach(el => {
|
|
212
|
+
el[prop] = value(el);
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
elements.forEach(el => {
|
|
216
|
+
el[prop] = value;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const createNestedPluginProxy = (elements, properties, plugins, methods) => {
|
|
224
|
+
return new Proxy({}, {
|
|
225
|
+
get(target, prop) {
|
|
226
|
+
if (properties[prop]) {
|
|
227
|
+
const plugin = properties[prop];
|
|
228
|
+
if (typeof plugin === 'function') {
|
|
229
|
+
return createElementProxy(plugin(elements), plugins, methods);
|
|
230
|
+
}
|
|
231
|
+
return plugin;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const createIntermediateProxy = (elements, propName, plugins, methods) => {
|
|
239
|
+
return new Proxy({}, {
|
|
240
|
+
get(target, prop) {
|
|
241
|
+
// Handle function properties (like classList.add)
|
|
242
|
+
const firstEl = elements[0];
|
|
243
|
+
if (!firstEl) {
|
|
244
|
+
// Return no-op function for any property access on empty collection
|
|
245
|
+
// This ensures chaining continues to work even with no elements
|
|
246
|
+
return (...args) => createElementProxy(elements, plugins, methods);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const value = firstEl[propName][prop];
|
|
250
|
+
if (typeof value === 'function') {
|
|
251
|
+
return (...args) => {
|
|
252
|
+
elements.forEach(el => el[propName][prop](...args));
|
|
253
|
+
return createElementProxy(elements, plugins, methods);
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Return array of values for leaf properties
|
|
258
|
+
return elements.map(el => el[propName][prop]);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
set(target, prop, value) {
|
|
262
|
+
if (typeof value === 'function') {
|
|
263
|
+
elements.forEach(el => {
|
|
264
|
+
el[propName][prop] = value(el);
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
elements.forEach(el => {
|
|
268
|
+
el[propName][prop] = value;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const sharedState = {
|
|
277
|
+
plugins: {},
|
|
278
|
+
methods: {}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const createElementProxy = (elements, plugins = sharedState.plugins, methods = sharedState.methods) => {
|
|
282
|
+
return new Proxy(elements, createMethodHandler(elements, plugins, methods));
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const toElementArray = (input) => {
|
|
286
|
+
if (input instanceof Element || input instanceof Document) {
|
|
287
|
+
return [input];
|
|
288
|
+
}
|
|
289
|
+
if (Array.isArray(input)) {
|
|
290
|
+
if (input.every(el => el instanceof Element || el instanceof Document)) {
|
|
291
|
+
return input;
|
|
292
|
+
}
|
|
293
|
+
throw new TypeError('All array elements must be DOM Elements or Document');
|
|
294
|
+
}
|
|
295
|
+
if (typeof input === 'string') {
|
|
296
|
+
return Array.from(document.querySelectorAll(input));
|
|
297
|
+
}
|
|
298
|
+
throw new TypeError('Input must be a selector string, Element, Document, or Array of Elements');
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Default plugins
|
|
302
|
+
const defaultPlugins = {
|
|
303
|
+
methods: {
|
|
304
|
+
eq(index) {
|
|
305
|
+
if (typeof index !== 'number') {
|
|
306
|
+
throw new TypeError('eq() requires a number as an argument');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Handle negative indices (counting from the end)
|
|
310
|
+
const normalizedIndex = index < 0 ? this.length + index : index;
|
|
311
|
+
|
|
312
|
+
// Return array with single element at index, or empty array if index is invalid
|
|
313
|
+
// ❗️ Allows chaining to continue
|
|
314
|
+
return this[normalizedIndex] ? [this[normalizedIndex]] : [];
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
at(index) {
|
|
318
|
+
if (typeof index !== 'number') {
|
|
319
|
+
throw new TypeError('at() requires a number as an argument');
|
|
320
|
+
}
|
|
321
|
+
const normalizedIndex = index < 0 ? this.length + index : index;
|
|
322
|
+
return this[normalizedIndex];
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
prop(properties) {
|
|
326
|
+
if (typeof properties !== 'object' || properties === null) {
|
|
327
|
+
throw new TypeError('prop() requires an object of properties');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
Object.entries(properties).forEach(([key, value]) => {
|
|
331
|
+
this.forEach(el => {
|
|
332
|
+
el[key] = value;
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return this;
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
css(styles) {
|
|
340
|
+
if (typeof styles !== 'object' || styles === null) {
|
|
341
|
+
throw new TypeError('css() requires an object of styles');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
Object.entries(styles).forEach(([property, value]) => {
|
|
345
|
+
this.forEach(el => {
|
|
346
|
+
el.style[property] = value;
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return this;
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
pluck(attr) {
|
|
354
|
+
return this.map(el => el.getAttribute(attr));
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
unique() {
|
|
358
|
+
return [...new Set(this)];
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
sortBy(fn) {
|
|
362
|
+
if (typeof fn === 'string') {
|
|
363
|
+
const attr = fn;
|
|
364
|
+
fn = el => el.getAttribute(attr);
|
|
365
|
+
}
|
|
366
|
+
return [...this].sort((a, b) => {
|
|
367
|
+
const aVal = fn(a);
|
|
368
|
+
const bVal = fn(b);
|
|
369
|
+
if (aVal < bVal) return -1;
|
|
370
|
+
if (aVal > bVal) return 1;
|
|
371
|
+
return 0;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const All = new Proxy(function (selectorOrElements, contextSelector) {
|
|
378
|
+
// If there's a context selector, search within that context
|
|
379
|
+
if (arguments.length === 2) {
|
|
380
|
+
// Support both string selectors and element references as context
|
|
381
|
+
let contextElements;
|
|
382
|
+
if (typeof contextSelector === 'string') {
|
|
383
|
+
contextElements = Array.from(document.querySelectorAll(contextSelector));
|
|
384
|
+
} else {
|
|
385
|
+
// Convert element/array/document to element array
|
|
386
|
+
contextElements = toElementArray(contextSelector);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// If first arg is a string selector, search within each context
|
|
390
|
+
if (typeof selectorOrElements === 'string') {
|
|
391
|
+
const elements = contextElements.flatMap(context => {
|
|
392
|
+
// Include context itself if it matches the selector
|
|
393
|
+
const matches = context.matches(selectorOrElements) ? [context] : [];
|
|
394
|
+
// Plus all descendants that match
|
|
395
|
+
const descendants = Array.from(context.querySelectorAll(selectorOrElements));
|
|
396
|
+
return [...matches, ...descendants];
|
|
397
|
+
});
|
|
398
|
+
return createElementProxy(elements);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// If first arg is elements, filter to those within context
|
|
402
|
+
const searchElements = toElementArray(selectorOrElements);
|
|
403
|
+
const elements = searchElements.filter(el =>
|
|
404
|
+
contextElements.some(context => context.contains(el))
|
|
405
|
+
);
|
|
406
|
+
return createElementProxy(elements);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Single argument - normalize and return
|
|
410
|
+
const elements = toElementArray(selectorOrElements);
|
|
411
|
+
return createElementProxy(elements);
|
|
412
|
+
}, {
|
|
413
|
+
get(target, prop) {
|
|
414
|
+
if (prop === 'use') {
|
|
415
|
+
return function (plugin) {
|
|
416
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
417
|
+
throw new TypeError('Plugin must be an object with "properties" or "methods"');
|
|
418
|
+
}
|
|
419
|
+
if (plugin.properties) {
|
|
420
|
+
Object.assign(sharedState.plugins, plugin.properties);
|
|
421
|
+
}
|
|
422
|
+
if (plugin.methods) {
|
|
423
|
+
Object.assign(sharedState.methods, plugin.methods);
|
|
424
|
+
}
|
|
425
|
+
return this;
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (prop === Symbol.iterator) return undefined;
|
|
430
|
+
if (prop in target) return target[prop];
|
|
431
|
+
|
|
432
|
+
const elements = Array.from(document.querySelectorAll(`[${prop}], .${prop}`));
|
|
433
|
+
return createElementProxy(elements);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Install default plugins
|
|
438
|
+
All.use(defaultPlugins);
|
|
439
|
+
|
|
440
|
+
export default All;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import nearest from "./nearest.js";
|
|
2
|
+
import pipe from "./pipe.js";
|
|
3
|
+
|
|
4
|
+
function init () {
|
|
5
|
+
|
|
6
|
+
// Bail if already initialized
|
|
7
|
+
if (HTMLElement.prototype.hasOwnProperty('nearest')) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// elem.el.menu returns nearest element with a "menu" attribute (alias for elem.nearest.menu)
|
|
12
|
+
// elem.nearest.project returns nearest element with a "project" attribute
|
|
13
|
+
Object.defineProperty(HTMLElement.prototype, 'el', {
|
|
14
|
+
configurable: true,
|
|
15
|
+
get: function() {
|
|
16
|
+
let element = this;
|
|
17
|
+
|
|
18
|
+
const handler = {
|
|
19
|
+
get(target, prop) {
|
|
20
|
+
return nearest(element, `[${prop}], .${prop}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return new Proxy({}, handler);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
Object.defineProperty(HTMLElement.prototype, 'nearest', {
|
|
29
|
+
configurable: true,
|
|
30
|
+
get: function() {
|
|
31
|
+
let element = this;
|
|
32
|
+
|
|
33
|
+
const handler = {
|
|
34
|
+
get(target, prop) {
|
|
35
|
+
return nearest(element, `[${prop}], .${prop}`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return new Proxy({}, handler);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// elem.val.project returns the value of the nearest element with "project" attribute
|
|
44
|
+
// elem.val.project = "hello world" sets the value of the nearest element with "project" attribute
|
|
45
|
+
// For form elements (input/select/textarea), uses the value property; otherwise uses the attribute
|
|
46
|
+
Object.defineProperty(HTMLElement.prototype, 'val', {
|
|
47
|
+
configurable: true,
|
|
48
|
+
get: function() {
|
|
49
|
+
let element = this;
|
|
50
|
+
|
|
51
|
+
const isFormElement = (elem) =>
|
|
52
|
+
elem.tagName === 'INPUT' || elem.tagName === 'SELECT' || elem.tagName === 'TEXTAREA';
|
|
53
|
+
|
|
54
|
+
const handler = {
|
|
55
|
+
get(target, prop) {
|
|
56
|
+
return nearest(element, `[${prop}], .${prop}`, elem => {
|
|
57
|
+
if (isFormElement(elem)) {
|
|
58
|
+
return elem.value;
|
|
59
|
+
}
|
|
60
|
+
return elem.getAttribute(prop);
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
set(target, prop, value) {
|
|
64
|
+
const foundElem = nearest(element, `[${prop}], .${prop}`);
|
|
65
|
+
|
|
66
|
+
if (foundElem) {
|
|
67
|
+
if (isFormElement(foundElem)) {
|
|
68
|
+
foundElem.value = value;
|
|
69
|
+
} else {
|
|
70
|
+
foundElem.setAttribute(prop, value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return new Proxy({}, handler);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// elem.text.project returns the innerText of the nearest element with the "project" attribute
|
|
83
|
+
// elem.text.project = "hello world" sets the innerText of the nearest element with the "project" attribute
|
|
84
|
+
Object.defineProperty(HTMLElement.prototype, 'text', {
|
|
85
|
+
configurable: true,
|
|
86
|
+
get: function() {
|
|
87
|
+
let element = this;
|
|
88
|
+
|
|
89
|
+
const handler = {
|
|
90
|
+
get(target, prop) {
|
|
91
|
+
return nearest(element, `[${prop}], .${prop}`, elem => elem.innerText);
|
|
92
|
+
},
|
|
93
|
+
set(target, prop, value) {
|
|
94
|
+
const foundElem = nearest(element, `[${prop}], .${prop}`);
|
|
95
|
+
|
|
96
|
+
if (foundElem) {
|
|
97
|
+
foundElem.innerText = value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return new Proxy({}, handler);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// elem.exec.sync_out() executes the code in the nearest "sync_out" attribute, using elem as the `this`
|
|
109
|
+
Object.defineProperty(HTMLElement.prototype, 'exec', {
|
|
110
|
+
configurable: true,
|
|
111
|
+
get: function() {
|
|
112
|
+
let element = this;
|
|
113
|
+
|
|
114
|
+
const handler = {
|
|
115
|
+
get(target, prop) {
|
|
116
|
+
return function() {
|
|
117
|
+
const foundElem = nearest(element, `[${prop}], .${prop}`);
|
|
118
|
+
if (foundElem) {
|
|
119
|
+
const code = foundElem.getAttribute(prop);
|
|
120
|
+
if (code) {
|
|
121
|
+
return new Function(code).call(foundElem);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return new Proxy({}, handler);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* CYCLE METHODS
|
|
134
|
+
*
|
|
135
|
+
* Element.prototype.cycle - Cycles through elements with a specific attribute and replaces the current element with the next one
|
|
136
|
+
* Usage: element.cycle(1, 'project') - Replaces element with the next element that has 'project' attribute
|
|
137
|
+
*
|
|
138
|
+
* Element.prototype.cycleAttr - Cycles through possible values of a specific attribute on matching elements
|
|
139
|
+
* Usage:
|
|
140
|
+
* element.cycleAttr(1, 'project_type') - Sets project_type to the next value found on elements with project_type
|
|
141
|
+
* element.cycleAttr(1, 'project_type', 'option:project_type') - Sets project_type based on values from option:project_type
|
|
142
|
+
*
|
|
143
|
+
* Notes:
|
|
144
|
+
* - Both methods support forward/backward cycling with positive/negative order parameter
|
|
145
|
+
* - For attributes containing special characters like colons (e.g., 'option:project_type'),
|
|
146
|
+
* the colon is automatically escaped for the CSS selector
|
|
147
|
+
* - cycle() replaces the entire element with the next one
|
|
148
|
+
* - cycleAttr() only changes the attribute value on the current element
|
|
149
|
+
*/
|
|
150
|
+
Element.prototype.cycle = function(order = 1, attr) {
|
|
151
|
+
const escapedAttr = attr.replace(/:/g, '\\:');
|
|
152
|
+
const next = pipe(
|
|
153
|
+
// Get all elements
|
|
154
|
+
() => document.querySelectorAll(`[${escapedAttr}]`),
|
|
155
|
+
|
|
156
|
+
// Convert to array of {element, value} objects
|
|
157
|
+
els => Array.from(els).map(el => ({
|
|
158
|
+
element: el,
|
|
159
|
+
value: el.textContent.trim()
|
|
160
|
+
})),
|
|
161
|
+
|
|
162
|
+
// Get unique by value
|
|
163
|
+
els => [...new Map(els.map(el => [el.value, el])).values()],
|
|
164
|
+
|
|
165
|
+
// Sort by value
|
|
166
|
+
els => els.sort((a, b) => a.value.localeCompare(b.value)),
|
|
167
|
+
|
|
168
|
+
// Find next element object
|
|
169
|
+
els => els[(els.findIndex(el => el.value === this.textContent.trim()) + order + els.length) % els.length],
|
|
170
|
+
|
|
171
|
+
// Return just the element
|
|
172
|
+
obj => obj.element
|
|
173
|
+
)();
|
|
174
|
+
|
|
175
|
+
if (next) this.replaceWith(next.cloneNode(true));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
Element.prototype.cycleAttr = function(order = 1, setAttr, lookupAttr) {
|
|
179
|
+
// If lookupAttr is not provided, use setAttr for both
|
|
180
|
+
lookupAttr = lookupAttr || setAttr;
|
|
181
|
+
|
|
182
|
+
const escapedLookupAttr = lookupAttr.replace(/:/g, '\\:');
|
|
183
|
+
const next = pipe(
|
|
184
|
+
// Get all elements that match the lookup attribute
|
|
185
|
+
() => document.querySelectorAll(`[${escapedLookupAttr}]`),
|
|
186
|
+
|
|
187
|
+
// Get unique attribute values
|
|
188
|
+
els => [...new Set(Array.from(els).map(el =>
|
|
189
|
+
el.getAttribute(lookupAttr)
|
|
190
|
+
))],
|
|
191
|
+
|
|
192
|
+
// Sort them
|
|
193
|
+
vals => vals.sort(),
|
|
194
|
+
|
|
195
|
+
// Get next value based on current setAttr value
|
|
196
|
+
vals => vals[(vals.indexOf(this.getAttribute(setAttr)) + order + vals.length) % vals.length]
|
|
197
|
+
)();
|
|
198
|
+
|
|
199
|
+
if (next) this.setAttribute(setAttr, next);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
Element.prototype.show = function() {
|
|
203
|
+
this.style.display = '';
|
|
204
|
+
return this;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
Element.prototype.hide = function() {
|
|
208
|
+
this.style.display = 'none';
|
|
209
|
+
return this;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Auto-init when module is imported
|
|
215
|
+
init();
|
|
216
|
+
|
|
217
|
+
export { init };
|
|
218
|
+
export default init;
|