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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/README.template.md +276 -0
  4. package/communication/behaviorCollector.js +230 -0
  5. package/communication/sendMessage.js +48 -0
  6. package/communication/uploadFile.js +348 -0
  7. package/core/adminContenteditable.js +36 -0
  8. package/core/adminInputs.js +58 -0
  9. package/core/adminOnClick.js +31 -0
  10. package/core/adminResources.js +33 -0
  11. package/core/adminSystem.js +15 -0
  12. package/core/editmode.js +8 -0
  13. package/core/editmodeSystem.js +18 -0
  14. package/core/enablePersistentFormInputValues.js +62 -0
  15. package/core/isAdminOfCurrentResource.js +13 -0
  16. package/core/optionVisibilityRuleGenerator.js +160 -0
  17. package/core/savePage.js +196 -0
  18. package/core/savePageCore.js +236 -0
  19. package/core/setPageTypeOnDocumentElement.js +23 -0
  20. package/custom-attributes/ajaxElements.js +94 -0
  21. package/custom-attributes/autosize.js +17 -0
  22. package/custom-attributes/domHelpers.js +175 -0
  23. package/custom-attributes/events.js +15 -0
  24. package/custom-attributes/inputHelpers.js +11 -0
  25. package/custom-attributes/onclickaway.js +27 -0
  26. package/custom-attributes/onclone.js +35 -0
  27. package/custom-attributes/onpagemutation.js +20 -0
  28. package/custom-attributes/onrender.js +30 -0
  29. package/custom-attributes/preventEnter.js +13 -0
  30. package/custom-attributes/sortable.js +76 -0
  31. package/dom-utilities/All.js +412 -0
  32. package/dom-utilities/getDataFromForm.js +60 -0
  33. package/dom-utilities/insertStyleTag.js +28 -0
  34. package/dom-utilities/onDomReady.js +7 -0
  35. package/dom-utilities/onLoad.js +7 -0
  36. package/hyperclay.js +465 -0
  37. package/module-dependency-graph.json +612 -0
  38. package/package.json +95 -0
  39. package/string-utilities/copy-to-clipboard.js +35 -0
  40. package/string-utilities/emmet-html.js +54 -0
  41. package/string-utilities/query.js +1 -0
  42. package/string-utilities/slugify.js +21 -0
  43. package/ui/info.js +39 -0
  44. package/ui/prompts.js +179 -0
  45. package/ui/theModal.js +677 -0
  46. package/ui/toast.js +273 -0
  47. package/utilities/cookie.js +45 -0
  48. package/utilities/debounce.js +12 -0
  49. package/utilities/mutation.js +403 -0
  50. package/utilities/nearest.js +97 -0
  51. package/utilities/pipe.js +1 -0
  52. package/utilities/throttle.js +21 -0
  53. package/vendor/Sortable.js +3351 -0
  54. package/vendor/idiomorph.min.js +8 -0
  55. package/vendor/tailwind-base.css +1471 -0
  56. 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;