hyperclayjs 1.8.0 → 1.10.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 +17 -13
- package/package.json +1 -1
- package/src/communication/live-sync.js +412 -0
- package/src/communication/sendMessage.js +2 -8
- package/src/core/autosave.js +6 -17
- package/src/core/enablePersistentFormInputValues.js +40 -45
- package/src/core/optionVisibility.js +7 -35
- package/src/core/savePageCore.js +100 -99
- package/src/core/snapshot.js +203 -0
- package/src/core/unsavedWarning.js +26 -0
- package/src/custom-attributes/ajaxElements.js +3 -10
- package/src/custom-attributes/onaftersave.js +5 -8
- package/src/dom-utilities/insertStyleTag.js +41 -18
- package/src/hyperclay.js +18 -1
- package/src/module-dependency-graph.json +111 -14
- package/src/ui/theModal.js +2 -0
- package/src/utilities/cacheBust.js +19 -0
- package/src/vendor/idiomorph.min.js +1 -0
|
@@ -31,16 +31,15 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import Mutation from "../utilities/mutation.js";
|
|
34
|
+
import insertStyles from "../dom-utilities/insertStyleTag.js";
|
|
35
|
+
|
|
36
|
+
const STYLE_NAME = 'option-visibility';
|
|
34
37
|
|
|
35
38
|
const optionVisibility = {
|
|
36
39
|
debug: false,
|
|
37
40
|
_started: false,
|
|
38
|
-
_styleElement: null,
|
|
39
41
|
_unsubscribe: null,
|
|
40
42
|
|
|
41
|
-
LAYER_NAME: 'option-visibility',
|
|
42
|
-
STYLE_CLASS: 'option-visibility-layer-styles',
|
|
43
|
-
|
|
44
43
|
log(...args) {
|
|
45
44
|
if (this.debug) console.log('[OptionVisibility:Layer]', ...args);
|
|
46
45
|
},
|
|
@@ -104,7 +103,7 @@ const optionVisibility = {
|
|
|
104
103
|
return `[option\\:${safeName}="${safeValue}"]{display:none!important}[${safeName}="${safeValue}"] [option\\:${safeName}="${safeValue}"]{display:revert-layer!important}`;
|
|
105
104
|
}).join('');
|
|
106
105
|
|
|
107
|
-
return `@layer ${
|
|
106
|
+
return `@layer ${STYLE_NAME}{${rules}}`;
|
|
108
107
|
},
|
|
109
108
|
|
|
110
109
|
/**
|
|
@@ -119,33 +118,8 @@ const optionVisibility = {
|
|
|
119
118
|
try {
|
|
120
119
|
const attributes = this.findOptionAttributes();
|
|
121
120
|
const css = this.generateCSS(attributes);
|
|
122
|
-
|
|
123
|
-
// Remove style element if no attributes
|
|
124
|
-
if (!css) {
|
|
125
|
-
if (this._styleElement) {
|
|
126
|
-
this._styleElement.remove();
|
|
127
|
-
this._styleElement = null;
|
|
128
|
-
this.log('Removed empty style element');
|
|
129
|
-
}
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Skip if unchanged
|
|
134
|
-
if (this._styleElement?.textContent === css) {
|
|
135
|
-
this.log('Styles unchanged');
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Create or update
|
|
140
|
-
if (!this._styleElement) {
|
|
141
|
-
this._styleElement = document.createElement('style');
|
|
142
|
-
this._styleElement.className = this.STYLE_CLASS;
|
|
143
|
-
document.head.appendChild(this._styleElement);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
this._styleElement.textContent = css;
|
|
121
|
+
insertStyles(STYLE_NAME, css);
|
|
147
122
|
this.log(`Generated ${attributes.length} rules`);
|
|
148
|
-
|
|
149
123
|
} catch (error) {
|
|
150
124
|
console.error('[OptionVisibility:Layer] Error generating rules:', error);
|
|
151
125
|
}
|
|
@@ -190,10 +164,8 @@ const optionVisibility = {
|
|
|
190
164
|
this._unsubscribe = null;
|
|
191
165
|
}
|
|
192
166
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this._styleElement = null;
|
|
196
|
-
}
|
|
167
|
+
const style = document.querySelector(`style[data-name="${STYLE_NAME}"]`);
|
|
168
|
+
if (style) style.remove();
|
|
197
169
|
|
|
198
170
|
this.log('Stopped');
|
|
199
171
|
}
|
package/src/core/savePageCore.js
CHANGED
|
@@ -1,64 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* savePageCore.js — Network save functionality
|
|
3
3
|
*
|
|
4
|
-
* This
|
|
5
|
-
*
|
|
4
|
+
* This module handles sending page contents to the server.
|
|
5
|
+
* It uses snapshot.js for capturing the DOM state.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* For the full save system with conveniences, use savePage.js instead.
|
|
7
|
+
* For full save system with state management, use savePage.js instead.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import cookie from "../utilities/cookie.js";
|
|
12
11
|
import { isEditMode } from "./isAdminOfCurrentResource.js";
|
|
12
|
+
import {
|
|
13
|
+
captureForSave,
|
|
14
|
+
isCodeMirrorPage,
|
|
15
|
+
getCodeMirrorContents,
|
|
16
|
+
beforeSave,
|
|
17
|
+
getPageContents,
|
|
18
|
+
onSnapshot,
|
|
19
|
+
onPrepareForSave
|
|
20
|
+
} from "./snapshot.js";
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// STATE
|
|
24
|
+
// =============================================================================
|
|
13
25
|
|
|
14
|
-
let beforeSaveCallbacks = [];
|
|
15
26
|
let saveInProgress = false;
|
|
16
27
|
const saveEndpoint = `/save/${cookie.get("currentResource")}`;
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// RE-EXPORTS FROM SNAPSHOT (for backwards compat)
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
export { beforeSave, getPageContents, onSnapshot, onPrepareForSave };
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// INTERNAL: GET PAGE CONTENTS
|
|
37
|
+
// =============================================================================
|
|
27
38
|
|
|
28
39
|
/**
|
|
29
|
-
* Get the current page contents as HTML
|
|
30
|
-
* Handles
|
|
40
|
+
* Get the current page contents as HTML string for saving.
|
|
41
|
+
* Handles both normal pages and CodeMirror editor pages.
|
|
42
|
+
* Emits snapshot-ready event for live-sync (normal pages only).
|
|
31
43
|
*
|
|
32
44
|
* @returns {string} HTML string of current page
|
|
33
45
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let docElem = document.documentElement.cloneNode(true);
|
|
39
|
-
|
|
40
|
-
// Run onbeforesave callbacks
|
|
41
|
-
docElem.querySelectorAll('[onbeforesave]').forEach(el =>
|
|
42
|
-
new Function(el.getAttribute('onbeforesave')).call(el)
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
// Remove elements marked save-ignore
|
|
46
|
-
docElem.querySelectorAll('[save-ignore]').forEach(el =>
|
|
47
|
-
el.remove()
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// Run registered beforeSave callbacks
|
|
51
|
-
beforeSaveCallbacks.forEach(cb => cb(docElem));
|
|
52
|
-
|
|
53
|
-
return "<!DOCTYPE html>" + docElem.outerHTML;
|
|
54
|
-
} else {
|
|
55
|
-
// For CodeMirror pages, get value from editor
|
|
56
|
-
return document.querySelector('.CodeMirror').CodeMirror.getValue();
|
|
46
|
+
function getContentsForSave() {
|
|
47
|
+
if (isCodeMirrorPage()) {
|
|
48
|
+
// CodeMirror pages don't emit snapshot-ready - no live-sync for code editors
|
|
49
|
+
return getCodeMirrorContents();
|
|
57
50
|
}
|
|
51
|
+
// Emit for live-sync when actually saving
|
|
52
|
+
return captureForSave({ emitForSync: true });
|
|
58
53
|
}
|
|
59
54
|
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// SAVE FUNCTIONS
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
60
59
|
/**
|
|
61
|
-
* Save the current page contents to the server
|
|
60
|
+
* Save the current page contents to the server.
|
|
62
61
|
*
|
|
63
62
|
* @param {Function} callback - Called with {msg, msgType} on completion
|
|
64
63
|
* msgType will be 'success' or 'error'
|
|
@@ -82,10 +81,10 @@ export function savePage(callback = () => {}) {
|
|
|
82
81
|
|
|
83
82
|
let currentContents;
|
|
84
83
|
try {
|
|
85
|
-
currentContents =
|
|
84
|
+
currentContents = getContentsForSave();
|
|
86
85
|
} catch (err) {
|
|
87
|
-
console.error('savePage:
|
|
88
|
-
callback({msg: err.message, msgType: "error"});
|
|
86
|
+
console.error('savePage: getContentsForSave failed', err);
|
|
87
|
+
callback({ msg: err.message, msgType: "error" });
|
|
89
88
|
return;
|
|
90
89
|
}
|
|
91
90
|
saveInProgress = true;
|
|
@@ -95,15 +94,15 @@ export function savePage(callback = () => {}) {
|
|
|
95
94
|
setTimeout(() => {
|
|
96
95
|
saveInProgress = false;
|
|
97
96
|
if (typeof callback === 'function') {
|
|
98
|
-
callback({msg: "Test mode: save skipped", msgType: "success"});
|
|
97
|
+
callback({ msg: "Test mode: save skipped", msgType: "success" });
|
|
99
98
|
}
|
|
100
99
|
}, 0);
|
|
101
100
|
return;
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
// Add timeout - abort if server doesn't respond within
|
|
103
|
+
// Add timeout - abort if server doesn't respond within 12 seconds
|
|
105
104
|
const controller = new AbortController();
|
|
106
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
105
|
+
const timeoutId = setTimeout(() => controller.abort('Save timeout'), 12000);
|
|
107
106
|
|
|
108
107
|
fetch(saveEndpoint, {
|
|
109
108
|
method: 'POST',
|
|
@@ -111,44 +110,43 @@ export function savePage(callback = () => {}) {
|
|
|
111
110
|
body: currentContents,
|
|
112
111
|
signal: controller.signal
|
|
113
112
|
})
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
.then(res => {
|
|
114
|
+
clearTimeout(timeoutId);
|
|
115
|
+
return res.json().then(data => {
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
|
|
118
|
+
}
|
|
119
|
+
return data;
|
|
120
|
+
});
|
|
121
|
+
})
|
|
122
|
+
.then(data => {
|
|
123
|
+
if (typeof callback === 'function') {
|
|
124
|
+
callback({ msg: data.msg, msgType: data.msgType || 'success' });
|
|
119
125
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (typeof callback === 'function') {
|
|
125
|
-
callback({msg: data.msg, msgType: data.msgType || 'success'});
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
.catch(err => {
|
|
129
|
-
clearTimeout(timeoutId);
|
|
130
|
-
console.error('Failed to save page:', err);
|
|
126
|
+
})
|
|
127
|
+
.catch(err => {
|
|
128
|
+
clearTimeout(timeoutId);
|
|
129
|
+
console.error('Failed to save page:', err);
|
|
131
130
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
const msg = err.name === 'AbortError'
|
|
132
|
+
? 'Server not responding'
|
|
133
|
+
: 'Save failed';
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
if (typeof callback === 'function') {
|
|
136
|
+
callback({ msg, msgType: "error" });
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.finally(() => {
|
|
140
|
+
clearTimeout(timeoutId);
|
|
141
|
+
saveInProgress = false;
|
|
142
|
+
});
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
/**
|
|
147
|
-
* Save specific HTML content to the server
|
|
146
|
+
* Save specific HTML content to the server.
|
|
148
147
|
*
|
|
149
148
|
* @param {string} html - HTML string to save
|
|
150
149
|
* @param {Function} callback - Called with (err, data) on completion
|
|
151
|
-
* err will be null on success, Error object on failure
|
|
152
150
|
*
|
|
153
151
|
* @example
|
|
154
152
|
* saveHtml(myHtml, (err, data) => {
|
|
@@ -171,7 +169,7 @@ export function saveHtml(html, callback = () => {}) {
|
|
|
171
169
|
setTimeout(() => {
|
|
172
170
|
saveInProgress = false;
|
|
173
171
|
if (typeof callback === 'function') {
|
|
174
|
-
callback(null, {msg: "Test mode: save skipped", msgType: "success"});
|
|
172
|
+
callback(null, { msg: "Test mode: save skipped", msgType: "success" });
|
|
175
173
|
}
|
|
176
174
|
}, 0);
|
|
177
175
|
return;
|
|
@@ -182,32 +180,32 @@ export function saveHtml(html, callback = () => {}) {
|
|
|
182
180
|
credentials: 'include',
|
|
183
181
|
body: html
|
|
184
182
|
})
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
.then(res => {
|
|
184
|
+
return res.json().then(data => {
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
|
|
187
|
+
}
|
|
188
|
+
return data;
|
|
189
|
+
});
|
|
190
|
+
})
|
|
191
|
+
.then(data => {
|
|
192
|
+
if (typeof callback === 'function') {
|
|
193
|
+
callback(null, data);
|
|
189
194
|
}
|
|
190
|
-
|
|
195
|
+
})
|
|
196
|
+
.catch(err => {
|
|
197
|
+
console.error('Failed to save page:', err);
|
|
198
|
+
if (typeof callback === 'function') {
|
|
199
|
+
callback(err);
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
.finally(() => {
|
|
203
|
+
saveInProgress = false;
|
|
191
204
|
});
|
|
192
|
-
})
|
|
193
|
-
.then(data => {
|
|
194
|
-
if (typeof callback === 'function') {
|
|
195
|
-
callback(null, data); // Success: no error
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
.catch(err => {
|
|
199
|
-
console.error('Failed to save page:', err);
|
|
200
|
-
if (typeof callback === 'function') {
|
|
201
|
-
callback(err); // Pass error
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
.finally(() => {
|
|
205
|
-
saveInProgress = false;
|
|
206
|
-
});
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
/**
|
|
210
|
-
* Fetch HTML from a URL and save it to replace the current page
|
|
208
|
+
* Fetch HTML from a URL and save it to replace the current page.
|
|
211
209
|
*
|
|
212
210
|
* @param {string} url - URL to fetch HTML from
|
|
213
211
|
* @param {Function} callback - Called with (err, data) on completion
|
|
@@ -243,7 +241,10 @@ export function replacePageWith(url, callback = () => {}) {
|
|
|
243
241
|
});
|
|
244
242
|
}
|
|
245
243
|
|
|
246
|
-
//
|
|
244
|
+
// =============================================================================
|
|
245
|
+
// WINDOW EXPORTS
|
|
246
|
+
// =============================================================================
|
|
247
|
+
|
|
247
248
|
if (!window.__hyperclayNoAutoExport) {
|
|
248
249
|
window.hyperclay = window.hyperclay || {};
|
|
249
250
|
window.hyperclay.savePage = savePage;
|
|
@@ -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
|
+
// Sends full cloned documentElement so live-sync can extract head and body
|
|
132
|
+
if (emitForSync) {
|
|
133
|
+
document.dispatchEvent(new CustomEvent('hyperclay:snapshot-ready', {
|
|
134
|
+
detail: { documentElement: clone }
|
|
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,
|
|
@@ -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();
|