hyperclayjs 1.0.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/LICENSE +21 -0
- package/README.md +360 -0
- package/README.template.md +276 -0
- package/communication/behaviorCollector.js +230 -0
- package/communication/sendMessage.js +48 -0
- package/communication/uploadFile.js +348 -0
- package/core/adminContenteditable.js +36 -0
- package/core/adminInputs.js +58 -0
- package/core/adminOnClick.js +31 -0
- package/core/adminResources.js +33 -0
- package/core/adminSystem.js +15 -0
- package/core/editmode.js +8 -0
- package/core/editmodeSystem.js +18 -0
- package/core/enablePersistentFormInputValues.js +62 -0
- package/core/isAdminOfCurrentResource.js +13 -0
- package/core/optionVisibilityRuleGenerator.js +160 -0
- package/core/savePage.js +196 -0
- package/core/savePageCore.js +236 -0
- package/core/setPageTypeOnDocumentElement.js +23 -0
- package/custom-attributes/ajaxElements.js +94 -0
- package/custom-attributes/autosize.js +17 -0
- package/custom-attributes/domHelpers.js +175 -0
- package/custom-attributes/events.js +15 -0
- package/custom-attributes/inputHelpers.js +11 -0
- package/custom-attributes/onclickaway.js +27 -0
- package/custom-attributes/onclone.js +35 -0
- package/custom-attributes/onpagemutation.js +20 -0
- package/custom-attributes/onrender.js +30 -0
- package/custom-attributes/preventEnter.js +13 -0
- package/custom-attributes/sortable.js +76 -0
- package/dom-utilities/All.js +412 -0
- package/dom-utilities/getDataFromForm.js +60 -0
- package/dom-utilities/insertStyleTag.js +28 -0
- package/dom-utilities/onDomReady.js +7 -0
- package/dom-utilities/onLoad.js +7 -0
- package/hyperclay.js +465 -0
- package/module-dependency-graph.json +612 -0
- package/package.json +95 -0
- package/string-utilities/copy-to-clipboard.js +35 -0
- package/string-utilities/emmet-html.js +54 -0
- package/string-utilities/query.js +1 -0
- package/string-utilities/slugify.js +21 -0
- package/ui/info.js +39 -0
- package/ui/prompts.js +179 -0
- package/ui/theModal.js +677 -0
- package/ui/toast.js +273 -0
- package/utilities/cookie.js +45 -0
- package/utilities/debounce.js +12 -0
- package/utilities/mutation.js +403 -0
- package/utilities/nearest.js +97 -0
- package/utilities/pipe.js +1 -0
- package/utilities/throttle.js +21 -0
- package/vendor/Sortable.js +3351 -0
- package/vendor/idiomorph.min.js +8 -0
- package/vendor/tailwind-base.css +1471 -0
- package/vendor/tailwind-play.js +169 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core save functionality for Hyperclay
|
|
3
|
+
*
|
|
4
|
+
* This is the minimal save system - just the basic save function you can call yourself.
|
|
5
|
+
* No toast notifications, no auto-save, no keyboard shortcuts.
|
|
6
|
+
*
|
|
7
|
+
* Use this if you want full control over save behavior and notifications.
|
|
8
|
+
* For the full save system with conveniences, use savePage.js instead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import cookie from "../utilities/cookie.js";
|
|
12
|
+
import { isEditMode } from "./isAdminOfCurrentResource.js";
|
|
13
|
+
|
|
14
|
+
let beforeSaveCallbacks = [];
|
|
15
|
+
let saveInProgress = false;
|
|
16
|
+
const saveEndpoint = `/save/${cookie.get("currentResource")}`;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a callback to run before saving
|
|
20
|
+
* Callbacks receive the cloned document element
|
|
21
|
+
*
|
|
22
|
+
* @param {Function} cb - Callback function(docElem)
|
|
23
|
+
*/
|
|
24
|
+
export function beforeSave(cb) {
|
|
25
|
+
beforeSaveCallbacks.push(cb);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the current page contents as HTML
|
|
30
|
+
* Handles CodeMirror pages, runs [onbeforesave] attributes, removes [save-ignore] elements
|
|
31
|
+
*
|
|
32
|
+
* @returns {string} HTML string of current page
|
|
33
|
+
*/
|
|
34
|
+
export function getPageContents() {
|
|
35
|
+
const isCodeMirrorPage = !!document.querySelector('.CodeMirror')?.CodeMirror;
|
|
36
|
+
|
|
37
|
+
if (!isCodeMirrorPage) {
|
|
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();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Save the current page contents to the server
|
|
62
|
+
*
|
|
63
|
+
* @param {Function} callback - Called with {msg, msgType} on completion
|
|
64
|
+
* msgType will be 'success' or 'error'
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* savePage(({msg, msgType}) => {
|
|
68
|
+
* if (msgType === 'error') {
|
|
69
|
+
* console.error('Save failed:', msg);
|
|
70
|
+
* } else {
|
|
71
|
+
* console.log('Saved:', msg);
|
|
72
|
+
* }
|
|
73
|
+
* });
|
|
74
|
+
*/
|
|
75
|
+
export function savePage(callback = () => {}) {
|
|
76
|
+
if (!isEditMode || saveInProgress) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const currentContents = getPageContents();
|
|
81
|
+
saveInProgress = true;
|
|
82
|
+
|
|
83
|
+
// Test mode: skip network request, return mock success
|
|
84
|
+
if (window.hyperclay?.testMode) {
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
saveInProgress = false;
|
|
87
|
+
if (typeof callback === 'function') {
|
|
88
|
+
callback({msg: "Test mode: save skipped", msgType: "success"});
|
|
89
|
+
}
|
|
90
|
+
}, 0);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fetch(saveEndpoint, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
credentials: 'include',
|
|
97
|
+
body: currentContents
|
|
98
|
+
})
|
|
99
|
+
.then(res => {
|
|
100
|
+
return res.json().then(data => {
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
|
|
103
|
+
}
|
|
104
|
+
return data;
|
|
105
|
+
});
|
|
106
|
+
})
|
|
107
|
+
.then(data => {
|
|
108
|
+
if (typeof callback === 'function') {
|
|
109
|
+
callback({msg: data.msg, msgType: data.msgType || 'success'});
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
.catch(err => {
|
|
113
|
+
console.error('Failed to save page:', err);
|
|
114
|
+
if (typeof callback === 'function') {
|
|
115
|
+
callback({msg: err.message || "Failed to save", msgType: "error"});
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
.finally(() => {
|
|
119
|
+
saveInProgress = false;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Save specific HTML content to the server
|
|
125
|
+
*
|
|
126
|
+
* @param {string} html - HTML string to save
|
|
127
|
+
* @param {Function} callback - Called with (err, data) on completion
|
|
128
|
+
* err will be null on success, Error object on failure
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* saveHtml(myHtml, (err, data) => {
|
|
132
|
+
* if (err) {
|
|
133
|
+
* console.error('Save failed:', err);
|
|
134
|
+
* } else {
|
|
135
|
+
* console.log('Saved:', data);
|
|
136
|
+
* }
|
|
137
|
+
* });
|
|
138
|
+
*/
|
|
139
|
+
export function saveHtml(html, callback = () => {}) {
|
|
140
|
+
if (!isEditMode || saveInProgress) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
saveInProgress = true;
|
|
145
|
+
|
|
146
|
+
// Test mode: skip network request, return mock success
|
|
147
|
+
if (window.hyperclay?.testMode) {
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
saveInProgress = false;
|
|
150
|
+
if (typeof callback === 'function') {
|
|
151
|
+
callback(null, {msg: "Test mode: save skipped", msgType: "success"});
|
|
152
|
+
}
|
|
153
|
+
}, 0);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fetch(saveEndpoint, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
credentials: 'include',
|
|
160
|
+
body: html
|
|
161
|
+
})
|
|
162
|
+
.then(res => {
|
|
163
|
+
return res.json().then(data => {
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
throw new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
|
|
166
|
+
}
|
|
167
|
+
return data;
|
|
168
|
+
});
|
|
169
|
+
})
|
|
170
|
+
.then(data => {
|
|
171
|
+
if (typeof callback === 'function') {
|
|
172
|
+
callback(null, data); // Success: no error
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
.catch(err => {
|
|
176
|
+
console.error('Failed to save page:', err);
|
|
177
|
+
if (typeof callback === 'function') {
|
|
178
|
+
callback(err); // Pass error
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
.finally(() => {
|
|
182
|
+
saveInProgress = false;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Fetch HTML from a URL and save it to replace the current page
|
|
188
|
+
*
|
|
189
|
+
* @param {string} url - URL to fetch HTML from
|
|
190
|
+
* @param {Function} callback - Called with (err, data) on completion
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* replacePageWith('/templates/blog.html', (err, data) => {
|
|
194
|
+
* if (err) {
|
|
195
|
+
* console.error('Failed:', err);
|
|
196
|
+
* } else {
|
|
197
|
+
* window.location.reload();
|
|
198
|
+
* }
|
|
199
|
+
* });
|
|
200
|
+
*/
|
|
201
|
+
export function replacePageWith(url, callback = () => {}) {
|
|
202
|
+
if (!isEditMode || saveInProgress) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fetch(url)
|
|
207
|
+
.then(res => res.text())
|
|
208
|
+
.then(html => {
|
|
209
|
+
saveHtml(html, (err, data) => {
|
|
210
|
+
if (typeof callback === 'function') {
|
|
211
|
+
callback(err, data);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
})
|
|
215
|
+
.catch(err => {
|
|
216
|
+
console.error('Failed to fetch template:', err);
|
|
217
|
+
if (typeof callback === 'function') {
|
|
218
|
+
callback(err);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Export save functions to window.hyperclay
|
|
225
|
+
*/
|
|
226
|
+
export function exportToWindow() {
|
|
227
|
+
if (!window.hyperclay) {
|
|
228
|
+
window.hyperclay = {};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
window.hyperclay.savePage = savePage;
|
|
232
|
+
window.hyperclay.saveHtml = saveHtml;
|
|
233
|
+
window.hyperclay.replacePageWith = replacePageWith;
|
|
234
|
+
window.hyperclay.beforeSave = beforeSave;
|
|
235
|
+
window.hyperclay.getPageContents = getPageContents;
|
|
236
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 setViewerPageTypeBeforeSave () {
|
|
6
|
+
beforeSave(docElem => {
|
|
7
|
+
docElem.setAttribute("editmode", "false");
|
|
8
|
+
docElem.setAttribute("pageowner", "false");
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setPageTypeOnPageLoad () {
|
|
13
|
+
onDomReady(() => {
|
|
14
|
+
document.documentElement.setAttribute("editmode", isEditMode ? "true" : "false");
|
|
15
|
+
document.documentElement.setAttribute("pageowner", isOwner ? "true" : "false");
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Auto-initialize
|
|
20
|
+
export function init() {
|
|
21
|
+
setViewerPageTypeBeforeSave();
|
|
22
|
+
setPageTypeOnPageLoad();
|
|
23
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import getDataFromForm from "../dom-utilities/getDataFromForm.js";
|
|
2
|
+
|
|
3
|
+
function handleFormSubmit(event) {
|
|
4
|
+
const form = event.target;
|
|
5
|
+
if (form.hasAttribute('ajax-form')) {
|
|
6
|
+
event.preventDefault();
|
|
7
|
+
submitAjax(form);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function handleButtonClick(event) {
|
|
12
|
+
const button = event.target.closest('[ajax-button]');
|
|
13
|
+
if (button) {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
submitAjax(button);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function submitAjax(elem) {
|
|
20
|
+
callSubmitEvents(elem).then(() => {
|
|
21
|
+
// Ajax buttons can have their own action/method or inherit from parent form
|
|
22
|
+
const isButton = elem.hasAttribute('ajax-button');
|
|
23
|
+
const parentForm = elem.closest('form');
|
|
24
|
+
|
|
25
|
+
// Get URL - prioritize element's own action attribute
|
|
26
|
+
let url = elem.getAttribute('action');
|
|
27
|
+
if (!url && parentForm) {
|
|
28
|
+
url = parentForm.getAttribute('action');
|
|
29
|
+
}
|
|
30
|
+
if (!url) {
|
|
31
|
+
url = window.location.href;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get method - prioritize element's own method attribute
|
|
35
|
+
let method = elem.getAttribute('method');
|
|
36
|
+
if (!method && parentForm) {
|
|
37
|
+
method = parentForm.getAttribute('method');
|
|
38
|
+
}
|
|
39
|
+
method = (method || 'POST').toUpperCase();
|
|
40
|
+
|
|
41
|
+
// Get data - for buttons, only use form data if button is inside a form
|
|
42
|
+
let data = {};
|
|
43
|
+
if (isButton && parentForm) {
|
|
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
|
|
51
|
+
|
|
52
|
+
fetch(url, {
|
|
53
|
+
method: method,
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json'
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify(data)
|
|
58
|
+
})
|
|
59
|
+
.then(res => res.json().then(resData => {
|
|
60
|
+
return { ...resData, ok: res.ok, msgType: res.ok ? "success" : "error" }
|
|
61
|
+
}))
|
|
62
|
+
.then((res) => {
|
|
63
|
+
handleResponse(elem, res);
|
|
64
|
+
})
|
|
65
|
+
.catch(error => console.error('Error:', error));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleResponse(elem, res) {
|
|
70
|
+
if (res.ok && elem.matches('form')) elem.reset();
|
|
71
|
+
const onResponseCode = elem.getAttribute('onresponse');
|
|
72
|
+
if (onResponseCode) {
|
|
73
|
+
new Function('res', onResponseCode).call(elem, res);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function callSubmitEvents(elem) {
|
|
78
|
+
const elemsWithSubmitEvents = [elem, ...elem.querySelectorAll("[onbeforesubmit]")];
|
|
79
|
+
for (const elemWithSubmitEvents of elemsWithSubmitEvents) {
|
|
80
|
+
if (elemWithSubmitEvents.hasAttribute('onbeforesubmit')) {
|
|
81
|
+
const result = new Function('return ' + elemWithSubmitEvents.getAttribute('onbeforesubmit')).call(elemWithSubmitEvents);
|
|
82
|
+
if (result instanceof Promise) {
|
|
83
|
+
await result;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function init () {
|
|
90
|
+
document.addEventListener('submit', handleFormSubmit);
|
|
91
|
+
document.addEventListener('click', handleButtonClick);
|
|
92
|
+
}
|
|
93
|
+
export { init };
|
|
94
|
+
export default init;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function init () {
|
|
2
|
+
document.addEventListener('input', event => {
|
|
3
|
+
const target = event.target;
|
|
4
|
+
if (target.matches('textarea[autosize]')) {
|
|
5
|
+
target.style.overflowY = 'hidden';
|
|
6
|
+
target.style.height = 'auto';
|
|
7
|
+
target.style.height = target.scrollHeight + 'px';
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
document.querySelectorAll('textarea[autosize]').forEach(textarea => {
|
|
12
|
+
textarea.style.overflowY = 'hidden';
|
|
13
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export { init };
|
|
17
|
+
export default init;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import nearest from "../utilities/nearest.js";
|
|
2
|
+
import pipe from "../utilities/pipe.js";
|
|
3
|
+
|
|
4
|
+
function init () {
|
|
5
|
+
|
|
6
|
+
// Bail if already initialized
|
|
7
|
+
if (HTMLElement.prototype.hasOwnProperty('nearest')) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// elem.nearest.project returns nearest element with a "project" attribute
|
|
12
|
+
Object.defineProperty(HTMLElement.prototype, 'nearest', {
|
|
13
|
+
configurable: true,
|
|
14
|
+
get: function() {
|
|
15
|
+
let element = this;
|
|
16
|
+
|
|
17
|
+
const handler = {
|
|
18
|
+
get(target, prop) {
|
|
19
|
+
return nearest(element, `[${prop}], .${prop}`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return new Proxy({}, handler);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
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
|
|
29
|
+
Object.defineProperty(HTMLElement.prototype, 'val', {
|
|
30
|
+
configurable: true,
|
|
31
|
+
get: function() {
|
|
32
|
+
let element = this;
|
|
33
|
+
|
|
34
|
+
const handler = {
|
|
35
|
+
get(target, prop) {
|
|
36
|
+
return nearest(element, `[${prop}], .${prop}`, elem => elem.getAttribute(prop));
|
|
37
|
+
},
|
|
38
|
+
set(target, prop, value) {
|
|
39
|
+
const foundElem = nearest(element, `[${prop}], .${prop}`);
|
|
40
|
+
|
|
41
|
+
if (foundElem) {
|
|
42
|
+
foundElem.setAttribute(prop, value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return new Proxy({}, handler);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// elem.text.project returns the innerText of the nearest element with the "project" attribute
|
|
54
|
+
// elem.text.project = "hello world" sets the innerText of the nearest element with the "project" attribute
|
|
55
|
+
Object.defineProperty(HTMLElement.prototype, 'text', {
|
|
56
|
+
configurable: true,
|
|
57
|
+
get: function() {
|
|
58
|
+
let element = this;
|
|
59
|
+
|
|
60
|
+
const handler = {
|
|
61
|
+
get(target, prop) {
|
|
62
|
+
return nearest(element, `[${prop}], .${prop}`, elem => elem.innerText);
|
|
63
|
+
},
|
|
64
|
+
set(target, prop, value) {
|
|
65
|
+
const foundElem = nearest(element, `[${prop}], .${prop}`);
|
|
66
|
+
|
|
67
|
+
if (foundElem) {
|
|
68
|
+
foundElem.innerText = value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return new Proxy({}, handler);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// elem.exec.sync_out() executes the code in the nearest "sync_out" attribute, using elem as the `this`
|
|
80
|
+
Object.defineProperty(HTMLElement.prototype, 'exec', {
|
|
81
|
+
configurable: true,
|
|
82
|
+
get: function() {
|
|
83
|
+
let element = this;
|
|
84
|
+
|
|
85
|
+
const handler = {
|
|
86
|
+
get(target, prop) {
|
|
87
|
+
return function() {
|
|
88
|
+
const foundElem = nearest(element, `[${prop}], .${prop}`);
|
|
89
|
+
if (foundElem) {
|
|
90
|
+
const code = foundElem.getAttribute(prop);
|
|
91
|
+
if (code) {
|
|
92
|
+
return new Function(code).call(foundElem);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return new Proxy({}, handler);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* CYCLE METHODS
|
|
105
|
+
*
|
|
106
|
+
* Element.prototype.cycle - Cycles through elements with a specific attribute and replaces the current element with the next one
|
|
107
|
+
* Usage: element.cycle(1, 'project') - Replaces element with the next element that has 'project' attribute
|
|
108
|
+
*
|
|
109
|
+
* Element.prototype.cycleAttr - Cycles through possible values of a specific attribute on matching elements
|
|
110
|
+
* Usage:
|
|
111
|
+
* element.cycleAttr(1, 'project_type') - Sets project_type to the next value found on elements with project_type
|
|
112
|
+
* element.cycleAttr(1, 'project_type', 'option:project_type') - Sets project_type based on values from option:project_type
|
|
113
|
+
*
|
|
114
|
+
* Notes:
|
|
115
|
+
* - Both methods support forward/backward cycling with positive/negative order parameter
|
|
116
|
+
* - For attributes containing special characters like colons (e.g., 'option:project_type'),
|
|
117
|
+
* the colon is automatically escaped for the CSS selector
|
|
118
|
+
* - cycle() replaces the entire element with the next one
|
|
119
|
+
* - cycleAttr() only changes the attribute value on the current element
|
|
120
|
+
*/
|
|
121
|
+
Element.prototype.cycle = function(order = 1, attr) {
|
|
122
|
+
const escapedAttr = attr.replace(/:/g, '\\:');
|
|
123
|
+
const next = pipe(
|
|
124
|
+
// Get all elements
|
|
125
|
+
() => document.querySelectorAll(`[${escapedAttr}]`),
|
|
126
|
+
|
|
127
|
+
// Convert to array of {element, value} objects
|
|
128
|
+
els => Array.from(els).map(el => ({
|
|
129
|
+
element: el,
|
|
130
|
+
value: el.textContent.trim()
|
|
131
|
+
})),
|
|
132
|
+
|
|
133
|
+
// Get unique by value
|
|
134
|
+
els => [...new Map(els.map(el => [el.value, el])).values()],
|
|
135
|
+
|
|
136
|
+
// Sort by value
|
|
137
|
+
els => els.sort((a, b) => a.value.localeCompare(b.value)),
|
|
138
|
+
|
|
139
|
+
// Find next element object
|
|
140
|
+
els => els[(els.findIndex(el => el.value === this.textContent.trim()) + order + els.length) % els.length],
|
|
141
|
+
|
|
142
|
+
// Return just the element
|
|
143
|
+
obj => obj.element
|
|
144
|
+
)();
|
|
145
|
+
|
|
146
|
+
if (next) this.replaceWith(next.cloneNode(true));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
Element.prototype.cycleAttr = function(order = 1, setAttr, lookupAttr) {
|
|
150
|
+
// If lookupAttr is not provided, use setAttr for both
|
|
151
|
+
lookupAttr = lookupAttr || setAttr;
|
|
152
|
+
|
|
153
|
+
const escapedLookupAttr = lookupAttr.replace(/:/g, '\\:');
|
|
154
|
+
const next = pipe(
|
|
155
|
+
// Get all elements that match the lookup attribute
|
|
156
|
+
() => document.querySelectorAll(`[${escapedLookupAttr}]`),
|
|
157
|
+
|
|
158
|
+
// Get unique attribute values
|
|
159
|
+
els => [...new Set(Array.from(els).map(el =>
|
|
160
|
+
el.getAttribute(lookupAttr)
|
|
161
|
+
))],
|
|
162
|
+
|
|
163
|
+
// Sort them
|
|
164
|
+
vals => vals.sort(),
|
|
165
|
+
|
|
166
|
+
// Get next value based on current setAttr value
|
|
167
|
+
vals => vals[(vals.indexOf(this.getAttribute(setAttr)) + order + vals.length) % vals.length]
|
|
168
|
+
)();
|
|
169
|
+
|
|
170
|
+
if (next) this.setAttribute(setAttr, next);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
export { init };
|
|
175
|
+
export default init;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Events module - combines all event attribute handlers
|
|
2
|
+
import { init as initOnclickaway } from './onclickaway.js';
|
|
3
|
+
import { init as initOnclone } from './onclone.js';
|
|
4
|
+
import { init as initOnpagemutation } from './onpagemutation.js';
|
|
5
|
+
import { init as initOnrender } from './onrender.js';
|
|
6
|
+
|
|
7
|
+
function init() {
|
|
8
|
+
initOnclickaway();
|
|
9
|
+
initOnclone();
|
|
10
|
+
initOnpagemutation();
|
|
11
|
+
initOnrender();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { init };
|
|
15
|
+
export default init;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Input helpers - combines prevent-enter and autosize
|
|
2
|
+
import { init as initPrevent } from './preventEnter.js';
|
|
3
|
+
import { init as initAutosize } from './autosize.js';
|
|
4
|
+
|
|
5
|
+
function init() {
|
|
6
|
+
initPrevent();
|
|
7
|
+
initAutosize();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { init };
|
|
11
|
+
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,35 @@
|
|
|
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
|
+
// Process only the top-level cloned element
|
|
17
|
+
processOnclone(clonedNode);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return clonedNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function processOnclone(element) {
|
|
24
|
+
const oncloneCode = element.getAttribute('onclone');
|
|
25
|
+
if (oncloneCode) {
|
|
26
|
+
try {
|
|
27
|
+
new Function(oncloneCode).call(element);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Error executing onclone:', error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export { init };
|
|
35
|
+
export default init;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Mutation from "../utilities/mutation.js";
|
|
2
|
+
|
|
3
|
+
function init() {
|
|
4
|
+
const executePageMutation = async element => {
|
|
5
|
+
try {
|
|
6
|
+
const code = element.getAttribute('onpagemutation');
|
|
7
|
+
const asyncFn = new Function(`return (async function() { ${code} })`)();
|
|
8
|
+
await asyncFn.call(element);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error('Error in onpagemutation execution:', error);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
Mutation.onAnyChange({
|
|
15
|
+
debounce: 200,
|
|
16
|
+
omitChangeDetails: true
|
|
17
|
+
}, () => document.querySelectorAll('[onpagemutation]').forEach(executePageMutation));
|
|
18
|
+
}
|
|
19
|
+
export { init };
|
|
20
|
+
export default init;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Mutation from "../utilities/mutation.js";
|
|
2
|
+
import onLoad from "../dom-utilities/onLoad.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
|
+
}, (changes) => {
|
|
25
|
+
changes.forEach(({ element }) => executeRender(element));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { init };
|
|
30
|
+
export default init;
|