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
|
@@ -18,13 +18,28 @@ export function enableAdminInputsOnPageLoad() {
|
|
|
18
18
|
if (!isEditMode) return;
|
|
19
19
|
|
|
20
20
|
onDomReady(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
enableAdminInputs();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Runtime toggle functions
|
|
26
|
+
export function enableAdminInputs() {
|
|
27
|
+
document.querySelectorAll('[edit-mode-input]').forEach(input => {
|
|
28
|
+
if (supportsReadonly(input)) {
|
|
29
|
+
input.removeAttribute('readonly');
|
|
30
|
+
} else {
|
|
31
|
+
input.removeAttribute('disabled');
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function disableAdminInputs() {
|
|
37
|
+
document.querySelectorAll('[edit-mode-input]').forEach(input => {
|
|
38
|
+
if (supportsReadonly(input)) {
|
|
39
|
+
input.setAttribute('readonly', '');
|
|
40
|
+
} else {
|
|
41
|
+
input.setAttribute('disabled', '');
|
|
42
|
+
}
|
|
28
43
|
});
|
|
29
44
|
}
|
|
30
45
|
|
|
@@ -55,4 +70,10 @@ function supportsReadonly(element) {
|
|
|
55
70
|
export function init() {
|
|
56
71
|
disableAdminInputsBeforeSave();
|
|
57
72
|
enableAdminInputsOnPageLoad();
|
|
58
|
-
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Export to window
|
|
76
|
+
window.hyperclay = window.hyperclay || {};
|
|
77
|
+
window.hyperclay.enableAdminInputs = enableAdminInputs;
|
|
78
|
+
window.hyperclay.disableAdminInputs = disableAdminInputs;
|
|
79
|
+
window.h = window.hyperclay;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
|
|
2
|
+
import onDomReady from "../dom-utilities/onDomReady.js";
|
|
3
|
+
import {beforeSave} from "./savePage.js";
|
|
4
|
+
|
|
5
|
+
export function disableOnClickBeforeSave () {
|
|
6
|
+
beforeSave(docElem => {
|
|
7
|
+
docElem.querySelectorAll('[edit-mode-onclick]').forEach(resource => {
|
|
8
|
+
const originalValue = resource.getAttribute("onclick");
|
|
9
|
+
resource.setAttribute("inert-onclick", originalValue);
|
|
10
|
+
resource.removeAttribute("onclick");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function enableOnClickForAdminOnPageLoad () {
|
|
16
|
+
if (!isEditMode) return;
|
|
17
|
+
|
|
18
|
+
onDomReady(() => {
|
|
19
|
+
enableOnClick();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Runtime toggle functions
|
|
24
|
+
export function enableOnClick() {
|
|
25
|
+
document.querySelectorAll('[edit-mode-onclick]').forEach(el => {
|
|
26
|
+
const val = el.getAttribute("inert-onclick");
|
|
27
|
+
if (val) {
|
|
28
|
+
el.setAttribute("onclick", val);
|
|
29
|
+
el.removeAttribute("inert-onclick");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function disableOnClick() {
|
|
35
|
+
document.querySelectorAll('[edit-mode-onclick]').forEach(el => {
|
|
36
|
+
const val = el.getAttribute("onclick");
|
|
37
|
+
if (val) {
|
|
38
|
+
el.setAttribute("inert-onclick", val);
|
|
39
|
+
el.removeAttribute("onclick");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Auto-initialize
|
|
45
|
+
export function init() {
|
|
46
|
+
disableOnClickBeforeSave();
|
|
47
|
+
enableOnClickForAdminOnPageLoad();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Export to window
|
|
51
|
+
window.hyperclay = window.hyperclay || {};
|
|
52
|
+
window.hyperclay.enableOnClick = enableOnClick;
|
|
53
|
+
window.hyperclay.disableOnClick = disableOnClick;
|
|
54
|
+
window.h = window.hyperclay;
|
|
@@ -18,11 +18,25 @@ export function enableAdminResourcesOnPageLoad () {
|
|
|
18
18
|
if (!isEditMode) return;
|
|
19
19
|
|
|
20
20
|
onDomReady(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
enableAdminResources();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Runtime toggle functions
|
|
26
|
+
export function enableAdminResources() {
|
|
27
|
+
document.querySelectorAll('[edit-mode-resource]:is(style, link, script)[type^="inert/"]').forEach(resource => {
|
|
28
|
+
resource.type = resource.type.replace(/inert\//g, '');
|
|
29
|
+
resource.replaceWith(resource.cloneNode(true));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function disableAdminResources() {
|
|
34
|
+
document.querySelectorAll('[edit-mode-resource]:is(style, link, script)').forEach(resource => {
|
|
35
|
+
const currentType = resource.getAttribute('type') || 'text/javascript';
|
|
36
|
+
if (!currentType.startsWith('inert/')) {
|
|
37
|
+
resource.setAttribute('type', `inert/${currentType}`);
|
|
24
38
|
resource.replaceWith(resource.cloneNode(true));
|
|
25
|
-
}
|
|
39
|
+
}
|
|
26
40
|
});
|
|
27
41
|
}
|
|
28
42
|
|
|
@@ -30,4 +44,10 @@ export function enableAdminResourcesOnPageLoad () {
|
|
|
30
44
|
export function init() {
|
|
31
45
|
disableAdminResourcesBeforeSave();
|
|
32
46
|
enableAdminResourcesOnPageLoad();
|
|
33
|
-
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Export to window
|
|
50
|
+
window.hyperclay = window.hyperclay || {};
|
|
51
|
+
window.hyperclay.enableAdminResources = enableAdminResources;
|
|
52
|
+
window.hyperclay.disableAdminResources = disableAdminResources;
|
|
53
|
+
window.h = window.hyperclay;
|
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
* Auto-save system for Hyperclay
|
|
3
3
|
*
|
|
4
4
|
* Automatically saves page on DOM changes with throttling.
|
|
5
|
-
* Warns before leaving page with unsaved changes.
|
|
6
5
|
*
|
|
7
6
|
* Requires the 'save-system' module to be loaded first.
|
|
8
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* Recommended companion modules:
|
|
9
|
+
* - 'unsaved-warning' - Warn before leaving with unsaved changes (required for beforeunload)
|
|
10
|
+
* - 'save-toast' - Show toast notifications on save events
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import Mutation from "../utilities/mutation.js";
|
|
12
|
-
import { isEditMode
|
|
13
|
-
import {
|
|
14
|
-
savePageThrottled,
|
|
15
|
-
getUnsavedChanges
|
|
16
|
-
} from "./savePage.js";
|
|
14
|
+
import { isEditMode } from "./isAdminOfCurrentResource.js";
|
|
15
|
+
import { savePageThrottled } from "./savePage.js";
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Initialize auto-save on DOM changes
|
|
@@ -28,16 +27,6 @@ function initSavePageOnChange() {
|
|
|
28
27
|
});
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
/**
|
|
32
|
-
* Warn before leaving page with unsaved changes
|
|
33
|
-
*/
|
|
34
|
-
window.addEventListener('beforeunload', (event) => {
|
|
35
|
-
if (getUnsavedChanges() && isOwner) {
|
|
36
|
-
event.preventDefault();
|
|
37
|
-
event.returnValue = '';
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
30
|
function init() {
|
|
42
31
|
if (!isEditMode) return;
|
|
43
32
|
initSavePageOnChange();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { onSnapshot } from './snapshot.js';
|
|
2
|
+
|
|
3
|
+
// <input type="checkbox" persist>
|
|
4
|
+
export default function enablePersistentFormInputValues(filterBySelector = "[persist]") {
|
|
5
|
+
const inputSelector = `input${filterBySelector}:not([type="password"]):not([type="hidden"]):not([type="file"])`;
|
|
6
|
+
const textareaSelector = `textarea${filterBySelector}`;
|
|
7
|
+
const selectSelector = `select${filterBySelector}`;
|
|
8
|
+
|
|
9
|
+
// Use onSnapshot so form values are synced for both save AND live-sync
|
|
10
|
+
onSnapshot((doc) => {
|
|
11
|
+
// Sync text inputs
|
|
12
|
+
const liveInputs = document.querySelectorAll(inputSelector);
|
|
13
|
+
const clonedInputs = doc.querySelectorAll(inputSelector);
|
|
14
|
+
clonedInputs.forEach((cloned, i) => {
|
|
15
|
+
const live = liveInputs[i];
|
|
16
|
+
if (live.type === 'checkbox' || live.type === 'radio') {
|
|
17
|
+
if (live.checked) {
|
|
18
|
+
cloned.setAttribute('checked', '');
|
|
19
|
+
} else {
|
|
20
|
+
cloned.removeAttribute('checked');
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
cloned.setAttribute('value', live.value);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Sync textareas
|
|
28
|
+
const liveTextareas = document.querySelectorAll(textareaSelector);
|
|
29
|
+
const clonedTextareas = doc.querySelectorAll(textareaSelector);
|
|
30
|
+
clonedTextareas.forEach((cloned, i) => {
|
|
31
|
+
cloned.textContent = liveTextareas[i].value;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Sync selects
|
|
35
|
+
const liveSelects = document.querySelectorAll(selectSelector);
|
|
36
|
+
const clonedSelects = doc.querySelectorAll(selectSelector);
|
|
37
|
+
clonedSelects.forEach((cloned, i) => {
|
|
38
|
+
const live = liveSelects[i];
|
|
39
|
+
const clonedOptions = cloned.querySelectorAll('option');
|
|
40
|
+
clonedOptions.forEach(opt => opt.removeAttribute('selected'));
|
|
41
|
+
|
|
42
|
+
if (live.multiple) {
|
|
43
|
+
Array.from(live.selectedOptions).forEach(opt => {
|
|
44
|
+
const idx = Array.from(live.options).indexOf(opt);
|
|
45
|
+
if (clonedOptions[idx]) clonedOptions[idx].setAttribute('selected', '');
|
|
46
|
+
});
|
|
47
|
+
} else if (live.selectedIndex >= 0 && clonedOptions[live.selectedIndex]) {
|
|
48
|
+
clonedOptions[live.selectedIndex].setAttribute('selected', '');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Auto-initialize with default selector
|
|
55
|
+
export function init() {
|
|
56
|
+
enablePersistentFormInputValues("[persist]");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Auto-export to window unless suppressed by loader
|
|
60
|
+
if (!window.__hyperclayNoAutoExport) {
|
|
61
|
+
window.hyperclay = window.hyperclay || {};
|
|
62
|
+
window.hyperclay.enablePersistentFormInputValues = enablePersistentFormInputValues;
|
|
63
|
+
window.h = window.hyperclay;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Auto-init when module is imported
|
|
67
|
+
init();
|
|
@@ -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
|
}
|
|
@@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
111
111
|
* @param {Function} callback - Optional callback for custom handling
|
|
112
112
|
*/
|
|
113
113
|
export function savePage(callback = () => {}) {
|
|
114
|
-
if (!isEditMode) {
|
|
114
|
+
if (!isEditMode && !window.hyperclay?.testMode) {
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* savePageCore.js — Network save functionality
|
|
3
|
+
*
|
|
4
|
+
* This module handles sending page contents to the server.
|
|
5
|
+
* It uses snapshot.js for capturing the DOM state.
|
|
6
|
+
*
|
|
7
|
+
* For full save system with state management, use savePage.js instead.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import cookie from "../utilities/cookie.js";
|
|
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
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
let saveInProgress = false;
|
|
27
|
+
const saveEndpoint = `/save/${cookie.get("currentResource")}`;
|
|
28
|
+
|
|
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
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
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).
|
|
43
|
+
*
|
|
44
|
+
* @returns {string} HTML string of current page
|
|
45
|
+
*/
|
|
46
|
+
function getContentsForSave() {
|
|
47
|
+
if (isCodeMirrorPage()) {
|
|
48
|
+
// CodeMirror pages don't emit snapshot-ready - no live-sync for code editors
|
|
49
|
+
return getCodeMirrorContents();
|
|
50
|
+
}
|
|
51
|
+
// Emit for live-sync when actually saving
|
|
52
|
+
return captureForSave({ emitForSync: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// SAVE FUNCTIONS
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Save the current page contents to the server.
|
|
61
|
+
*
|
|
62
|
+
* @param {Function} callback - Called with {msg, msgType} on completion
|
|
63
|
+
* msgType will be 'success' or 'error'
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* savePage(({msg, msgType}) => {
|
|
67
|
+
* if (msgType === 'error') {
|
|
68
|
+
* console.error('Save failed:', msg);
|
|
69
|
+
* } else {
|
|
70
|
+
* console.log('Saved:', msg);
|
|
71
|
+
* }
|
|
72
|
+
* });
|
|
73
|
+
*/
|
|
74
|
+
export function savePage(callback = () => {}) {
|
|
75
|
+
if (saveInProgress) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!isEditMode && !window.hyperclay?.testMode) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let currentContents;
|
|
83
|
+
try {
|
|
84
|
+
currentContents = getContentsForSave();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('savePage: getContentsForSave failed', err);
|
|
87
|
+
callback({ msg: err.message, msgType: "error" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
saveInProgress = true;
|
|
91
|
+
|
|
92
|
+
// Test mode: skip network request, return mock success
|
|
93
|
+
if (window.hyperclay?.testMode) {
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
saveInProgress = false;
|
|
96
|
+
if (typeof callback === 'function') {
|
|
97
|
+
callback({ msg: "Test mode: save skipped", msgType: "success" });
|
|
98
|
+
}
|
|
99
|
+
}, 0);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Add timeout - abort if server doesn't respond within 12 seconds
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeoutId = setTimeout(() => controller.abort('Save timeout'), 12000);
|
|
106
|
+
|
|
107
|
+
fetch(saveEndpoint, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
credentials: 'include',
|
|
110
|
+
body: currentContents,
|
|
111
|
+
signal: controller.signal
|
|
112
|
+
})
|
|
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' });
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
.catch(err => {
|
|
128
|
+
clearTimeout(timeoutId);
|
|
129
|
+
console.error('Failed to save page:', err);
|
|
130
|
+
|
|
131
|
+
const msg = err.name === 'AbortError'
|
|
132
|
+
? 'Server not responding'
|
|
133
|
+
: 'Save failed';
|
|
134
|
+
|
|
135
|
+
if (typeof callback === 'function') {
|
|
136
|
+
callback({ msg, msgType: "error" });
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.finally(() => {
|
|
140
|
+
clearTimeout(timeoutId);
|
|
141
|
+
saveInProgress = false;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Save specific HTML content to the server.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} html - HTML string to save
|
|
149
|
+
* @param {Function} callback - Called with (err, data) on completion
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* saveHtml(myHtml, (err, data) => {
|
|
153
|
+
* if (err) {
|
|
154
|
+
* console.error('Save failed:', err);
|
|
155
|
+
* } else {
|
|
156
|
+
* console.log('Saved:', data);
|
|
157
|
+
* }
|
|
158
|
+
* });
|
|
159
|
+
*/
|
|
160
|
+
export function saveHtml(html, callback = () => {}) {
|
|
161
|
+
if (!isEditMode || saveInProgress) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
saveInProgress = true;
|
|
166
|
+
|
|
167
|
+
// Test mode: skip network request, return mock success
|
|
168
|
+
if (window.hyperclay?.testMode) {
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
saveInProgress = false;
|
|
171
|
+
if (typeof callback === 'function') {
|
|
172
|
+
callback(null, { msg: "Test mode: save skipped", msgType: "success" });
|
|
173
|
+
}
|
|
174
|
+
}, 0);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fetch(saveEndpoint, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
credentials: 'include',
|
|
181
|
+
body: html
|
|
182
|
+
})
|
|
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);
|
|
194
|
+
}
|
|
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;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Fetch HTML from a URL and save it to replace the current page.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} url - URL to fetch HTML from
|
|
211
|
+
* @param {Function} callback - Called with (err, data) on completion
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* replacePageWith('/templates/blog.html', (err, data) => {
|
|
215
|
+
* if (err) {
|
|
216
|
+
* console.error('Failed:', err);
|
|
217
|
+
* } else {
|
|
218
|
+
* window.location.reload();
|
|
219
|
+
* }
|
|
220
|
+
* });
|
|
221
|
+
*/
|
|
222
|
+
export function replacePageWith(url, callback = () => {}) {
|
|
223
|
+
if (!isEditMode || saveInProgress) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fetch(url)
|
|
228
|
+
.then(res => res.text())
|
|
229
|
+
.then(html => {
|
|
230
|
+
saveHtml(html, (err, data) => {
|
|
231
|
+
if (typeof callback === 'function') {
|
|
232
|
+
callback(err, data);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
})
|
|
236
|
+
.catch(err => {
|
|
237
|
+
console.error('Failed to fetch template:', err);
|
|
238
|
+
if (typeof callback === 'function') {
|
|
239
|
+
callback(err);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// =============================================================================
|
|
245
|
+
// WINDOW EXPORTS
|
|
246
|
+
// =============================================================================
|
|
247
|
+
|
|
248
|
+
if (!window.__hyperclayNoAutoExport) {
|
|
249
|
+
window.hyperclay = window.hyperclay || {};
|
|
250
|
+
window.hyperclay.savePage = savePage;
|
|
251
|
+
window.hyperclay.saveHtml = saveHtml;
|
|
252
|
+
window.hyperclay.replacePageWith = replacePageWith;
|
|
253
|
+
window.hyperclay.beforeSave = beforeSave;
|
|
254
|
+
window.hyperclay.getPageContents = getPageContents;
|
|
255
|
+
window.h = window.hyperclay;
|
|
256
|
+
}
|