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,230 @@
1
+ const behaviorCollector = (() => {
2
+ let lastMouseMove = 0;
3
+ let lastScroll = 0;
4
+ const THROTTLE_MS = 50;
5
+
6
+ const data = {
7
+ mousePositions: [],
8
+ scrollEvents: [],
9
+ keyboardEvents: [],
10
+ startTime: Date.now(),
11
+ webdriver: navigator.webdriver,
12
+ interactions: {
13
+ clicks: [],
14
+ touches: [],
15
+ focusEvents: [],
16
+ blurEvents: [],
17
+ tabSwitches: [],
18
+ keysSummary: new Set()
19
+ },
20
+ navigation: {
21
+ scrollCount: 0,
22
+ maxScrollDepth: 0,
23
+ lastScrollPosition: 0,
24
+ scrollDirectionChanges: 0
25
+ },
26
+ mouseMetrics: {
27
+ totalDistance: 0,
28
+ lastPosition: null
29
+ },
30
+ timing: {
31
+ firstInteraction: null,
32
+ lastInteraction: null
33
+ }
34
+ };
35
+
36
+ function updateTiming() {
37
+ const now = Date.now();
38
+ if (!data.timing.firstInteraction) {
39
+ data.timing.firstInteraction = now;
40
+ }
41
+ data.timing.lastInteraction = now;
42
+ }
43
+
44
+ function setupListeners() {
45
+ document.addEventListener('mousemove', (e) => {
46
+ const now = Date.now();
47
+ if (now - lastMouseMove < THROTTLE_MS) return;
48
+ lastMouseMove = now;
49
+
50
+ const position = {
51
+ x: Math.round(e.clientX),
52
+ y: Math.round(e.clientY),
53
+ timestamp: now,
54
+ isTrusted: e.isTrusted
55
+ };
56
+ data.mousePositions.push(position);
57
+
58
+ if (data.mouseMetrics.lastPosition) {
59
+ const distance = Math.sqrt(
60
+ Math.pow(position.x - data.mouseMetrics.lastPosition.x, 2) +
61
+ Math.pow(position.y - data.mouseMetrics.lastPosition.y, 2)
62
+ );
63
+ data.mouseMetrics.totalDistance += distance;
64
+ }
65
+
66
+ data.mouseMetrics.lastPosition = position;
67
+ updateTiming();
68
+
69
+ if (data.mousePositions.length > 100) {
70
+ data.mousePositions.shift();
71
+ }
72
+ }, {
73
+ passive: true
74
+ });
75
+
76
+ window.addEventListener('scroll', (e) => {
77
+ const now = Date.now();
78
+ if (now - lastScroll < THROTTLE_MS) return;
79
+ lastScroll = now;
80
+
81
+ const position = Math.round(window.scrollY);
82
+ data.navigation.scrollCount++;
83
+
84
+ const scrollEvent = {
85
+ position: position,
86
+ timestamp: now,
87
+ direction: position > data.navigation.lastScrollPosition ? 'down' : 'up',
88
+ isTrusted: e.isTrusted
89
+ };
90
+
91
+ data.scrollEvents.push(scrollEvent);
92
+
93
+ if (position > data.navigation.maxScrollDepth) {
94
+ data.navigation.maxScrollDepth = position;
95
+ }
96
+
97
+ if (data.scrollEvents.length > 1) {
98
+ const lastEvent = data.scrollEvents[data.scrollEvents.length - 2];
99
+ if (lastEvent.direction !== scrollEvent.direction) {
100
+ data.navigation.scrollDirectionChanges++;
101
+ }
102
+ }
103
+
104
+ data.navigation.lastScrollPosition = position;
105
+ updateTiming();
106
+
107
+ if (data.scrollEvents.length > 50) {
108
+ data.scrollEvents.shift();
109
+ }
110
+ }, {
111
+ passive: true
112
+ });
113
+
114
+ // Capture blur events
115
+ document.addEventListener('blur', (e) => {
116
+ data.interactions.blurEvents.push({
117
+ target: e.target.tagName,
118
+ timestamp: Date.now(),
119
+ isTrusted: e.isTrusted
120
+ });
121
+
122
+ if (data.interactions.blurEvents.length > 20) {
123
+ data.interactions.blurEvents.shift();
124
+ }
125
+ }, {
126
+ capture: true
127
+ });
128
+
129
+ document.addEventListener('keydown', (e) => {
130
+ data.keyboardEvents.push({
131
+ key: e.key,
132
+ timestamp: Date.now(),
133
+ modifiers: {
134
+ ctrl: e.ctrlKey,
135
+ alt: e.altKey,
136
+ shift: e.shiftKey
137
+ },
138
+ isTrusted: e.isTrusted
139
+ });
140
+ data.interactions.keysSummary.add(e.key);
141
+ updateTiming();
142
+
143
+ if (data.keyboardEvents.length > 50) {
144
+ data.keyboardEvents.shift();
145
+ }
146
+ }, {
147
+ passive: true
148
+ });
149
+
150
+ document.addEventListener('click', (e) => {
151
+ data.interactions.clicks.push({
152
+ x: Math.round(e.clientX),
153
+ y: Math.round(e.clientY),
154
+ timestamp: Date.now(),
155
+ target: e.target.tagName,
156
+ isTrusted: e.isTrusted
157
+ });
158
+ updateTiming();
159
+
160
+ if (data.interactions.clicks.length > 20) {
161
+ data.interactions.clicks.shift();
162
+ }
163
+ }, {
164
+ passive: true
165
+ });
166
+
167
+ document.addEventListener('touchstart', (e) => {
168
+ data.interactions.touches.push({
169
+ touchCount: e.touches.length,
170
+ timestamp: Date.now(),
171
+ isTrusted: e.isTrusted
172
+ });
173
+ updateTiming();
174
+
175
+ if (data.interactions.touches.length > 20) {
176
+ data.interactions.touches.shift();
177
+ }
178
+ }, {
179
+ passive: true
180
+ });
181
+
182
+ document.addEventListener('focus', (e) => {
183
+ data.interactions.focusEvents.push({
184
+ target: e.target.tagName,
185
+ timestamp: Date.now(),
186
+ isTrusted: e.isTrusted
187
+ });
188
+ updateTiming();
189
+
190
+ if (data.interactions.focusEvents.length > 20) {
191
+ data.interactions.focusEvents.shift();
192
+ }
193
+ }, {
194
+ passive: true,
195
+ capture: true
196
+ });
197
+
198
+ document.addEventListener('visibilitychange', () => {
199
+ data.interactions.tabSwitches.push({
200
+ state: document.visibilityState,
201
+ timestamp: Date.now()
202
+ });
203
+
204
+ if (data.interactions.tabSwitches.length > 20) {
205
+ data.interactions.tabSwitches.shift();
206
+ }
207
+ }, {
208
+ passive: true
209
+ });
210
+ }
211
+
212
+ return {
213
+ init: setupListeners,
214
+ getData: () => ({
215
+ ...data,
216
+ timeSpent: Date.now() - data.startTime,
217
+ interactions: {
218
+ ...data.interactions,
219
+ keysSummary: Array.from(data.interactions.keysSummary)
220
+ }
221
+ })
222
+ };
223
+ })();
224
+
225
+ export default behaviorCollector;
226
+
227
+ // Auto-initialize - start collecting behavior data
228
+ export function init() {
229
+ behaviorCollector.init();
230
+ }
@@ -0,0 +1,48 @@
1
+ import behaviorCollector from "./behaviorCollector.js";
2
+ import getDataFromForm from "../dom-utilities/getDataFromForm.js";
3
+ import toast from "../ui/toast.js";
4
+
5
+ export default function sendMessage(eventOrObj, successMessage = "Successfully sent", callback) {
6
+ let form;
7
+ let data;
8
+
9
+ if (eventOrObj instanceof Event) {
10
+ eventOrObj.preventDefault();
11
+ form = eventOrObj.target.closest('form');
12
+
13
+ if (!form) {
14
+ toast('No form found for this element', 'error');
15
+ return Promise.reject('No form found');
16
+ }
17
+
18
+ data = getDataFromForm(form);
19
+ } else {
20
+ data = eventOrObj;
21
+ if (this?.closest) {
22
+ form = this.closest('form');
23
+ }
24
+ }
25
+
26
+ data.behaviorData = behaviorCollector.getData();
27
+
28
+ return fetch("/message", {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify(data)
32
+ })
33
+ .then(res => res.ok ? res.json() : Promise.reject(`Request failed: ${res.status}`))
34
+ .then(result => {
35
+ toast(successMessage);
36
+
37
+ if (form?.reset) {
38
+ form.reset();
39
+ }
40
+
41
+ if (callback) callback(result);
42
+ return result;
43
+ })
44
+ .catch(error => {
45
+ toast(`Failed to send message: ${error}`);
46
+ throw error;
47
+ });
48
+ }
@@ -0,0 +1,348 @@
1
+ import toast from "../ui/toast.js";
2
+ import debounce from "../utilities/debounce.js";
3
+ import copyToClipboard from "../string-utilities/copy-to-clipboard.js";
4
+
5
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
6
+
7
+ export function uploadFile(eventOrFile, callback = () => {}, extraData = {}) {
8
+ // handle event
9
+ if (eventOrFile instanceof Event) {
10
+ eventOrFile.preventDefault();
11
+ const fileInput = eventOrFile.target;
12
+ const file = fileInput.files[0];
13
+ return uploadFileFromObject(file, res => {
14
+ callback(res);
15
+ fileInput.value = ""; // Reset fileInput after upload
16
+ }, extraData);
17
+ }
18
+
19
+ // handle raw file object
20
+ if (eventOrFile instanceof File) {
21
+ return uploadFileFromObject(eventOrFile, callback, extraData);
22
+ }
23
+
24
+ return Promise.reject(new Error('uploadFile requires either an Event or File object'));
25
+ }
26
+
27
+ export function createFile(eventOrData) {
28
+ // handle file as event
29
+ if (eventOrData instanceof Event) {
30
+ eventOrData.preventDefault();
31
+ const formElem = eventOrData.target;
32
+ const fileNameInput = formElem.querySelector('[name="file_name"]');
33
+ const fileBodyInput = formElem.querySelector('[name="file_body"]');
34
+
35
+ const fileName = fileNameInput.value.trim();
36
+ const fileBody = fileBodyInput.value;
37
+
38
+ if (!isValidFileName(fileName)) {
39
+ toast('Invalid filename', 'error');
40
+ return Promise.reject(new Error('Invalid filename'));
41
+ }
42
+
43
+ return createFileFromData(fileName, fileBody, () => {
44
+ resetFormElements(formElem);
45
+ });
46
+ }
47
+
48
+ // handle file as base64 data
49
+ if (eventOrData && typeof eventOrData === 'object' && 'fileName' in eventOrData && 'fileBody' in eventOrData) {
50
+ return createFileFromData(eventOrData.fileName, eventOrData.fileBody);
51
+ }
52
+
53
+ // handle file as base64 data
54
+ if (arguments.length === 2) {
55
+ const [fileName, fileBody] = arguments;
56
+ return createFileFromData(fileName, fileBody);
57
+ }
58
+
59
+ return Promise.reject(new Error('createFile requires either an Event, {fileName, fileBody} object, or (fileName, fileBody) parameters'));
60
+ }
61
+
62
+ export function uploadFileBasic (eventOrFile, {
63
+ onProgress = (percent) => {},
64
+ onComplete = (url) => {},
65
+ onError = (error) => {}
66
+ } = {}) {
67
+ function handleUpload(file) {
68
+ if (file.size > MAX_FILE_SIZE) {
69
+ onError(new Error('File too large'));
70
+ return Promise.reject(new Error('File too large'));
71
+ }
72
+
73
+ return new Promise((resolve, reject) => {
74
+ const reader = new FileReader();
75
+
76
+ reader.onload = async function () {
77
+ try {
78
+ const fileBody = reader.result.split(",")[1];
79
+ const fileName = file.name;
80
+ const result = await uploadFileData(fileName, fileBody, (percentComplete, res) => {
81
+ if (percentComplete === -1) {
82
+ onError(new Error('Upload cancelled'));
83
+ } else if (percentComplete < 100) {
84
+ onProgress(percentComplete);
85
+ } else if (res) {
86
+ onComplete(res);
87
+ }
88
+ });
89
+ resolve(result);
90
+ } catch (error) {
91
+ onError(error);
92
+ reject(error);
93
+ }
94
+ };
95
+
96
+ reader.onerror = () => {
97
+ const error = new Error('Failed to read file');
98
+ onError(error);
99
+ reject(error);
100
+ };
101
+ reader.readAsDataURL(file);
102
+ });
103
+ }
104
+
105
+ if (eventOrFile instanceof Event) {
106
+ eventOrFile.preventDefault();
107
+ const file = eventOrFile.target.files[0];
108
+ return handleUpload(file);
109
+ }
110
+
111
+ if (eventOrFile instanceof File) {
112
+ return handleUpload(eventOrFile);
113
+ }
114
+
115
+ const error = new Error('uploadFile requires either an Event or File object');
116
+ onError(error);
117
+ return Promise.reject(error);
118
+ }
119
+
120
+ function uploadFileFromObject(file, onComplete = () => {}, extraData = {}) {
121
+ if (file.size > MAX_FILE_SIZE) {
122
+ toast(`Maximum file size: ${MAX_FILE_SIZE / 1024 / 1024}MB`, 'error');
123
+ return Promise.reject(new Error('File too large'));
124
+ }
125
+
126
+ return new Promise((resolve, reject) => {
127
+ const reader = new FileReader();
128
+
129
+ reader.onload = async function () {
130
+ try {
131
+ const fileBody = reader.result.split(",")[1];
132
+ const fileName = file.name;
133
+ const result = await uploadFileData(fileName, fileBody, function progressCallback(percentComplete, res) {
134
+ if (percentComplete === -1) {
135
+ toast('Upload cancelled', 'error');
136
+ } else if (percentComplete < 100) {
137
+ toast(`${percentComplete}% uploaded`);
138
+ } else if (res) {
139
+ const urls = res.urls ? res.urls.join("\n") : "";
140
+ if (urls) {
141
+ copyToClipboard(urls);
142
+ toast(res.msg, res.msgType);
143
+ onComplete(res);
144
+ } else {
145
+ toast(res.msg, res.msgType);
146
+ onComplete(res);
147
+ }
148
+ }
149
+ }, extraData);
150
+ resolve(result);
151
+ } catch (error) {
152
+ const {msg, msgType} = error?.response ? JSON.parse(error.response) : {msg: 'Upload failed', msgType: 'error'};
153
+ toast(msg, msgType);
154
+ reject(error);
155
+ }
156
+ };
157
+
158
+ reader.onerror = () => {
159
+ toast('Failed to read file', 'error');
160
+ reject(new Error('Failed to read file'));
161
+ };
162
+
163
+ reader.readAsDataURL(file);
164
+ });
165
+ }
166
+
167
+ function createFileFromData(fileName, fileBody, onComplete = () => {}) {
168
+ if (!isValidFileName(fileName)) {
169
+ return Promise.reject(new Error('Invalid filename'));
170
+ }
171
+
172
+ const { adjustedFileName, base64FileBody } = processFileContents(fileName, fileBody);
173
+ return uploadFileData(adjustedFileName, base64FileBody, function progressCallback(percentComplete, res) {
174
+ if (percentComplete === -1) {
175
+ toast('Upload cancelled', 'error');
176
+ } else if (percentComplete < 100) {
177
+ toast(`${percentComplete}% uploaded`);
178
+ } else {
179
+ const urls = res.urls.join("\n")
180
+ copyToClipboard(urls);
181
+ toast(res.msg, res.msgType);
182
+ onComplete(res);
183
+ }
184
+ });
185
+ }
186
+
187
+ function uploadFileData(fileName, fileBody, progressCallback, extraData = {}) {
188
+ const xhr = new XMLHttpRequest();
189
+ xhr.open("POST", "/upload", true);
190
+ xhr.setRequestHeader("Content-Type", "application/json");
191
+
192
+ let lastReportedProgress = 0;
193
+ const debouncedProgressCallback = debounce(function (event) {
194
+ if (event.lengthComputable) {
195
+ const percentComplete = Math.floor((event.loaded / event.total) * 100);
196
+ if (
197
+ (percentComplete >= 10 && percentComplete < 50 && lastReportedProgress < 10) ||
198
+ (percentComplete >= 50 && percentComplete < 80 && lastReportedProgress < 50) ||
199
+ (percentComplete >= 80 && percentComplete < 100 && lastReportedProgress < 80)
200
+ ) {
201
+ progressCallback(percentComplete);
202
+ lastReportedProgress = percentComplete;
203
+ }
204
+ }
205
+ }, 200);
206
+
207
+ xhr.upload.onprogress = debouncedProgressCallback;
208
+
209
+ const upload = new Promise((resolve, reject) => {
210
+ xhr.onreadystatechange = function () {
211
+ if (xhr.readyState === XMLHttpRequest.DONE) {
212
+ if (xhr.status === 200) {
213
+ const response = JSON.parse(xhr.responseText);
214
+ progressCallback(100, response);
215
+ resolve(response);
216
+ } else {
217
+ let errorMessage = 'Upload failed';
218
+ let errorResponse = {};
219
+ let msgType = 'error';
220
+
221
+ try {
222
+ errorResponse = JSON.parse(xhr.responseText);
223
+ errorMessage = errorResponse.msg || errorMessage;
224
+ msgType = errorResponse.msgType || 'error';
225
+ } catch (e) {
226
+ // If response isn't valid JSON, use default message
227
+ }
228
+
229
+ // Show toast with the error message
230
+ toast(errorMessage, msgType);
231
+
232
+ const error = new Error(errorMessage);
233
+ error.status = xhr.status;
234
+ error.response = xhr.responseText;
235
+ reject(error);
236
+ }
237
+ }
238
+ };
239
+
240
+ xhr.onerror = () => {
241
+ reject(new Error('Network error occurred'));
242
+ };
243
+
244
+ xhr.ontimeout = () => {
245
+ reject(new Error('Upload timed out'));
246
+ };
247
+
248
+ const data = JSON.stringify({
249
+ fileName,
250
+ fileBody,
251
+ ...extraData
252
+ });
253
+
254
+ xhr.send(data);
255
+ });
256
+
257
+ upload.abort = () => {
258
+ xhr.abort();
259
+ progressCallback(-1, { msg: 'Upload cancelled' });
260
+ };
261
+
262
+ return upload;
263
+ }
264
+
265
+ function processFileContents(fileName, fileBody) {
266
+ const contentTypeInfo = detectContentType(fileBody);
267
+
268
+ const adjustedFileName = adjustFileExtension(fileName, contentTypeInfo.extension);
269
+ const base64FileBody = base64EncodeUnicode(fileBody);
270
+
271
+ return { adjustedFileName, base64FileBody };
272
+ }
273
+
274
+ function isValidFileName(name) {
275
+ if (!name || typeof name !== 'string') return false;
276
+ const invalidChars = /[<>:"/\\|?*\x00-\x1F]/g;
277
+ return name.length > 0 && name.length <= 255 && !invalidChars.test(name);
278
+ }
279
+
280
+ function adjustFileExtension(fileName, extension) {
281
+ const dotIndex = fileName.lastIndexOf(".");
282
+ if (dotIndex !== -1) {
283
+ fileName = fileName.substring(0, dotIndex) + extension;
284
+ } else {
285
+ fileName += extension;
286
+ }
287
+ return fileName;
288
+ }
289
+
290
+ function base64EncodeUnicode(str) {
291
+ return btoa(unescape(encodeURIComponent(str)));
292
+ }
293
+
294
+ function resetFormElements(formElem) {
295
+ const inputs = formElem.querySelectorAll('input[name], textarea[name]');
296
+ inputs.forEach((input) => {
297
+ if (input.hasAttribute("data-default-value")) {
298
+ input.value = input.getAttribute("data-default-value");
299
+ } else {
300
+ input.value = "";
301
+ }
302
+ });
303
+ }
304
+
305
+ function detectContentType(content) {
306
+ const patterns = [
307
+ // HTML and SVG patterns
308
+ { type: "html", mime: "text/html", regex: /<html/i },
309
+ { type: "svg", mime: "image/svg+xml", regex: /<svg/i },
310
+ // CSS pattern moved before JSON
311
+ {
312
+ type: "css",
313
+ mime: "text/css",
314
+ regex: /^\s*(\/\*[\s\S]*?\*\/\s*)?(@[a-z\-]+\s+)?([.#]?[a-zA-Z][\w-]*\s*,?\s*)+\s*\{/im,
315
+ },
316
+ // JavaScript pattern
317
+ {
318
+ type: "js",
319
+ mime: "application/javascript",
320
+ regex: /^\s*(\/\*[\s\S]*?\*\/\s*)?(\/\/.*\s*)*(import|export|var|let|const|function|class)\s+/im,
321
+ },
322
+ // CSV pattern
323
+ { type: "csv", mime: "text/csv", regex: /^[^,\n]+(,[^,\n]+)+/ },
324
+ // Markdown pattern
325
+ {
326
+ type: "md",
327
+ mime: "text/markdown",
328
+ regex: /#\S+\s+\S+|```[\s\S]*```/
329
+ },
330
+ ];
331
+
332
+ for (const { type, mime, regex } of patterns) {
333
+ if (regex.test(content)) {
334
+ return { type, mime, extension: `.${type}` };
335
+ }
336
+ }
337
+
338
+ // Try to parse as JSON
339
+ try {
340
+ JSON.parse(content);
341
+ return { type: "json", mime: "application/json", extension: ".json" };
342
+ } catch (e) {
343
+ // Not JSON
344
+ }
345
+
346
+ // Default to plain text
347
+ return { type: "txt", mime: "text/plain", extension: ".txt" };
348
+ }
@@ -0,0 +1,36 @@
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 disableContentEditableBeforeSave () {
6
+ beforeSave(docElem => {
7
+ docElem.querySelectorAll('[edit-mode-contenteditable]').forEach(resource => {
8
+ const originalValue = resource.getAttribute("contenteditable");
9
+ resource.setAttribute("inert-contenteditable", originalValue);
10
+ resource.removeAttribute("contenteditable");
11
+ });
12
+ });
13
+ }
14
+
15
+ export function enableContentEditableForAdminOnPageLoad () {
16
+ if (!isEditMode) return;
17
+
18
+ onDomReady(() => {
19
+ document.querySelectorAll('[edit-mode-contenteditable]').forEach(resource => {
20
+ let originalValue = resource.getAttribute("inert-contenteditable");
21
+
22
+ if (!["false", "plaintext-only"].includes(originalValue)) {
23
+ originalValue = "true";
24
+ }
25
+
26
+ resource.setAttribute("contenteditable", originalValue);
27
+ resource.removeAttribute("inert-contenteditable");
28
+ });
29
+ });
30
+ }
31
+
32
+ // Auto-initialize
33
+ export function init() {
34
+ disableContentEditableBeforeSave();
35
+ enableContentEditableForAdminOnPageLoad();
36
+ }