@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// easily convert a form into a JS object
|
|
2
|
+
function getDataFromForm(container) {
|
|
3
|
+
const formData = {};
|
|
4
|
+
|
|
5
|
+
// Helper function to process a single element
|
|
6
|
+
const processElement = (elem) => {
|
|
7
|
+
const name = elem.getAttribute('name');
|
|
8
|
+
const value = elem.value || elem.getAttribute('value');
|
|
9
|
+
|
|
10
|
+
// Skip elements without a name or with a disabled attribute
|
|
11
|
+
if (!name || elem.disabled) return;
|
|
12
|
+
|
|
13
|
+
// Handle different element types
|
|
14
|
+
switch (elem.type) {
|
|
15
|
+
case 'checkbox':
|
|
16
|
+
if (!formData[name]) {
|
|
17
|
+
formData[name] = [];
|
|
18
|
+
}
|
|
19
|
+
if (elem.checked) {
|
|
20
|
+
formData[name].push(value);
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
case 'radio':
|
|
24
|
+
if (elem.checked) {
|
|
25
|
+
formData[name] = value;
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
case 'select-multiple':
|
|
29
|
+
formData[name] = Array.from(elem.selectedOptions, option => option.value);
|
|
30
|
+
break;
|
|
31
|
+
case 'button':
|
|
32
|
+
case 'submit':
|
|
33
|
+
case 'reset':
|
|
34
|
+
// Only include buttons if they have both name and value attributes
|
|
35
|
+
if (name && value) {
|
|
36
|
+
formData[name] = value;
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
formData[name] = value;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// If container is a form, use elements property
|
|
45
|
+
if (container instanceof HTMLFormElement) {
|
|
46
|
+
Array.from(container.elements).forEach(processElement);
|
|
47
|
+
}
|
|
48
|
+
// Otherwise, process container itself and then query for elements with name attribute
|
|
49
|
+
else {
|
|
50
|
+
// Process container element if it has a name attribute
|
|
51
|
+
if (container.hasAttribute('name')) {
|
|
52
|
+
processElement(container);
|
|
53
|
+
}
|
|
54
|
+
// Process all child elements with name attributes
|
|
55
|
+
const elements = container.querySelectorAll('[name]');
|
|
56
|
+
elements.forEach(processElement);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return formData;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default getDataFromForm;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insert styles into the document (inline CSS or external stylesheet).
|
|
3
|
+
*
|
|
4
|
+
* With a persistent DOM (i.e. clayjs), we need a way to update styles.
|
|
5
|
+
* This function reuses existing elements when possible:
|
|
6
|
+
* - Inline styles: matches by data-name, reuses if content matches
|
|
7
|
+
* - External stylesheets: matches by normalized full URL path
|
|
8
|
+
*
|
|
9
|
+
* This ensures:
|
|
10
|
+
* - No DOM churn: existing elements are reused when content/path matches
|
|
11
|
+
* - No duplicates: removes any duplicate style/link elements
|
|
12
|
+
* - Callback always runs: attributes can be updated on existing elements
|
|
13
|
+
*
|
|
14
|
+
* WHY REUSE IN-PLACE:
|
|
15
|
+
* In a persistent DOM (clayjs), removing and re-adding elements changes their
|
|
16
|
+
* position and surrounding whitespace. This causes snapshot diffs even when content
|
|
17
|
+
* is identical, triggering false "unsaved changes" warnings. Reusing existing
|
|
18
|
+
* elements preserves DOM structure for stable snapshots.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* insertStyles('/path/to/file.css') // External stylesheet
|
|
22
|
+
* insertStyles('/path/to/file.css', (link) => { ... }) // With callback
|
|
23
|
+
* insertStyles('my-styles', '.foo { ... }') // Inline CSS
|
|
24
|
+
* insertStyles('my-styles', '.foo { ... }', (style) => { ... }) // With callback
|
|
25
|
+
*/
|
|
26
|
+
function insertStyles(nameOrHref, cssOrCallback, callback) {
|
|
27
|
+
if (typeof cssOrCallback === 'string') {
|
|
28
|
+
// Inline style: insertStyles('my-styles', '.foo { ... }', optionalCallback)
|
|
29
|
+
const name = nameOrHref;
|
|
30
|
+
const css = cssOrCallback;
|
|
31
|
+
const existingStyles = [...document.querySelectorAll(`style[data-name="${name}"]`)];
|
|
32
|
+
|
|
33
|
+
// If exact match exists, just update attributes via callback and return it
|
|
34
|
+
const exactMatch = existingStyles.find(el => el.textContent === css);
|
|
35
|
+
if (exactMatch) {
|
|
36
|
+
if (callback) callback(exactMatch);
|
|
37
|
+
// Remove any duplicates
|
|
38
|
+
existingStyles.filter(el => el !== exactMatch).forEach(el => el.remove());
|
|
39
|
+
return exactMatch;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Update first existing style in-place, or create new one
|
|
43
|
+
let style;
|
|
44
|
+
if (existingStyles.length > 0) {
|
|
45
|
+
style = existingStyles[0];
|
|
46
|
+
style.textContent = css;
|
|
47
|
+
if (callback) callback(style);
|
|
48
|
+
// Remove duplicates
|
|
49
|
+
existingStyles.slice(1).forEach(el => el.remove());
|
|
50
|
+
} else {
|
|
51
|
+
style = document.createElement('style');
|
|
52
|
+
style.dataset.name = name;
|
|
53
|
+
style.textContent = css;
|
|
54
|
+
if (callback) callback(style);
|
|
55
|
+
document.head.appendChild(style);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return style;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// External stylesheet: insertStyles('/path/to/file.css', optionalCallback)
|
|
62
|
+
const href = nameOrHref;
|
|
63
|
+
const cb = typeof cssOrCallback === 'function' ? cssOrCallback : undefined;
|
|
64
|
+
|
|
65
|
+
// Helper to get base URL without query params (for comparison)
|
|
66
|
+
const getBaseUrl = (url) => {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = new URL(url, window.location.href);
|
|
69
|
+
return parsed.origin + parsed.pathname;
|
|
70
|
+
} catch {
|
|
71
|
+
return url;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Normalize href to full URL path (without query params) for comparison
|
|
76
|
+
const normalizedHref = getBaseUrl(href);
|
|
77
|
+
|
|
78
|
+
// Find all links with matching normalized path (ignoring query params like ?v=)
|
|
79
|
+
const existingLinks = [...document.querySelectorAll('link[rel="stylesheet"]')]
|
|
80
|
+
.filter(el => {
|
|
81
|
+
try {
|
|
82
|
+
return getBaseUrl(el.getAttribute('href')) === normalizedHref;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// If match exists, just update attributes via callback and return it
|
|
89
|
+
if (existingLinks.length > 0) {
|
|
90
|
+
const link = existingLinks[0];
|
|
91
|
+
if (cb) cb(link);
|
|
92
|
+
// Remove any duplicates
|
|
93
|
+
existingLinks.slice(1).forEach(el => el.remove());
|
|
94
|
+
return link;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create new link element
|
|
98
|
+
const link = document.createElement('link');
|
|
99
|
+
link.rel = 'stylesheet';
|
|
100
|
+
link.href = href;
|
|
101
|
+
if (cb) cb(link);
|
|
102
|
+
document.head.appendChild(link);
|
|
103
|
+
|
|
104
|
+
return link;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { insertStyles };
|
|
108
|
+
export { insertStyles as insertStyleTag }; // backwards-compat alias
|
|
109
|
+
export default insertStyles;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Searches for elements matching a CSS selector by exploring the DOM tree outward from a starting point,
|
|
3
|
+
* checking nearby elements first before moving to more distant parts of the document.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `element.closest()` which only searches ancestors, this explores the DOM tree in a
|
|
6
|
+
* unique pattern designed to find nearby elements in the visual layout:
|
|
7
|
+
*
|
|
8
|
+
* TRAVERSAL ORDER (for each level):
|
|
9
|
+
* 1. Current element
|
|
10
|
+
* 2. All children of current element (deeply)
|
|
11
|
+
* 3. All previous siblings (right-to-left), exploring each fully with descendants
|
|
12
|
+
* 4. All next siblings (left-to-right), exploring each fully with descendants
|
|
13
|
+
* 5. Move to parent and repeat from step 1
|
|
14
|
+
*
|
|
15
|
+
* KEY FEATURES:
|
|
16
|
+
* - Global visited cache prevents revisiting nodes (big performance boost)
|
|
17
|
+
* - Searches "outward" from start position, checking nearby elements first
|
|
18
|
+
* - Explores each sibling's entire subtree before moving to next sibling
|
|
19
|
+
* - Continues up the ancestor chain until document.body
|
|
20
|
+
*
|
|
21
|
+
* USE CASES:
|
|
22
|
+
* - Finding related UI elements that might be siblings or cousins
|
|
23
|
+
* - Locating the "next" instance of something in reading order
|
|
24
|
+
* - Finding nearby form fields, buttons, or other interactive elements
|
|
25
|
+
*
|
|
26
|
+
* @param {Element} startElem - Starting element for the search
|
|
27
|
+
* @param {string} selector - CSS selector to match
|
|
28
|
+
* @param {Function} elementFoundReturnValue - Transform function for the found element
|
|
29
|
+
* @returns {*} Transformed element if found, null otherwise
|
|
30
|
+
*/
|
|
31
|
+
export default function nearest (startElem, selector, elementFoundReturnValue = x => x) {
|
|
32
|
+
const visited = new Set();
|
|
33
|
+
|
|
34
|
+
// Check node and its descendants using BFS
|
|
35
|
+
function checkDeep(root) {
|
|
36
|
+
if (!root || visited.has(root)) return null;
|
|
37
|
+
|
|
38
|
+
const queue = [root];
|
|
39
|
+
const localVisited = new Set(); // Prevent cycles within this BFS
|
|
40
|
+
|
|
41
|
+
while (queue.length > 0) {
|
|
42
|
+
const node = queue.shift();
|
|
43
|
+
if (!node || localVisited.has(node) || visited.has(node)) continue;
|
|
44
|
+
|
|
45
|
+
visited.add(node);
|
|
46
|
+
localVisited.add(node);
|
|
47
|
+
|
|
48
|
+
if (node.matches(selector)) {
|
|
49
|
+
return elementFoundReturnValue(node);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
queue.push(...node.children);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check siblings in a direction
|
|
58
|
+
function checkSiblings(start, direction) {
|
|
59
|
+
let sibling = start[direction];
|
|
60
|
+
while (sibling) {
|
|
61
|
+
const result = checkDeep(sibling);
|
|
62
|
+
if (result) return result;
|
|
63
|
+
sibling = sibling[direction];
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Main traversal
|
|
69
|
+
// check current → children → siblings → move up
|
|
70
|
+
let current = startElem;
|
|
71
|
+
|
|
72
|
+
while (current) {
|
|
73
|
+
// Check current node (shallow)
|
|
74
|
+
if (!visited.has(current)) {
|
|
75
|
+
visited.add(current);
|
|
76
|
+
if (current.matches(selector)) {
|
|
77
|
+
return elementFoundReturnValue(current);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check children deeply
|
|
82
|
+
for (const child of current.children) {
|
|
83
|
+
const result = checkDeep(child);
|
|
84
|
+
if (result) return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check siblings deeply
|
|
88
|
+
let result = checkSiblings(current, 'previousElementSibling') ||
|
|
89
|
+
checkSiblings(current, 'nextElementSibling');
|
|
90
|
+
if (result) return result;
|
|
91
|
+
|
|
92
|
+
// Move up to parent
|
|
93
|
+
current = current.parentElement;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
package/src/dom/pipe.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default (...fns) => x => fns.reduce((v, f) => f(v), x);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Events module - combines all event attribute handlers
|
|
2
|
+
import { init as initOnclickaway } from './onclickaway.js';
|
|
3
|
+
import { init as initOnclickchildren } from './onclickchildren.js';
|
|
4
|
+
import { init as initOnclone } from './onclone.js';
|
|
5
|
+
import { init as initOnmutation } from './onmutation.js';
|
|
6
|
+
import { init as initOnpagemutation } from './onpagemutation.js';
|
|
7
|
+
import { init as initOnrender } from './onrender.js';
|
|
8
|
+
|
|
9
|
+
function init() {
|
|
10
|
+
initOnclickaway();
|
|
11
|
+
initOnclickchildren();
|
|
12
|
+
initOnclone();
|
|
13
|
+
initOnmutation();
|
|
14
|
+
initOnpagemutation();
|
|
15
|
+
initOnrender();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Auto-init when module is imported
|
|
19
|
+
init();
|
|
20
|
+
|
|
21
|
+
export { init };
|
|
22
|
+
export default init;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function init () {
|
|
2
|
+
|
|
3
|
+
// the code inside `onclickaway` attributes will be executed whenever that element is NOT clicked
|
|
4
|
+
document.addEventListener('click', function(event) {
|
|
5
|
+
const elementsWithOnClickAway = document.querySelectorAll('[onclickaway]');
|
|
6
|
+
|
|
7
|
+
elementsWithOnClickAway.forEach(element => {
|
|
8
|
+
let targetElement = event.target; // clicked element
|
|
9
|
+
|
|
10
|
+
do {
|
|
11
|
+
if (targetElement === element) {
|
|
12
|
+
// Click inside, do nothing
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Go up the DOM
|
|
16
|
+
targetElement = targetElement.parentNode;
|
|
17
|
+
} while (targetElement);
|
|
18
|
+
|
|
19
|
+
// Click outside the element, execute onclickaway
|
|
20
|
+
new Function(element.getAttribute('onclickaway')).call(element);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { init };
|
|
27
|
+
export default init;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function init() {
|
|
2
|
+
document.addEventListener('click', function(event) {
|
|
3
|
+
const parent = event.target.closest('[onclickchildren]');
|
|
4
|
+
if (parent && event.target !== parent) {
|
|
5
|
+
const directChild = event.composedPath().find(el => el.parentNode === parent);
|
|
6
|
+
new Function(parent.getAttribute('onclickchildren')).call(directChild);
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { init };
|
|
12
|
+
export default init;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function init() {
|
|
2
|
+
// Bail if already patched (idempotence)
|
|
3
|
+
if (Node.prototype.__hyperclayOnclone) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const originalCloneNode = Node.prototype.cloneNode;
|
|
8
|
+
|
|
9
|
+
// Store original for idempotence check
|
|
10
|
+
Node.prototype.__hyperclayOnclone = originalCloneNode;
|
|
11
|
+
|
|
12
|
+
Node.prototype.cloneNode = function(deep) {
|
|
13
|
+
const clonedNode = originalCloneNode.call(this, deep);
|
|
14
|
+
|
|
15
|
+
if (clonedNode.nodeType === Node.ELEMENT_NODE) {
|
|
16
|
+
if (!window.__preventOnclone) {
|
|
17
|
+
processOnclone(clonedNode);
|
|
18
|
+
clonedNode.querySelectorAll('[onclone]').forEach(processOnclone);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Patch textareas: the persist module writes live values to data-value
|
|
22
|
+
// on every keystroke (because writing textContent on a focused textarea
|
|
23
|
+
// destroys cursor/scroll). Shift data-value into textContent on the
|
|
24
|
+
// clone so consumers get the current value without special handling.
|
|
25
|
+
if (deep) {
|
|
26
|
+
const textareas = clonedNode.tagName === 'TEXTAREA'
|
|
27
|
+
? [clonedNode]
|
|
28
|
+
: clonedNode.querySelectorAll('textarea[data-value]');
|
|
29
|
+
textareas.forEach(ta => {
|
|
30
|
+
const val = ta.getAttribute('data-value');
|
|
31
|
+
if (val !== null) {
|
|
32
|
+
ta.textContent = val;
|
|
33
|
+
ta.removeAttribute('data-value');
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return clonedNode;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function processOnclone(element) {
|
|
43
|
+
const oncloneCode = element.getAttribute('onclone');
|
|
44
|
+
if (oncloneCode) {
|
|
45
|
+
try {
|
|
46
|
+
new Function(oncloneCode).call(element);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Error executing onclone:', error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export { init };
|
|
54
|
+
export default init;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
[onmutation] - Trigger code when this element or its children change
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
<div onmutation="console.log('My content changed')">
|
|
6
|
+
<span contenteditable>Edit me</span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
Unlike [onglobalmutation]/[onpagemutation] which fires on ANY DOM change,
|
|
10
|
+
[onmutation] only fires when the element itself or its descendants mutate.
|
|
11
|
+
*/
|
|
12
|
+
import Mutation from "../lib/mutation.js";
|
|
13
|
+
|
|
14
|
+
async function executeMutation(element) {
|
|
15
|
+
try {
|
|
16
|
+
const code = element.getAttribute('onmutation');
|
|
17
|
+
if (!code) return;
|
|
18
|
+
const asyncFn = new Function(`return (async function() { ${code} })`)();
|
|
19
|
+
await asyncFn.call(element);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Error in onmutation execution:', error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function init() {
|
|
26
|
+
// Source from the single shared observer instead of one MutationObserver per
|
|
27
|
+
// [onmutation] element. For each change, fire every [onmutation] element whose
|
|
28
|
+
// subtree it belongs to: walk up from the element whose subtree actually
|
|
29
|
+
// changed — the still-attached parent for add/remove (the removed node is
|
|
30
|
+
// detached), otherwise the changed element itself — and run any [onmutation]
|
|
31
|
+
// ancestor, exactly as a per-element subtree observer would. An element's own
|
|
32
|
+
// insertion is its parent's childList change, so (correctly) doesn't fire it.
|
|
33
|
+
//
|
|
34
|
+
// require:'observed' makes it region-aware (skips no-watch / extension noise),
|
|
35
|
+
// which the old per-element observers were not. It stays PAUSABLE (the
|
|
36
|
+
// default): a live-sync morph must NOT fire the hook, or the hook's own DOM
|
|
37
|
+
// writes would autosave → broadcast → morph the other tab → fire the same hook
|
|
38
|
+
// → broadcast back, looping forever. Skipping morphs is safe because the other
|
|
39
|
+
// tab already ran this identical hook, so the morph carries the result.
|
|
40
|
+
Mutation.onAnyChange({ require: 'observed' }, (changes) => {
|
|
41
|
+
const toFire = new Set();
|
|
42
|
+
for (const change of changes) {
|
|
43
|
+
let el = (change.type === 'add' || change.type === 'remove') ? change.parent : change.element;
|
|
44
|
+
while (el && el.nodeType === 1) {
|
|
45
|
+
if (el.hasAttribute('onmutation')) toFire.add(el);
|
|
46
|
+
el = el.parentElement;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
toFire.forEach(executeMutation);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { init };
|
|
54
|
+
export default init;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
[onpagemutation] / [onglobalmutation] - Trigger code when ANY element on the page changes
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
<span onglobalmutation="this.textContent = All('li').length">0</span>
|
|
6
|
+
<span onpagemutation="this.textContent = All('li').length">0</span>
|
|
7
|
+
|
|
8
|
+
Both attributes are equivalent - onglobalmutation is the preferred name for clarity.
|
|
9
|
+
*/
|
|
10
|
+
import Mutation from "../lib/mutation.js";
|
|
11
|
+
|
|
12
|
+
function init() {
|
|
13
|
+
const executeGlobalMutation = async element => {
|
|
14
|
+
try {
|
|
15
|
+
// Support both onglobalmutation and onpagemutation (legacy)
|
|
16
|
+
const code = element.getAttribute('onglobalmutation') || element.getAttribute('onpagemutation');
|
|
17
|
+
const asyncFn = new Function(`return (async function() { ${code} })`)();
|
|
18
|
+
await asyncFn.call(element);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Error in onglobalmutation/onpagemutation execution:', error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Pausable (the default): a live-sync morph must NOT trigger these hooks, or
|
|
25
|
+
// their DOM writes would autosave → broadcast → morph the other tab → fire the
|
|
26
|
+
// same hook → broadcast back, looping forever. Safe to skip morphs because the
|
|
27
|
+
// other tab already ran the identical hook and the morph carries the result.
|
|
28
|
+
Mutation.onAnyChange({
|
|
29
|
+
debounce: 200,
|
|
30
|
+
omitChangeDetails: true,
|
|
31
|
+
require: 'observed'
|
|
32
|
+
}, () => {
|
|
33
|
+
document.querySelectorAll('[onglobalmutation], [onpagemutation]').forEach(executeGlobalMutation);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export { init };
|
|
37
|
+
export default init;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Mutation from "../lib/mutation.js";
|
|
2
|
+
import onLoad from "../lib/on-load.js";
|
|
3
|
+
|
|
4
|
+
function init() {
|
|
5
|
+
const executeRender = async (element) => {
|
|
6
|
+
try {
|
|
7
|
+
const code = element.getAttribute('onrender');
|
|
8
|
+
const asyncFn = new Function(`return (async function() { ${code} })`)();
|
|
9
|
+
await asyncFn.call(element);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Error in onrender execution:', error);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Execute onrender on page load
|
|
16
|
+
onLoad(() => {
|
|
17
|
+
document.querySelectorAll('[onrender]').forEach(executeRender);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Execute onrender when new elements are added
|
|
21
|
+
Mutation.onAddElement({
|
|
22
|
+
selectorFilter: "[onrender]",
|
|
23
|
+
debounce: 200,
|
|
24
|
+
require: 'observed',
|
|
25
|
+
pausable: false
|
|
26
|
+
}, (changes) => {
|
|
27
|
+
changes.forEach(({ element }) => executeRender(element));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { init };
|
|
32
|
+
export default init;
|
package/src/lib/mutation.js
CHANGED
|
@@ -72,7 +72,7 @@ import { isUserDrivenNow, markUserDriven } from './user-gesture.js';
|
|
|
72
72
|
|
|
73
73
|
const dummyElem = document.createElement("div");
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const localMutation = {
|
|
76
76
|
_callbacks: {
|
|
77
77
|
anyChange: [],
|
|
78
78
|
addOrRemove: [],
|
|
@@ -707,13 +707,30 @@ const Mutation = {
|
|
|
707
707
|
}
|
|
708
708
|
};
|
|
709
709
|
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
710
|
+
// Realm-global singleton, keyed by identity not module URL. A satellite bootstrap
|
|
711
|
+
// that imports this module from a different origin (apex vs www, clayjs.com vs
|
|
712
|
+
// jsdelivr, version-pinned URLs) evaluates a second module instance; without this,
|
|
713
|
+
// each would create its own MutationObserver and silently double-observe. The
|
|
714
|
+
// first instance to evaluate publishes its hub on window.__clayMutation; every
|
|
715
|
+
// later instance adopts it and never creates a second observer.
|
|
716
|
+
const existingHub = typeof window !== 'undefined' && window.__clayMutation;
|
|
717
|
+
const Mutation = existingHub || localMutation;
|
|
718
|
+
|
|
719
|
+
if (typeof window !== 'undefined' && !existingHub) {
|
|
720
|
+
window.__clayMutation = Mutation;
|
|
721
|
+
// Publish the vendor-compat mirror BEFORE the readiness dispatch: sap's mutation
|
|
722
|
+
// bridge reads window.hyperclay.Mutation when hyperclay:mutation-ready fires, so
|
|
723
|
+
// the hub must already be in place or sap keeps its own native observer.
|
|
724
|
+
window.hyperclay = window.hyperclay || {};
|
|
725
|
+
window.hyperclay.Mutation = Mutation;
|
|
726
|
+
// Signal consumers (e.g. hypercms ?cms=true auto-open, sap's late-hub listener)
|
|
727
|
+
// that Mutation is ready, so they can react instead of polling. Wrapped so a
|
|
728
|
+
// dispatch failure can never break the install.
|
|
729
|
+
try {
|
|
730
|
+
document.dispatchEvent(new CustomEvent('clay:mutation-ready', { detail: { Mutation } }));
|
|
731
|
+
// vendor-compat: hypercms's readiness fast path listens for the legacy name
|
|
732
|
+
document.dispatchEvent(new CustomEvent('hyperclay:mutation-ready', { detail: { Mutation } }));
|
|
733
|
+
} catch {}
|
|
734
|
+
}
|
|
718
735
|
|
|
719
736
|
export default Mutation;
|
package/src/loader-logic.js
CHANGED
|
@@ -21,9 +21,10 @@ export const PLUGIN_PATHS = {
|
|
|
21
21
|
sortable: { path: "plugins/sortable.js", editOnly: true, default: false },
|
|
22
22
|
undo: { path: "plugins/undo.js", editOnly: true, default: false },
|
|
23
23
|
cms: { path: "vendor/hypercms.vendor.js", editOnly: false, default: false },
|
|
24
|
+
demo: { path: "plugins/demo.js", editOnly: false, default: false },
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "cms", "sync"];
|
|
27
|
+
const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "cms", "sync", "demo"];
|
|
27
28
|
|
|
28
29
|
function parseCsv(params, key, enabled, apply) {
|
|
29
30
|
const raw = params.get(key);
|
package/src/loader.js
CHANGED
|
@@ -108,6 +108,8 @@ function attachPluginMember(path, mod) {
|
|
|
108
108
|
} else if (path === "vendor/hypercms.vendor.js") {
|
|
109
109
|
clay.cms = mod.cms || mod.default;
|
|
110
110
|
window.hyperclay.hypercms = clay.cms;
|
|
111
|
+
} else if (path === "plugins/demo.js") {
|
|
112
|
+
clay.demo = mod.demo;
|
|
111
113
|
} else if (path === "vendor/richclay.vendor.js") {
|
|
112
114
|
window.hyperclay.RichClay = mod.RichClay || mod.default;
|
|
113
115
|
}
|