hyperclayjs 1.7.0 → 1.9.0
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/README.md +25 -21
- package/package.json +17 -25
- package/src/communication/live-sync.js +396 -0
- package/{communication → src/communication}/sendMessage.js +2 -8
- package/src/core/adminContenteditable.js +51 -0
- package/{core → src/core}/adminInputs.js +29 -8
- package/src/core/adminOnClick.js +54 -0
- package/{core → src/core}/adminResources.js +25 -5
- package/{core → src/core}/autosave.js +6 -17
- package/src/core/enablePersistentFormInputValues.js +67 -0
- package/{core → src/core}/optionVisibility.js +7 -35
- package/{core → src/core}/savePage.js +1 -1
- package/src/core/savePageCore.js +256 -0
- package/src/core/snapshot.js +203 -0
- package/src/core/unsavedWarning.js +26 -0
- package/{custom-attributes → src/custom-attributes}/ajaxElements.js +3 -10
- package/{custom-attributes → src/custom-attributes}/domHelpers.js +17 -4
- package/{custom-attributes → src/custom-attributes}/events.js +2 -0
- package/{custom-attributes → src/custom-attributes}/onaftersave.js +5 -8
- package/src/custom-attributes/onmutation.js +90 -0
- package/src/custom-attributes/onpagemutation.js +32 -0
- package/{custom-attributes → src/custom-attributes}/sortable.js +16 -1
- package/{dom-utilities → src/dom-utilities}/All.js +22 -0
- package/src/dom-utilities/insertStyleTag.js +61 -0
- package/{hyperclay.js → src/hyperclay.js} +20 -3
- package/{module-dependency-graph.json → src/module-dependency-graph.json} +121 -34
- package/{ui → src/ui}/prompts.js +13 -18
- package/{ui → src/ui}/theModal.js +103 -0
- package/{ui → src/ui}/toast.js +4 -3
- package/src/utilities/cacheBust.js +19 -0
- package/{vendor → src/vendor}/idiomorph.min.js +1 -0
- package/core/adminContenteditable.js +0 -36
- package/core/adminOnClick.js +0 -31
- package/core/enablePersistentFormInputValues.js +0 -72
- package/core/savePageCore.js +0 -245
- package/custom-attributes/onpagemutation.js +0 -20
- package/dom-utilities/insertStyleTag.js +0 -38
- /package/{communication → src/communication}/behaviorCollector.js +0 -0
- /package/{communication → src/communication}/uploadFile.js +0 -0
- /package/{core → src/core}/adminSystem.js +0 -0
- /package/{core → src/core}/editmode.js +0 -0
- /package/{core → src/core}/editmodeSystem.js +0 -0
- /package/{core → src/core}/exportToWindow.js +0 -0
- /package/{core → src/core}/isAdminOfCurrentResource.js +0 -0
- /package/{core → src/core}/saveToast.js +0 -0
- /package/{core → src/core}/setPageTypeOnDocumentElement.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/autosize.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/inputHelpers.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/onclickaway.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/onclone.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/onrender.js +0 -0
- /package/{custom-attributes → src/custom-attributes}/preventEnter.js +0 -0
- /package/{dom-utilities → src/dom-utilities}/getDataFromForm.js +0 -0
- /package/{dom-utilities → src/dom-utilities}/onDomReady.js +0 -0
- /package/{dom-utilities → src/dom-utilities}/onLoad.js +0 -0
- /package/{string-utilities → src/string-utilities}/copy-to-clipboard.js +0 -0
- /package/{string-utilities → src/string-utilities}/query.js +0 -0
- /package/{string-utilities → src/string-utilities}/slugify.js +0 -0
- /package/{ui → src/ui}/toast-hyperclay.js +0 -0
- /package/{utilities → src/utilities}/cookie.js +0 -0
- /package/{utilities → src/utilities}/debounce.js +0 -0
- /package/{utilities → src/utilities}/loadVendorScript.js +0 -0
- /package/{utilities → src/utilities}/mutation.js +0 -0
- /package/{utilities → src/utilities}/nearest.js +0 -0
- /package/{utilities → src/utilities}/pipe.js +0 -0
- /package/{utilities → src/utilities}/throttle.js +0 -0
- /package/{vendor → src/vendor}/Sortable.vendor.js +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snapshot.js — The source of truth for page state
|
|
3
|
+
*
|
|
4
|
+
* THE SAVE/SYNC PIPELINE:
|
|
5
|
+
*
|
|
6
|
+
* ┌─────────────────────────────────────────────────────────┐
|
|
7
|
+
* │ 1. CLONE document.documentElement.cloneNode() │
|
|
8
|
+
* └─────────────────────────────────────────────────────────┘
|
|
9
|
+
* │
|
|
10
|
+
* ▼
|
|
11
|
+
* ┌─────────────────────────────────────────────────────────┐
|
|
12
|
+
* │ 2. SNAPSHOT HOOKS onSnapshot callbacks │
|
|
13
|
+
* │ (form value sync) │
|
|
14
|
+
* │ │
|
|
15
|
+
* │ ✓ Used by: SAVE and LIVE-SYNC │
|
|
16
|
+
* └─────────────────────────────────────────────────────────┘
|
|
17
|
+
* │
|
|
18
|
+
* ┌───────────────┴───────────────┐
|
|
19
|
+
* ▼ ▼
|
|
20
|
+
* ┌─────────────────────────┐ ┌─────────────────────────┐
|
|
21
|
+
* │ 3a. PREPARE HOOKS │ │ 3b. DONE │
|
|
22
|
+
* │ onPrepareForSave │ │ (live-sync stops here) │
|
|
23
|
+
* │ [onbeforesave] │ │ │
|
|
24
|
+
* │ [save-ignore] │ │ → emits snapshot-ready │
|
|
25
|
+
* │ │ └─────────────────────────┘
|
|
26
|
+
* │ ✓ Used by: SAVE only │
|
|
27
|
+
* └─────────────────────────┘
|
|
28
|
+
* │
|
|
29
|
+
* ▼
|
|
30
|
+
* ┌─────────────────────────┐
|
|
31
|
+
* │ 4. SERIALIZE │
|
|
32
|
+
* │ "<!DOCTYPE html>" │
|
|
33
|
+
* │ + outerHTML │
|
|
34
|
+
* │ │
|
|
35
|
+
* │ → sent to server │
|
|
36
|
+
* └─────────────────────────┘
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// HOOK REGISTRIES
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
const snapshotHooks = []; // Phase 2: Always run (form sync)
|
|
44
|
+
const prepareForSaveHooks = []; // Phase 3a: Save only (strip admin)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register a hook that runs on EVERY snapshot (save AND sync).
|
|
48
|
+
* Use for: syncing form values to the clone.
|
|
49
|
+
*
|
|
50
|
+
* @param {Function} callback - Receives the cloned document element
|
|
51
|
+
*/
|
|
52
|
+
export function onSnapshot(callback) {
|
|
53
|
+
snapshotHooks.push(callback);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Register a hook that runs ONLY when preparing for save.
|
|
58
|
+
* Use for: stripping admin elements, cleanup.
|
|
59
|
+
*
|
|
60
|
+
* @param {Function} callback - Receives the cloned document element
|
|
61
|
+
*/
|
|
62
|
+
export function onPrepareForSave(callback) {
|
|
63
|
+
prepareForSaveHooks.push(callback);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Backwards compat alias
|
|
67
|
+
export const beforeSave = onPrepareForSave;
|
|
68
|
+
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// CAPTURE FUNCTIONS
|
|
71
|
+
// =============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* PHASE 1-2: Clone the DOM and run snapshot hooks.
|
|
75
|
+
*
|
|
76
|
+
* This is the "canonical" state — form values synced, nothing stripped.
|
|
77
|
+
* Used as the base for both saving and syncing.
|
|
78
|
+
*
|
|
79
|
+
* @returns {HTMLElement} Cloned document element with snapshot hooks applied
|
|
80
|
+
*/
|
|
81
|
+
export function captureSnapshot() {
|
|
82
|
+
const clone = document.documentElement.cloneNode(true);
|
|
83
|
+
|
|
84
|
+
for (const hook of snapshotHooks) {
|
|
85
|
+
hook(clone);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return clone;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Prepare an already-captured snapshot for saving.
|
|
93
|
+
* Mutates the clone — only call once per snapshot.
|
|
94
|
+
*
|
|
95
|
+
* @param {HTMLElement} clone - A snapshot from captureSnapshot()
|
|
96
|
+
* @returns {string} Full HTML string ready for server
|
|
97
|
+
*/
|
|
98
|
+
function prepareCloneForSave(clone) {
|
|
99
|
+
// Run inline [onbeforesave] handlers
|
|
100
|
+
for (const el of clone.querySelectorAll('[onbeforesave]')) {
|
|
101
|
+
new Function(el.getAttribute('onbeforesave')).call(el);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Remove elements that shouldn't be saved
|
|
105
|
+
for (const el of clone.querySelectorAll('[save-ignore]')) {
|
|
106
|
+
el.remove();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Run registered prepare hooks
|
|
110
|
+
for (const hook of prepareForSaveHooks) {
|
|
111
|
+
hook(clone);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return "<!DOCTYPE html>" + clone.outerHTML;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* PHASE 1-4: Full pipeline for saving to server.
|
|
119
|
+
*
|
|
120
|
+
* Captures snapshot, emits for live-sync, then prepares for save.
|
|
121
|
+
* This is the main entry point for the save process.
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} options
|
|
124
|
+
* @param {boolean} options.emitForSync - Whether to emit snapshot-ready event (default: true)
|
|
125
|
+
* @returns {string} Full HTML string ready for server
|
|
126
|
+
*/
|
|
127
|
+
export function captureForSave({ emitForSync = true } = {}) {
|
|
128
|
+
const clone = captureSnapshot();
|
|
129
|
+
|
|
130
|
+
// Emit for live-sync before stripping admin elements
|
|
131
|
+
if (emitForSync) {
|
|
132
|
+
const bodyForSync = clone.querySelector('body').innerHTML;
|
|
133
|
+
document.dispatchEvent(new CustomEvent('hyperclay:snapshot-ready', {
|
|
134
|
+
detail: { body: bodyForSync }
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return prepareCloneForSave(clone);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* PHASE 1-2 (body only): For live-sync between admin users.
|
|
143
|
+
*
|
|
144
|
+
* Includes admin elements — no stripping.
|
|
145
|
+
* Note: Prefer listening to 'hyperclay:snapshot-ready' event instead,
|
|
146
|
+
* which reuses the save's clone.
|
|
147
|
+
*
|
|
148
|
+
* @returns {string} Body innerHTML with form values synced
|
|
149
|
+
*/
|
|
150
|
+
export function captureBodyForSync() {
|
|
151
|
+
const clone = captureSnapshot();
|
|
152
|
+
return clone.querySelector('body').innerHTML;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get page contents for change detection.
|
|
157
|
+
* Does NOT emit snapshot-ready event (safe for comparison).
|
|
158
|
+
*
|
|
159
|
+
* For CodeMirror pages, returns editor content directly.
|
|
160
|
+
* For normal pages, returns full HTML via snapshot pipeline.
|
|
161
|
+
*/
|
|
162
|
+
export function getPageContents() {
|
|
163
|
+
if (isCodeMirrorPage()) {
|
|
164
|
+
return getCodeMirrorContents();
|
|
165
|
+
}
|
|
166
|
+
return captureForSave({ emitForSync: false });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// =============================================================================
|
|
170
|
+
// CODEMIRROR SUPPORT
|
|
171
|
+
// =============================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if this is a CodeMirror editor page.
|
|
175
|
+
* CodeMirror pages bypass the normal snapshot pipeline.
|
|
176
|
+
*/
|
|
177
|
+
export function isCodeMirrorPage() {
|
|
178
|
+
return !!document.querySelector('.CodeMirror')?.CodeMirror;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get CodeMirror editor contents.
|
|
183
|
+
* @returns {string} Editor contents
|
|
184
|
+
*/
|
|
185
|
+
export function getCodeMirrorContents() {
|
|
186
|
+
return document.querySelector('.CodeMirror').CodeMirror.getValue();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// WINDOW EXPORTS
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
if (!window.__hyperclayNoAutoExport) {
|
|
194
|
+
window.hyperclay = window.hyperclay || {};
|
|
195
|
+
window.hyperclay.captureSnapshot = captureSnapshot;
|
|
196
|
+
window.hyperclay.captureForSave = captureForSave;
|
|
197
|
+
window.hyperclay.captureBodyForSync = captureBodyForSync;
|
|
198
|
+
window.hyperclay.onSnapshot = onSnapshot;
|
|
199
|
+
window.hyperclay.onPrepareForSave = onPrepareForSave;
|
|
200
|
+
window.hyperclay.beforeSave = beforeSave;
|
|
201
|
+
window.hyperclay.getPageContents = getPageContents;
|
|
202
|
+
window.h = window.hyperclay;
|
|
203
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unsaved Warning Module
|
|
3
|
+
*
|
|
4
|
+
* Warns users before leaving the page if there are unsaved changes.
|
|
5
|
+
* Self-contained: compares current page content to last saved content on beforeunload.
|
|
6
|
+
*
|
|
7
|
+
* Works independently of autosave - no mutation observer needed during editing,
|
|
8
|
+
* just a single comparison when the user tries to leave.
|
|
9
|
+
*
|
|
10
|
+
* Requires the 'save-system' module (automatically included as dependency).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isOwner, isEditMode } from "./isAdminOfCurrentResource.js";
|
|
14
|
+
import { getPageContents, getLastSavedContents } from "./savePage.js";
|
|
15
|
+
|
|
16
|
+
window.addEventListener('beforeunload', (event) => {
|
|
17
|
+
if (!isOwner || !isEditMode) return;
|
|
18
|
+
|
|
19
|
+
const currentContents = getPageContents();
|
|
20
|
+
const lastSaved = getLastSavedContents();
|
|
21
|
+
|
|
22
|
+
if (currentContents !== lastSaved) {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
event.returnValue = '';
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -38,16 +38,9 @@ function submitAjax(elem) {
|
|
|
38
38
|
}
|
|
39
39
|
method = (method || 'POST').toUpperCase();
|
|
40
40
|
|
|
41
|
-
// Get data -
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Button inside form: use form data
|
|
45
|
-
data = getDataFromForm(parentForm);
|
|
46
|
-
} else if (!isButton) {
|
|
47
|
-
// It's a form element itself
|
|
48
|
-
data = getDataFromForm(elem);
|
|
49
|
-
}
|
|
50
|
-
// For standalone buttons with no form, data remains empty object
|
|
41
|
+
// Get data - from parent form if button is inside one, otherwise from element itself
|
|
42
|
+
const dataSource = (isButton && parentForm) ? parentForm : elem;
|
|
43
|
+
const data = getDataFromForm(dataSource);
|
|
51
44
|
|
|
52
45
|
fetch(url, {
|
|
53
46
|
method: method,
|
|
@@ -24,22 +24,35 @@ function init () {
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
// elem.val.project returns the value of the nearest "project" attribute
|
|
28
|
-
// elem.val.project = "hello world" sets the value of the nearest "project" attribute
|
|
27
|
+
// elem.val.project returns the value of the nearest element with "project" attribute
|
|
28
|
+
// elem.val.project = "hello world" sets the value of the nearest element with "project" attribute
|
|
29
|
+
// For form elements (input/select/textarea), uses the value property; otherwise uses the attribute
|
|
29
30
|
Object.defineProperty(HTMLElement.prototype, 'val', {
|
|
30
31
|
configurable: true,
|
|
31
32
|
get: function() {
|
|
32
33
|
let element = this;
|
|
33
34
|
|
|
35
|
+
const isFormElement = (elem) =>
|
|
36
|
+
elem.tagName === 'INPUT' || elem.tagName === 'SELECT' || elem.tagName === 'TEXTAREA';
|
|
37
|
+
|
|
34
38
|
const handler = {
|
|
35
39
|
get(target, prop) {
|
|
36
|
-
return nearest(element, `[${prop}], .${prop}`, elem =>
|
|
40
|
+
return nearest(element, `[${prop}], .${prop}`, elem => {
|
|
41
|
+
if (isFormElement(elem)) {
|
|
42
|
+
return elem.value;
|
|
43
|
+
}
|
|
44
|
+
return elem.getAttribute(prop);
|
|
45
|
+
});
|
|
37
46
|
},
|
|
38
47
|
set(target, prop, value) {
|
|
39
48
|
const foundElem = nearest(element, `[${prop}], .${prop}`);
|
|
40
49
|
|
|
41
50
|
if (foundElem) {
|
|
42
|
-
foundElem
|
|
51
|
+
if (isFormElement(foundElem)) {
|
|
52
|
+
foundElem.value = value;
|
|
53
|
+
} else {
|
|
54
|
+
foundElem.setAttribute(prop, value);
|
|
55
|
+
}
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
return true;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Events module - combines all event attribute handlers
|
|
2
2
|
import { init as initOnclickaway } from './onclickaway.js';
|
|
3
3
|
import { init as initOnclone } from './onclone.js';
|
|
4
|
+
import { init as initOnmutation } from './onmutation.js';
|
|
4
5
|
import { init as initOnpagemutation } from './onpagemutation.js';
|
|
5
6
|
import { init as initOnrender } from './onrender.js';
|
|
6
7
|
|
|
7
8
|
function init() {
|
|
8
9
|
initOnclickaway();
|
|
9
10
|
initOnclone();
|
|
11
|
+
initOnmutation();
|
|
10
12
|
initOnpagemutation();
|
|
11
13
|
initOnrender();
|
|
12
14
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [onaftersave] Custom Attribute
|
|
3
3
|
*
|
|
4
|
-
* Runs inline JavaScript
|
|
5
|
-
*
|
|
4
|
+
* Runs inline JavaScript after a successful save.
|
|
5
|
+
* Only fires on 'hyperclay:save-saved' events (not on error/offline).
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* <span onaftersave="this.innerText = event.detail.msg"></span>
|
|
9
|
-
* <
|
|
9
|
+
* <link href="styles.css" onaftersave="cacheBust(this)">
|
|
10
10
|
*
|
|
11
11
|
* The event.detail object contains:
|
|
12
|
-
* - status: '
|
|
13
|
-
* - msg: string (e.g., 'Saved'
|
|
12
|
+
* - status: 'saved'
|
|
13
|
+
* - msg: string (e.g., 'Saved')
|
|
14
14
|
* - timestamp: number (Date.now())
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -30,10 +30,7 @@ function broadcast(e) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function init() {
|
|
33
|
-
document.addEventListener('hyperclay:save-saving', broadcast);
|
|
34
33
|
document.addEventListener('hyperclay:save-saved', broadcast);
|
|
35
|
-
document.addEventListener('hyperclay:save-offline', broadcast);
|
|
36
|
-
document.addEventListener('hyperclay:save-error', broadcast);
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
init();
|
|
@@ -0,0 +1,90 @@
|
|
|
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 "../utilities/mutation.js";
|
|
13
|
+
|
|
14
|
+
const observers = new WeakMap();
|
|
15
|
+
|
|
16
|
+
function setupMutationObserver(element) {
|
|
17
|
+
if (observers.has(element)) return;
|
|
18
|
+
|
|
19
|
+
const executeMutation = async () => {
|
|
20
|
+
try {
|
|
21
|
+
const code = element.getAttribute('onmutation');
|
|
22
|
+
if (!code) return;
|
|
23
|
+
const asyncFn = new Function(`return (async function() { ${code} })`)();
|
|
24
|
+
await asyncFn.call(element);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Error in onmutation execution:', error);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const observer = new MutationObserver(() => {
|
|
31
|
+
executeMutation();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
observer.observe(element, {
|
|
35
|
+
childList: true,
|
|
36
|
+
subtree: true,
|
|
37
|
+
characterData: true,
|
|
38
|
+
attributes: true
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
observers.set(element, observer);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function teardownMutationObserver(element) {
|
|
45
|
+
const observer = observers.get(element);
|
|
46
|
+
if (observer) {
|
|
47
|
+
observer.disconnect();
|
|
48
|
+
observers.delete(element);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function init() {
|
|
53
|
+
// Set up existing elements
|
|
54
|
+
document.querySelectorAll('[onmutation]').forEach(setupMutationObserver);
|
|
55
|
+
|
|
56
|
+
// Watch for dynamically added elements with onmutation
|
|
57
|
+
Mutation.onAddElement({
|
|
58
|
+
selectorFilter: '[onmutation]',
|
|
59
|
+
debounce: 200
|
|
60
|
+
}, (changes) => {
|
|
61
|
+
changes.forEach(({ element }) => {
|
|
62
|
+
setupMutationObserver(element);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Clean up when elements are removed
|
|
67
|
+
Mutation.onRemoveElement({
|
|
68
|
+
selectorFilter: '[onmutation]',
|
|
69
|
+
debounce: 200
|
|
70
|
+
}, (changes) => {
|
|
71
|
+
changes.forEach(({ element }) => {
|
|
72
|
+
teardownMutationObserver(element);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Watch for attribute removal
|
|
77
|
+
Mutation.onAttribute({
|
|
78
|
+
selectorFilter: '[onmutation]',
|
|
79
|
+
debounce: 200
|
|
80
|
+
}, (changes) => {
|
|
81
|
+
changes.forEach(({ element, attribute, newValue }) => {
|
|
82
|
+
if (attribute === 'onmutation' && newValue === null) {
|
|
83
|
+
teardownMutationObserver(element);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { init };
|
|
90
|
+
export default init;
|
|
@@ -0,0 +1,32 @@
|
|
|
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 "../utilities/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
|
+
Mutation.onAnyChange({
|
|
25
|
+
debounce: 200,
|
|
26
|
+
omitChangeDetails: true
|
|
27
|
+
}, () => {
|
|
28
|
+
document.querySelectorAll('[onglobalmutation], [onpagemutation]').forEach(executeGlobalMutation);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export { init };
|
|
32
|
+
export default init;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
How to use:
|
|
6
6
|
- add `sortable` attribute to an element to make children sortable
|
|
7
7
|
- e.g. <div sortable></div>
|
|
8
|
+
- add `onsorting` attribute to execute code during drag
|
|
9
|
+
- e.g. <ul sortable onsorting="console.log('Dragging!')"></ul>
|
|
8
10
|
- add `onsorted` attribute to execute code when items are sorted
|
|
9
11
|
- e.g. <ul sortable onsorted="console.log('Items reordered!')"></ul>
|
|
10
12
|
|
|
@@ -39,7 +41,20 @@ function makeSortable(sortableElem, Sortable) {
|
|
|
39
41
|
options.handle = '[sortable-handle]';
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
// Add
|
|
44
|
+
// Add onsorting callback if attribute exists (fires during drag)
|
|
45
|
+
const onsortingCode = sortableElem.getAttribute('onsorting');
|
|
46
|
+
if (onsortingCode) {
|
|
47
|
+
options.onMove = function(evt) {
|
|
48
|
+
try {
|
|
49
|
+
const asyncFn = new Function(`return (async function(evt) { ${onsortingCode} })`)();
|
|
50
|
+
asyncFn.call(sortableElem, evt);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error in onsorting execution:', error);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add onsorted callback if attribute exists (fires after drop)
|
|
43
58
|
const onsortedCode = sortableElem.getAttribute('onsorted');
|
|
44
59
|
if (onsortedCode) {
|
|
45
60
|
options.onEnd = function(evt) {
|
|
@@ -336,6 +336,28 @@ const defaultPlugins = {
|
|
|
336
336
|
});
|
|
337
337
|
|
|
338
338
|
return this;
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
pluck(attr) {
|
|
342
|
+
return this.map(el => el.getAttribute(attr));
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
unique() {
|
|
346
|
+
return [...new Set(this)];
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
sortBy(fn) {
|
|
350
|
+
if (typeof fn === 'string') {
|
|
351
|
+
const attr = fn;
|
|
352
|
+
fn = el => el.getAttribute(attr);
|
|
353
|
+
}
|
|
354
|
+
return [...this].sort((a, b) => {
|
|
355
|
+
const aVal = fn(a);
|
|
356
|
+
const bVal = fn(b);
|
|
357
|
+
if (aVal < bVal) return -1;
|
|
358
|
+
if (aVal > bVal) return 1;
|
|
359
|
+
return 0;
|
|
360
|
+
});
|
|
339
361
|
}
|
|
340
362
|
}
|
|
341
363
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insert styles into the document (inline CSS or external stylesheet).
|
|
3
|
+
*
|
|
4
|
+
* With a persistent DOM (i.e. hyperclay), we need a way to update styles.
|
|
5
|
+
* This function always inserts the new styles first, then removes any
|
|
6
|
+
* duplicates. This ensures:
|
|
7
|
+
* - No flickering: new styles are applied before old ones are removed
|
|
8
|
+
* - Always upgrades: we default to the new styles/approach
|
|
9
|
+
*/
|
|
10
|
+
function insertStyles(nameOrHref, css) {
|
|
11
|
+
if (css !== undefined) {
|
|
12
|
+
// Inline style: insertStyles('my-styles', '.foo { ... }')
|
|
13
|
+
const name = nameOrHref;
|
|
14
|
+
const oldStyles = document.querySelectorAll(`style[data-name="${name}"]`);
|
|
15
|
+
|
|
16
|
+
const style = document.createElement('style');
|
|
17
|
+
style.dataset.name = name;
|
|
18
|
+
style.textContent = css;
|
|
19
|
+
document.head.appendChild(style);
|
|
20
|
+
|
|
21
|
+
oldStyles.forEach(el => el.remove());
|
|
22
|
+
return style;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// External stylesheet: insertStyles('/path/to/file.css')
|
|
26
|
+
const href = nameOrHref;
|
|
27
|
+
|
|
28
|
+
let identifier;
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(href, window.location.href);
|
|
31
|
+
identifier = url.pathname.split('/').pop();
|
|
32
|
+
} catch (e) {
|
|
33
|
+
identifier = href;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const oldLinks = document.querySelectorAll(
|
|
37
|
+
`link[href="${href}"], link[href*="${identifier}"]`
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const link = document.createElement('link');
|
|
41
|
+
link.rel = 'stylesheet';
|
|
42
|
+
link.href = href;
|
|
43
|
+
document.head.appendChild(link);
|
|
44
|
+
|
|
45
|
+
oldLinks.forEach(el => el.remove());
|
|
46
|
+
return link;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Auto-export to window unless suppressed by loader
|
|
50
|
+
if (!window.__hyperclayNoAutoExport) {
|
|
51
|
+
window.insertStyles = insertStyles;
|
|
52
|
+
window.insertStyleTag = insertStyles; // backwards-compat alias
|
|
53
|
+
window.hyperclay = window.hyperclay || {};
|
|
54
|
+
window.hyperclay.insertStyles = insertStyles;
|
|
55
|
+
window.hyperclay.insertStyleTag = insertStyles; // backwards-compat alias
|
|
56
|
+
window.h = window.hyperclay;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { insertStyles };
|
|
60
|
+
export { insertStyles as insertStyleTag }; // backwards-compat alias
|
|
61
|
+
export default insertStyles;
|