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,58 @@
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 disableAdminInputsBeforeSave() {
6
+ beforeSave(docElem => {
7
+ docElem.querySelectorAll('[edit-mode-input]').forEach(input => {
8
+ if (supportsReadonly(input)) {
9
+ input.setAttribute('readonly', '');
10
+ } else {
11
+ input.setAttribute('disabled', '');
12
+ }
13
+ });
14
+ });
15
+ }
16
+
17
+ export function enableAdminInputsOnPageLoad() {
18
+ if (!isEditMode) return;
19
+
20
+ onDomReady(() => {
21
+ document.querySelectorAll('[edit-mode-input]').forEach(input => {
22
+ if (supportsReadonly(input)) {
23
+ input.removeAttribute('readonly');
24
+ } else {
25
+ input.removeAttribute('disabled');
26
+ }
27
+ });
28
+ });
29
+ }
30
+
31
+ // Input types that support the readonly attribute
32
+ const readonlyTypes = ['text', 'search', 'url', 'tel', 'email', 'password', 'date', 'month', 'week', 'time', 'datetime-local', 'number'];
33
+
34
+ function supportsReadonly(element) {
35
+ // Handle different element types
36
+ const tagName = element.tagName?.toUpperCase();
37
+
38
+ // TEXTAREA supports readonly
39
+ if (tagName === 'TEXTAREA') return true;
40
+
41
+ // SELECT, BUTTON, FIELDSET use disabled
42
+ if (tagName === 'SELECT' || tagName === 'BUTTON' || tagName === 'FIELDSET') return false;
43
+
44
+ // For INPUT elements, check the type
45
+ if (tagName === 'INPUT') {
46
+ const type = element.type || 'text';
47
+ return readonlyTypes.includes(type);
48
+ }
49
+
50
+ // Default to disabled for unknown elements
51
+ return false;
52
+ }
53
+
54
+ // Auto-initialize
55
+ export function init() {
56
+ disableAdminInputsBeforeSave();
57
+ enableAdminInputsOnPageLoad();
58
+ }
@@ -0,0 +1,31 @@
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
+ document.querySelectorAll('[edit-mode-onclick]').forEach(resource => {
20
+ const originalValue = resource.getAttribute("inert-onclick");
21
+ resource.setAttribute("onclick", originalValue);
22
+ resource.removeAttribute("inert-onclick");
23
+ });
24
+ });
25
+ }
26
+
27
+ // Auto-initialize
28
+ export function init() {
29
+ disableOnClickBeforeSave();
30
+ enableOnClickForAdminOnPageLoad();
31
+ }
@@ -0,0 +1,33 @@
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 disableAdminResourcesBeforeSave () {
6
+ beforeSave(docElem => {
7
+ docElem.querySelectorAll('[edit-mode-resource]:is(style, link, script)').forEach(resource => {
8
+ const currentType = resource.getAttribute('type') || 'text/javascript';
9
+ // Only add the inert/ prefix if it's not already there
10
+ if (!currentType.startsWith('inert/')) {
11
+ resource.setAttribute('type', `inert/${currentType}`);
12
+ }
13
+ });
14
+ });
15
+ }
16
+
17
+ export function enableAdminResourcesOnPageLoad () {
18
+ if (!isEditMode) return;
19
+
20
+ onDomReady(() => {
21
+ document.querySelectorAll('[edit-mode-resource]:is(style, link, script)[type^="inert/"]').forEach(resource => {
22
+ // works for js and css
23
+ resource.type = resource.type.replace(/inert\//g, '');
24
+ resource.replaceWith(resource.cloneNode(true));
25
+ });
26
+ });
27
+ }
28
+
29
+ // Auto-initialize
30
+ export function init() {
31
+ disableAdminResourcesBeforeSave();
32
+ enableAdminResourcesOnPageLoad();
33
+ }
@@ -0,0 +1,15 @@
1
+ // Admin system - combines all admin features
2
+ import { init as initContenteditable } from './adminContenteditable.js';
3
+ import { init as initInputs } from './adminInputs.js';
4
+ import { init as initOnClick } from './adminOnClick.js';
5
+ import { init as initResources } from './adminResources.js';
6
+
7
+ function init() {
8
+ initContenteditable();
9
+ initInputs();
10
+ initOnClick();
11
+ initResources();
12
+ }
13
+
14
+ export { init };
15
+ export default init;
@@ -0,0 +1,8 @@
1
+ import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
2
+
3
+ export function toggleEditMode() {
4
+ const url = new URL(window.location.href);
5
+ const newMode = isEditMode ? "false" : "true";
6
+ url.searchParams.set('editmode', newMode);
7
+ window.location.href = url.toString();
8
+ }
@@ -0,0 +1,18 @@
1
+ // Edit mode system - combines editmode toggle with page type setting
2
+ import { toggleEditMode } from './editmode.js';
3
+ import { init as initPageType } from './setPageTypeOnDocumentElement.js';
4
+
5
+ function init() {
6
+ initPageType();
7
+ }
8
+
9
+ function exportToWindow() {
10
+ if (!window.hyperclay) {
11
+ window.hyperclay = {};
12
+ }
13
+
14
+ window.hyperclay.toggleEditMode = toggleEditMode;
15
+ }
16
+
17
+ export { init, exportToWindow };
18
+ export default { init, exportToWindow };
@@ -0,0 +1,62 @@
1
+ import { beforeSave } from './savePage.js';
2
+
3
+ // <input type="checkbox" persist>
4
+ export default function enablePersistentFormInputValues(filterBySelector = "[persist]") {
5
+ const selector = `input${filterBySelector}:not([type="password"]):not([type="hidden"]):not([type="file"]), textarea${filterBySelector}, select${filterBySelector}`;
6
+
7
+ document.addEventListener('input', (event) => {
8
+ const elem = event.target;
9
+ if (elem.matches(selector) && !(elem.type === 'checkbox' || elem.type === 'radio')) {
10
+ if (elem.tagName.toLowerCase() === 'textarea') {
11
+ // Store in value attribute instead of textContent to preserve cursor position
12
+ elem.setAttribute('value', elem.value);
13
+ } else {
14
+ elem.setAttribute('value', elem.value);
15
+ }
16
+ }
17
+ });
18
+
19
+ document.addEventListener('change', (event) => {
20
+ const elem = event.target;
21
+ if (elem.matches(selector)) {
22
+ // Handle checkboxes and radios
23
+ if (elem.type === 'checkbox' || elem.type === 'radio') {
24
+ if (elem.checked) {
25
+ elem.setAttribute('checked', '');
26
+ } else {
27
+ elem.removeAttribute('checked');
28
+ }
29
+ }
30
+ // Handle select elements
31
+ else if (elem.tagName.toLowerCase() === 'select') {
32
+ // Remove selected from all options
33
+ const options = elem.querySelectorAll('option');
34
+ options.forEach(option => option.removeAttribute('selected'));
35
+
36
+ // Add selected to currently selected option(s)
37
+ if (elem.multiple) {
38
+ const selectedOptions = elem.selectedOptions;
39
+ Array.from(selectedOptions).forEach(option => {
40
+ option.setAttribute('selected', '');
41
+ });
42
+ } else if (elem.selectedIndex >= 0) {
43
+ options[elem.selectedIndex].setAttribute('selected', '');
44
+ }
45
+ }
46
+ }
47
+ });
48
+
49
+ // Before save, transfer value attribute to textContent for textareas
50
+ beforeSave((doc) => {
51
+ const textareas = doc.querySelectorAll('textarea[value]');
52
+ textareas.forEach(textarea => {
53
+ textarea.textContent = textarea.getAttribute('value');
54
+ textarea.removeAttribute('value');
55
+ });
56
+ });
57
+ }
58
+
59
+ // Auto-initialize with default selector
60
+ export function init() {
61
+ enablePersistentFormInputValues("[persist]");
62
+ }
@@ -0,0 +1,13 @@
1
+ import cookie from "../utilities/cookie.js";
2
+ import query from "../string-utilities/query.js";
3
+
4
+ const isEditMode = query.editmode
5
+ ? query.editmode === "true" // takes precedence over cookie
6
+ : Boolean(cookie.get("isAdminOfCurrentResource"));
7
+
8
+ const isOwner = Boolean(cookie.get("isAdminOfCurrentResource"));
9
+
10
+ export {
11
+ isEditMode,
12
+ isOwner
13
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ *
3
+ * Automatically show/hide elements with "option" attributes based on ancestors' attributes.
4
+ *
5
+ * # Usage:
6
+ * optionVisibilityRuleGenerator.debug = true;
7
+ * optionVisibilityRuleGenerator.start();
8
+ *
9
+ * # HTML Example:
10
+ * <div editmode="true"> <!-- Parent element with matching attribute -->
11
+ * <div option:editmode="true"></div> <!-- This will be visible -->
12
+ * <div option:editmode="false"></div> <!-- This will be hidden -->
13
+ * </div>
14
+ *
15
+ * Elements with `option:` attributes will be:
16
+ * - Visible if any ancestor has matching attribute
17
+ * - Hidden if no ancestor has matching attribute
18
+ *
19
+ */
20
+ import Mutation from "../utilities/mutation.js";
21
+
22
+ const optionVisibilityRuleGenerator = {
23
+ debug: false,
24
+ styleElement: null,
25
+
26
+ HIDDEN_STYLES: `
27
+ visibility: hidden;
28
+ pointer-events: none;
29
+ width: 0;
30
+ height: 0;
31
+ overflow: hidden;
32
+ `,
33
+ VISIBLE_STYLES: `
34
+ visibility: visible;
35
+ pointer-events: auto;
36
+ width: auto;
37
+ height: auto;
38
+ overflow: visible;
39
+ `,
40
+
41
+ STYLE_CLASS: 'option-visibility-styles',
42
+
43
+ log(message, ...args) {
44
+ if (this.debug) {
45
+ console.log(`[OptionVisibilityRuleGenerator] ${message}`, ...args);
46
+ }
47
+ },
48
+
49
+ findOptionAttributes() {
50
+ const html = document.documentElement.outerHTML;
51
+ const optionAttributes = new Set(); // Using Set for unique combinations
52
+ const optionRegex = /option:([^\s"']+)=["']([^"']+)["']/g; // regex: "option:" + (anything but space and quote) + equal + quote + (anything but quote) + quote
53
+
54
+ let match;
55
+ while ((match = optionRegex.exec(html)) !== null) {
56
+ // Create a unique key for each name-value pair
57
+ const key = JSON.stringify({name: match[1], value: match[2]});
58
+ optionAttributes.add(key);
59
+ }
60
+
61
+ // Convert back to objects
62
+ return Array.from(optionAttributes).map(key => JSON.parse(key));
63
+ },
64
+
65
+ minifyCSS(css) {
66
+ return css
67
+ .replace(/\s+/g, ' ')
68
+ .replace(/{\s+/g, '{')
69
+ .replace(/\s+}/g, '}')
70
+ .replace(/;\s+/g, ';')
71
+ .replace(/:\s+/g, ':')
72
+ .trim();
73
+ },
74
+
75
+ generateCSSRules(optionAttributes) {
76
+ const rules = [];
77
+
78
+ optionAttributes.forEach(({name, value}) => {
79
+ const escapedValue = value.replace(/["\\]/g, '\\$&');
80
+
81
+ rules.push(`
82
+ [option\\:${name}="${escapedValue}"] {
83
+ ${this.HIDDEN_STYLES}
84
+ }
85
+ `);
86
+
87
+ rules.push(`
88
+ [${name}="${escapedValue}"] [option\\:${name}="${escapedValue}"] {
89
+ ${this.VISIBLE_STYLES}
90
+ }
91
+ `);
92
+ });
93
+
94
+ return this.minifyCSS(rules.join('\n'));
95
+ },
96
+
97
+ generateRules() {
98
+ try {
99
+ this.log('Starting rule generation');
100
+
101
+ const optionAttributes = this.findOptionAttributes();
102
+ this.log('Found option attributes:', optionAttributes);
103
+
104
+ // Early return if no option attributes found
105
+ if (optionAttributes.length === 0) {
106
+ this.log('No option attributes found, skipping style creation');
107
+ return;
108
+ }
109
+
110
+ const cssRules = this.generateCSSRules(optionAttributes);
111
+ this.log('Generated CSS rules:', cssRules);
112
+
113
+ // Check if we already have these exact rules
114
+ const existingStyleElement = document.head.querySelector(`.${this.STYLE_CLASS}`);
115
+ if (existingStyleElement && existingStyleElement.textContent.trim() === cssRules) {
116
+ this.log('Rules unchanged, skipping update');
117
+ return;
118
+ }
119
+
120
+ // Create new style element
121
+ const newStyleElement = document.createElement('style');
122
+ newStyleElement.className = this.STYLE_CLASS;
123
+ newStyleElement.textContent = cssRules;
124
+ document.head.appendChild(newStyleElement);
125
+
126
+ // Remove all previous style elements
127
+ document.head
128
+ .querySelectorAll(`.${this.STYLE_CLASS}`)
129
+ .forEach(el => {
130
+ if (el !== newStyleElement) {
131
+ el.remove();
132
+ }
133
+ });
134
+
135
+ this.styleElement = newStyleElement;
136
+
137
+ this.log('Rule generation complete');
138
+ } catch (error) {
139
+ console.error('Error generating visibility rules:', error);
140
+ }
141
+ },
142
+
143
+ start() {
144
+ Mutation.onAnyChange({
145
+ selectorFilter: el => [...el.attributes].some(attr => attr.name.startsWith('option:')),
146
+ debounce: 200
147
+ }, () => {
148
+ this.generateRules();
149
+ });
150
+ this.generateRules();
151
+ this.log('Started observing DOM mutations');
152
+ },
153
+ };
154
+
155
+ export default optionVisibilityRuleGenerator;
156
+
157
+ // Auto-initialize
158
+ export function init() {
159
+ optionVisibilityRuleGenerator.start();
160
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Full save system for Hyperclay
3
+ *
4
+ * This includes all the conveniences: change detection, toast notifications,
5
+ * auto-save, keyboard shortcuts, and more.
6
+ *
7
+ * Built on top of savePageCore.js
8
+ */
9
+
10
+ import toast from "../ui/toast.js";
11
+ import throttle from "../utilities/throttle.js";
12
+ import Mutation from "../utilities/mutation.js";
13
+ import { isEditMode, isOwner } from "./isAdminOfCurrentResource.js";
14
+ import {
15
+ savePage as savePageCore,
16
+ getPageContents,
17
+ replacePageWith as replacePageWithCore
18
+ } from "./savePageCore.js";
19
+
20
+ // Re-export beforeSave from core for backward compatibility
21
+ export { beforeSave } from "./savePageCore.js";
22
+
23
+ let unsavedChanges = false;
24
+ let lastSavedContents = '';
25
+ let baselineContents = ''; // State after initial setup, used to prevent autosave from setup mutations
26
+
27
+ // Initialize lastSavedContents on page load to match what's on disk
28
+ // This prevents unnecessary save attempts when content hasn't changed
29
+ document.addEventListener('DOMContentLoaded', () => {
30
+ if (isEditMode) {
31
+ // Capture initial state immediately for comparison
32
+ lastSavedContents = getPageContents();
33
+
34
+ // Also capture baseline after setup for autosave detection
35
+ setTimeout(() => {
36
+ baselineContents = getPageContents();
37
+ }, 1500);
38
+ }
39
+ });
40
+
41
+ /**
42
+ * Save the current page with change detection and toast notifications
43
+ *
44
+ * @param {Function} callback - Optional callback for custom handling
45
+ */
46
+ export function savePage(callback = () => {}) {
47
+ if (!isEditMode) {
48
+ return;
49
+ }
50
+
51
+ const currentContents = getPageContents();
52
+
53
+ // Track whether there are unsaved changes
54
+ unsavedChanges = (currentContents !== lastSavedContents);
55
+
56
+ // Skip if content hasn't changed
57
+ if (!unsavedChanges) {
58
+ return;
59
+ }
60
+
61
+ savePageCore(({msg, msgType}) => {
62
+ // Update tracking on success
63
+ if (msgType !== 'error') {
64
+ lastSavedContents = currentContents;
65
+ unsavedChanges = false;
66
+ }
67
+
68
+ // Call user callback if provided
69
+ if (typeof callback === 'function' && msg) {
70
+ callback({msg, msgType});
71
+ }
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Throttled version of savePage for auto-save
77
+ */
78
+ const throttledSave = throttle(savePage, 1200);
79
+
80
+ /**
81
+ * Save the page with throttling, for use with auto-save
82
+ * Checks both baseline and last saved content to prevent saves from initial setup
83
+ *
84
+ * @param {Function} callback - Optional callback
85
+ */
86
+ export function savePageThrottled(callback = () => {}) {
87
+ if (!isEditMode) return;
88
+
89
+ const currentContents = getPageContents();
90
+ // For autosave: check both that content changed from baseline AND from last save
91
+ // This prevents saves from initial setup mutations
92
+ if (currentContents !== baselineContents && currentContents !== lastSavedContents) {
93
+ unsavedChanges = true;
94
+ throttledSave(callback);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Fetch HTML from a URL and save it, then reload
100
+ * Shows toast notifications
101
+ *
102
+ * @param {string} url - URL to fetch from
103
+ */
104
+ export function replacePageWith(url) {
105
+ if (!isEditMode) {
106
+ return;
107
+ }
108
+
109
+ replacePageWithCore(url, (err, data) => {
110
+ if (err) {
111
+ // Show error toast if save failed
112
+ toast(err.message || "Failed to save template", "error");
113
+ } else {
114
+ // Only reload if save was successful
115
+ window.location.reload();
116
+ }
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Initialize keyboard shortcut for save (CMD/CTRL+S)
122
+ */
123
+ export function initSaveKeyboardShortcut() {
124
+ document.addEventListener("keydown", function(event) {
125
+ let isMac = window.navigator.platform.match("Mac");
126
+ let metaKeyPressed = isMac ? event.metaKey : event.ctrlKey;
127
+ if (metaKeyPressed && event.keyCode == 83) {
128
+ event.preventDefault();
129
+ savePage(({msg, msgType} = {}) => {
130
+ if (msg) toast(msg, msgType);
131
+ });
132
+ }
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Initialize save button handler
138
+ * Looks for elements with [trigger-save] attribute
139
+ */
140
+ export function initHyperclaySaveButton() {
141
+ document.addEventListener("click", event => {
142
+ if (event.target.closest("[trigger-save]")) {
143
+ savePage(({msg, msgType} = {}) => {
144
+ if (msg) toast(msg, msgType);
145
+ });
146
+ }
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Initialize auto-save on DOM changes
152
+ * Uses debounced mutation observer
153
+ */
154
+ export function initSavePageOnChange() {
155
+ Mutation.onAnyChange({
156
+ debounce: 3333,
157
+ omitChangeDetails: true
158
+ }, () => {
159
+ savePageThrottled(({msg, msgType} = {}) => {
160
+ if (msg) toast(msg, msgType);
161
+ });
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Warn before leaving page with unsaved changes
167
+ */
168
+ window.addEventListener('beforeunload', (event) => {
169
+ if (unsavedChanges && isOwner) {
170
+ event.preventDefault();
171
+ event.returnValue = '';
172
+ }
173
+ });
174
+
175
+ /**
176
+ * Initialize the full save system
177
+ */
178
+ export function init() {
179
+ if (!isEditMode) return;
180
+
181
+ initSaveKeyboardShortcut();
182
+ initHyperclaySaveButton();
183
+ initSavePageOnChange();
184
+ }
185
+
186
+ /**
187
+ * Export save functions to window.hyperclay
188
+ */
189
+ export function exportToWindow() {
190
+ if (!window.hyperclay) {
191
+ window.hyperclay = {};
192
+ }
193
+
194
+ window.hyperclay.savePage = savePage;
195
+ window.hyperclay.replacePageWith = replacePageWith;
196
+ }