# HyperclayJS > HyperclayJS is a modular JavaScript library for building malleable HTML files on [Hyperclay](https://hyperclay.com) — self-contained HTML files that use the DOM as their database, can modify themselves, and save changes back to the server. ## How It Works Hyperclay apps use HTML as both the front end and the database. When the page changes, HyperclayJS grabs the DOM, strips admin-only controls, and POSTs the clean HTML to the save endpoint. Anonymous visitors see a static, read-only page. When the owner loads the page, edit controls are restored. This is the **save lifecycle**: change → strip admin UI → save → restore admin UI on load. ## Quick Start ```html ``` One script tag gives you: auto-save on DOM changes, edit/view mode toggling, form persistence, DOM helpers, UI dialogs, and more. Start simple with jQuery or vanilla JS for DOM manipulation — HyperclayJS handles the save lifecycle automatically. ## Key Concepts **Edit Mode vs View Mode** — Every page has an `editmode` attribute on `` set to `true` or `false`. Use `option:editmode="true"` on any element to show it only for editors. **Persist** — Add `persist` to any ``, ` ``` ## How It Works 1. On page load, sets initial height to match content 2. On input, recalculates height based on `scrollHeight` 3. Hides vertical overflow to prevent scrollbar flicker The textarea grows as you type and shrinks when content is deleted. ## Example ```html ``` ## Styling Tips ```css /* Set a minimum height */ textarea[autosize] { min-height: 80px; } /* Set a maximum height (enables scrolling beyond) */ textarea[autosize] { max-height: 300px; overflow-y: auto !important; } ``` --- # cacheBust Cache-bust an element's href or src attribute by adding or updating a version query parameter. Useful for reloading stylesheets or scripts after dynamic changes. ## Signature ```js cacheBust(element) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | element | HTMLElement | — | Element with href or src attribute to cache-bust | ## Returns `void` ## Example ```js // Cache-bust a stylesheet link const link = document.querySelector('link[rel="stylesheet"]'); cacheBust(link); // href="/styles.css" becomes "/styles.css?v=1702847291234" // Cache-bust an image const img = document.querySelector('img'); cacheBust(img); // src="/photo.jpg?v=123" becomes "/photo.jpg?v=1702847291234" // Use with onaftersave // // Note: for stylesheets where a flash matters, use [refetch-on-save] instead ``` --- # consent Display a confirmation modal to get user consent before an action. ## Signature ```js consent(promptText, yesCallback, extraContent) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | promptText | string | — | The question or prompt to display | | yesCallback | function | — | Called when user confirms | | extraContent | string | `''` | Additional HTML content to display | ## Returns `Promise` - Resolves when user confirms, rejects if user closes modal ## Example ```js // Basic usage with async/await try { await consent('Delete this item?'); deleteItem(); } catch (e) { // User cancelled } // With callback consent('Are you sure?', () => { performAction(); }); // With extra content consent('Publish changes?', null, '

This will be visible to all users.

'); ``` --- # cookie Utility object for reading and removing browser cookies. ## Signature ```js cookie.get(name) cookie.remove(name) ``` ## Methods | Method | Description | |--------|-------------| | `get(name)` | Get cookie value. Returns parsed JSON if valid, otherwise decoded string, or `null` if not found. | | `remove(name)` | Remove cookie from current path, host domain, and apex domain. | ## Example ```js // Get a cookie value const userId = cookie.get('userId'); // Get JSON cookie (auto-parsed) const preferences = cookie.get('userPrefs'); // { theme: 'dark', lang: 'en' } // Check if cookie exists if (cookie.get('authToken')) { showLoggedInUI(); } // Remove a cookie cookie.remove('sessionId'); // Clear authentication cookie.remove('authToken'); cookie.remove('userId'); ``` --- # copyToClipboard Copy text to the system clipboard. ## Signature ```js copyToClipboard(text) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | text | string | — | The text to copy to clipboard | ## Returns `void` ## Example ```js // Copy a URL copyToClipboard('https://example.com/share/123'); // Copy with user feedback copyToClipboard(embedCode); toast('Copied to clipboard!'); // Copy from an element const code = document.querySelector('pre').textContent; copyToClipboard(code); ``` --- # createFile Create and upload a file from text content. Automatically detects content type (HTML, CSS, JS, JSON, etc.) and adjusts the file extension. ## Signature ```js createFile(eventOrData) createFile(fileName, fileBody) ``` ## Parameters ### From event: | Name | Type | Description | |------|------|-------------| | eventOrData | Event | Form submit event with `file_name` and `file_body` inputs | ### From object: | Name | Type | Description | |------|------|-------------| | eventOrData | object | `{ fileName: string, fileBody: string }` | ### From arguments: | Name | Type | Description | |------|------|-------------| | fileName | string | Name for the file | | fileBody | string | Content of the file | ## Returns `Promise` - Resolves with server response containing URLs ## Validation Filenames are validated before upload: - Must be non-empty and ≤ 255 characters - Cannot contain: `< > : " / \ | ? *` or control characters Invalid filenames reject the promise with an error (and show a toast for form submissions). ## Example ```js // From form submission document.querySelector('#create-file-form').onsubmit = (e) => { createFile(e); }; // From object createFile({ fileName: 'styles.css', fileBody: '.container { max-width: 1200px; }' }); // From arguments createFile('config.json', JSON.stringify({ theme: 'dark' })); // Content type auto-detection: // - HTML content → .html // - CSS content → .css // - JavaScript → .js // - Valid JSON → .json // - Unknown → .txt ``` --- # debounce Delay function execution until after a period of inactivity. The timer resets each time the function is called. ## Signature ```js debounce(callback, delay) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | callback | function | — | The function to debounce | | delay | number | — | Wait time in milliseconds after last call | ## Returns `function` - Debounced version of the callback ## Example ```js // Debounce search input const searchInput = document.querySelector('#search'); const handleSearch = debounce((query) => { fetchSearchResults(query); }, 300); searchInput.addEventListener('input', (e) => { handleSearch(e.target.value); }); // Debounce window resize const handleResize = debounce(() => { recalculateLayout(); }, 250); window.addEventListener('resize', handleResize); // Auto-save after user stops typing const autoSave = debounce(() => { saveDraft(); }, 1000); ``` --- # dom-helpers Adds convenience methods to all HTML elements for finding and manipulating nearby elements. Built on top of the `nearest` utility. ## Properties | Property | Description | |----------|-------------| | `el.nearest.name` | Find nearest element with `[name]` attribute or `.name` class | | `el.val.name` | Get/set value of nearest element (form value or attribute) | | `el.text.name` | Get/set innerText of nearest element | | `el.exec.name()` | Execute code from `name` attribute on nearest element | ## Methods | Method | Description | |--------|-------------| | `el.cycle(order, attr)` | Replace element with next element having same attribute | | `el.cycleAttr(order, setAttr, lookupAttr?)` | Cycle through attribute values | ## Example ```js // Find nearest element with [project] or .project const projectEl = this.nearest.project; // Get/set values (smart: uses .value for form elements, attribute otherwise) const projectName = this.val.project; this.val.project = "New Name"; // Get/set innerText const label = this.text.title; this.text.title = "Updated Title"; // Execute code from an attribute // If
exists nearby, this runs savePage() this.exec.sync_out(); // Cycle through elements // Replaces current element with next element having [variant] attribute this.cycle(1, 'variant'); // forward this.cycle(-1, 'variant'); // backward // Cycle through attribute values // Sets theme to next value found on any [theme] element this.cycleAttr(1, 'theme'); // Cycle with different lookup attribute // Sets color based on values from [option:color] elements this.cycleAttr(1, 'color', 'option:color'); ``` ## How It Works All properties use `nearest()` to search outward from the element, checking siblings, children, and ancestors. The search pattern finds visually nearby elements first. ### val Behavior - For ``, ` ``` ## How It Works Browser form values exist only in JavaScript (`.value`), not in the DOM attributes. Without `persist`, saving or syncing the page would lose user-entered data. When a snapshot is captured (for save or live-sync), `persist` copies values from the live DOM to the snapshot clone: | Element | What's synced | |---------|--------------| | `` | `.value` → `value` attribute | | `` | `.checked` → `checked` attribute | | ` ``` --- # prevent-enter Prevent the Enter key from creating newlines in an element. ## Usage ```html ``` ## How It Works A global keydown listener intercepts Enter key presses. If the event target (or any ancestor) has `prevent-enter`, the default action is prevented. Works with: - `contenteditable` elements - `
Name Email
``` ## Use Cases - **Inline editable text**: Titles, labels, single-line fields - **Form fields**: Where Enter should submit instead of add newlines - **Chat inputs**: Combined with custom Enter-to-send logic --- # query An object containing parsed URL query parameters from the current page URL. ## Signature ```js query ``` ## Type `object` - Key-value pairs of URL search parameters ## Example ```js // URL: https://example.com/page?name=john&page=2&active=true query.name; // 'john' query.page; // '2' query.active; // 'true' // Check if parameter exists if (query.debug) { enableDebugMode(); } // Use with defaults const page = query.page || '1'; const sort = query.sort || 'date'; // Destructure parameters const { name, page, sort = 'date' } = query; ``` --- # refetch-on-save Flash-free refetch of any element's `href` or `src` after a successful save. The old resource stays visible until the new one has fully loaded, preventing FOUC. ## Usage ```html ``` Add `refetch-on-save` to any element with an `href` or `src` attribute. After every save, the resource is re-fetched with a cache-busted URL and swapped in without a flash. ## How It Works 1. Listens for `hyperclay:save-saved` events 2. For each `[refetch-on-save]` element, creates a clone with a fresh `?v={timestamp}` on the URL 3. Inserts the new element right after the old one (both coexist briefly) 4. When the new element finishes loading (`onload`), removes the old one 5. Fallback: if `onload` doesn't fire within 2 seconds, removes the old element anyway The new element keeps the `refetch-on-save` attribute so it works on subsequent saves. It's also marked `save-ignore` so it doesn't trigger dirty-checking. ## Example ```html ``` ## Comparison with cacheBust | Approach | FOUC? | How | |----------|-------|-----| | `refetch-on-save` | No | Swaps old/new element, old stays until new loads | | `onaftersave="cacheBust(this)"` | Yes | Updates `href`/`src` in-place, brief unstyled flash | Use `refetch-on-save` for stylesheets where a flash matters. Use `cacheBust` for resources where a brief flash is acceptable. --- # save-freeze Freeze an element's innerHTML for save purposes. The live DOM can change freely at runtime, but the saved HTML always contains the original content from when the element first appeared. ## Usage ```html
Content that JS will modify at runtime
``` ## How It Works 1. On page load, the original `innerHTML` of every `[save-freeze]` element is captured 2. For elements added dynamically after load, the content is captured when they enter the DOM 3. At save time, the clone's innerHTML is replaced with the stored original Changes inside `[save-freeze]` elements do not trigger autosave dirty checks. ## Example ```html 0
Try editing this — it won't save.
``` ## Comparison with Other Save Attributes | Attribute | Effect | |-----------|--------| | `save-remove` | Element is removed from saved HTML entirely | | `save-ignore` | Element is excluded from dirty-checking but still saved as-is | | `save-freeze` | Element is saved with its original content, ignoring runtime changes | ## Edit Mode Only This module only runs in edit mode. In view mode, no capturing or freezing occurs. --- # save-system Manual save with change detection, state management, keyboard shortcuts, and save button support. ## Methods | Function | Description | |----------|-------------| | `savePage(callback?)` | Save page if content changed | | `savePageThrottled(callback?)` | Throttled save for auto-save use | | `replacePageWith(url)` | Fetch HTML from URL and save it | | `beforeSave(fn)` | Register a hook to modify content before saving | | `getPageContents()` | Get current page HTML as string | ## Save States The `` element gets a `savestatus` attribute: | State | Description | |-------|-------------| | `saving` | Save in progress (shows after 500ms delay) | | `saved` | Save completed successfully | | `offline` | No network connection | | `error` | Save failed | ## Events | Event | Description | |-------|-------------| | `hyperclay:save-saving` | Fired when save starts | | `hyperclay:save-saved` | Fired on successful save | | `hyperclay:save-offline` | Fired when offline | | `hyperclay:save-error` | Fired on save error | ## Example ```js // Manual save hyperclay.savePage(); // Save with callback hyperclay.savePage(({msg, msgType}) => { if (msgType === 'error') { console.error('Save failed:', msg); } }); // Register before-save hook hyperclay.beforeSave((clone) => { // Modify the snapshot clone before saving clone.querySelectorAll('.temp').forEach(el => el.remove()); }); // Replace page with template hyperclay.replacePageWith('/templates/blog.html'); ``` ```html ``` ## Change Detection - Tracks content changes since last save - Skips save if content hasn't changed - Compares against baseline captured after page load (1.5s delay) ## Save Lifecycle Attributes | Attribute | Effect | |-----------|--------| | `save-remove` | Element is removed from saved HTML entirely | | `save-ignore` | Element is excluded from dirty-checking but still saved as-is | | `save-freeze` | Element is saved with its original content, ignoring runtime changes | | `onbeforesave` | Inline JS that runs on the snapshot clone before save | | `onaftersave` | Inline JS that runs on the live DOM after a successful save | | `trigger-save` | Click triggers a save | ## Related Modules - `autosave` - Auto-save on DOM changes - `save-freeze` - Freeze element content for saves - `save-toast` - Toast notifications for save events - `unsaved-warning` - Warn before leaving with unsaved changes --- # save-toast Shows toast notifications for save lifecycle events. No configuration needed. ## Methods | Method | Description | |--------|-------------| | `hyperclay:save-saved` | Green success toast with "Saved" | | `hyperclay:save-error` | Red error toast with "Failed to save" | | `hyperclay:save-offline` | Red error toast with "No internet connection" | ## Example ```html ``` --- # sendMessage Send form data or a custom object to the `/message` endpoint. Automatically collects form data, includes behavior tracking, and handles success/error toasts. ## Signature ```js sendMessage(eventOrObj, successMessage, callback) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | eventOrObj | Event\|object | — | Form submit event, click event, or data object to send | | successMessage | string | `'Successfully sent'` | Toast message on success | | callback | function | — | Called with response data on success | ## Returns `Promise` - Resolves with server response, rejects on error ## Example ```js // Handle form submission document.querySelector('form').onsubmit = (e) => { sendMessage(e, 'Message sent!'); }; // Event outside a form (sends behavior data only) document.querySelector('#contact-btn').onclick = (e) => { sendMessage(e, 'Contact request sent!'); }; // Send custom data object sendMessage({ name: 'John', email: 'john@example.com' }, 'Contact form submitted'); // With async/await try { const result = await sendMessage(formEvent); redirectToThankYou(); } catch (error) { console.error('Failed:', error); } ``` --- # slugify Convert text into a URL-friendly slug. Handles accents, spaces, and special characters. ## Signature ```js slugify(text) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | text | string | — | The text to convert to a slug | ## Returns `string` - URL-friendly slug ## Example ```js slugify('Hello World'); // 'hello-world' slugify('Café & Restaurant'); // 'cafe-restaurant' slugify(' Multiple Spaces '); // 'multiple-spaces' slugify('Ñoño with Accénts'); // 'nono-with-accents' // Use for URLs const title = 'My Blog Post Title!'; const url = `/posts/${slugify(title)}`; // '/posts/my-blog-post-title' ``` --- # snippet Display a modal with a code snippet and a copy-to-clipboard button. ## Signature ```js snippet(title, content, extraContent) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | title | string | — | The modal heading | | content | string | — | The code/text to display and copy | | extraContent | string | `''` | Optional warning or info text below the copy button | ## Returns `Promise` - Resolves when modal is closed ## Example ```js // Show embed code snippet('Embed Code', ''); // With a warning message snippet( 'API Key', 'sk-1234567890abcdef', 'Keep this key secret. Do not share it publicly.' ); // Show configuration const config = JSON.stringify({ theme: 'dark', lang: 'en' }, null, 2); snippet('Your Settings', config); ``` --- # sortable Enable drag-and-drop sorting on child elements. ## Usage ```html
  • Item 1
  • Item 2
``` ## Attributes | Attribute | Description | |-----------|-------------| | `sortable` | Enable sorting. Optional value sets group name for cross-list dragging. | | `sortable-handle` | Restrict dragging to this element | | `onsorting` | Code to run during drag | | `onsorted` | Code to run after drop | ## Edit Mode Only Sortable.js (~118KB) is only loaded in edit mode. The vendor script is injected with `save-remove` so it's stripped from saved pages. ## Example ```html
  • Drag me
  • And me
  • Me too
  • ⋮⋮ Item with handle
  • ⋮⋮ Another item
  • List A - Item 1
  • List A - Item 2
  • List B - Item 1
  • List B - Item 2
  • Item 1
  • Item 2
  • Item 1
  • Item 2
``` ## Callback Context Both `onsorting` and `onsorted` receive: - `this` - the sortable container element - `evt` - the Sortable.js event object ```html
    ...
``` --- # tell Display an informational modal with a title and optional content paragraphs. ## Signature ```js tell(promptText, ...content) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | promptText | string | — | The title/heading text | | ...content | string[] | — | Additional content paragraphs (variadic) | ## Returns `Promise` - Resolves when user confirms, rejects on close ## Example ```js // Simple message await tell('Welcome!'); // With additional content await tell( 'About This App', 'This is a collaborative editing platform.', 'Changes are saved automatically.', 'Press CMD+S to save manually.' ); // Informational popup tell('Tip', 'You can drag and drop items to reorder them.'); ``` --- # themodal A flexible modal window creation system. Configure and display custom modals with full control over content and behavior. ## Signature ```js themodal.html = content; themodal.yes = buttonContent; themodal.no = buttonContent; themodal.open(); themodal.close(); ``` ## Properties | Name | Type | Default | Description | |------|------|---------|-------------| | html | string | `''` | Main content HTML | | yes | string | `''` | Confirm button HTML (hidden if empty) | | no | string | `''` | Cancel button HTML (hidden if empty) | | closeHtml | string | `''` | Close button HTML | | zIndex | string | `'100'` | CSS z-index for the modal | | fontFamily | string | system monospace | Font family for modal text | | fontSize | string | `'18px'` | Base font size | | inputFontSize | string | `'16px'` | Font size for inputs | | disableFocus | boolean | `false` | Disable auto-focus on first input | | disableScroll | boolean | `true` | Disable body scroll when modal is open | | isShowing | boolean | `false` | Whether modal is currently visible (read-only) | ## Methods | Method | Description | |--------|-------------| | `open()` | Show the modal | | `close()` | Close the modal (triggers onNo callbacks) | | `onYes(callback)` | Add callback for confirm action. Return `false` to prevent closing | | `onNo(callback)` | Add callback for cancel/close action | | `onOpen(callback)` | Add callback for when modal opens | ## Example ```js // Basic custom modal themodal.html = '

Custom Title

Your content here

'; themodal.yes = 'Confirm'; themodal.no = 'Cancel'; themodal.onYes(() => { console.log('User confirmed'); }); themodal.onNo(() => { console.log('User cancelled'); }); themodal.open(); // Modal with form validation themodal.html = ''; themodal.yes = 'Submit'; themodal.onYes(() => { const email = document.querySelector('.micromodal__input').value; if (!email.includes('@')) { toast('Invalid email', 'error'); return false; // Prevent modal from closing } processEmail(email); }); themodal.open(); ``` --- # throttle Limit how often a function can be called. The function executes at most once per specified delay period. ## Signature ```js throttle(callback, delay, executeFirst) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | callback | function | — | The function to throttle | | delay | number | — | Minimum time between calls in milliseconds | | executeFirst | boolean | `true` | Execute immediately on first call | ## Returns `function` - Throttled version of the callback ## Example ```js // Throttle scroll handler to once per 100ms const handleScroll = throttle(() => { updateScrollPosition(); }, 100); window.addEventListener('scroll', handleScroll); // Throttle resize handler const handleResize = throttle(() => { recalculateLayout(); }, 200); window.addEventListener('resize', handleResize); // Don't execute immediately on first call const lazyUpdate = throttle(updateUI, 500, false); ``` --- # toast Display a toast notification for success or error messages. ## Signature ```js toast(message, messageType) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | message | string | — | The message to display | | messageType | string | `'success'` | Either `'success'` or `'error'` | ## Returns `void` ## Example ```js // Show a success message toast('Changes saved!', 'success'); // Show an error message toast('Something went wrong', 'error'); // Success is the default type toast('Uploaded successfully'); ``` --- # unsaved-warning Warns users before leaving the page if there are unsaved changes. On `beforeunload`, compares current page content to last saved content and shows the browser's native dialog if different. ## Methods | Method | Description | |--------|-------------| | `beforeunload` | Fires automatically when user tries to leave with unsaved changes | | `getPageContents()` | Used internally to compare current vs saved state | ## Example ```html ``` --- # uploadFile Upload a file to the `/upload` endpoint with progress toasts and automatic clipboard copy of the resulting URL. ## Signature ```js uploadFile(eventOrFile, callback, extraData) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | eventOrFile | Event\|File | — | File input change event or File object | | callback | function | `() => {}` | Called with response on success | | extraData | object | `{}` | Additional data to include in the request | ## Returns `Promise` - Resolves with server response containing URLs ## Limits - Maximum file size: **10 MB**. Larger files are rejected with an error toast. ## Example ```js // Handle file input document.querySelector('input[type="file"]').onchange = (e) => { uploadFile(e, (response) => { console.log('Uploaded to:', response.urls); }); }; // Upload a File object directly const file = new File(['content'], 'test.txt', { type: 'text/plain' }); uploadFile(file); // With extra metadata uploadFile(event, null, { folder: 'images', public: true }); // Progress is shown automatically via toasts: // "10% uploaded" → "50% uploaded" → "80% uploaded" → "Uploaded! URL copied" ``` --- # uploadFileBasic Upload a file with custom progress, completion, and error callbacks. A lower-level alternative to `uploadFile` without progress toasts (HTTP errors still trigger toast notifications). ## Signature ```js uploadFileBasic(eventOrFile, options) ``` ## Parameters | Name | Type | Default | Description | |------|------|---------|-------------| | eventOrFile | Event\|File | — | File input change event or File object | | options | object | `{}` | Callback options | | options.onProgress | function | `() => {}` | Called with percent complete (0-100) | | options.onComplete | function | `() => {}` | Called with response on success | | options.onError | function | `() => {}` | Called with error on failure | ## Returns `Promise` - Resolves with server response, rejects on error ## Limits - Maximum file size: **10 MB**. Larger files are rejected with an error. ## Example ```js // With custom progress UI uploadFileBasic(fileInput.files[0], { onProgress: (percent) => { progressBar.style.width = percent + '%'; progressText.textContent = percent + '%'; }, onComplete: (response) => { progressBar.classList.add('complete'); showSuccessMessage(response.urls[0]); }, onError: (error) => { progressBar.classList.add('error'); showErrorMessage(error.message); } }); // Minimal usage uploadFileBasic(event, { onComplete: (res) => console.log('Done:', res.urls) }); // With async/await try { const result = await uploadFileBasic(file, { onProgress: p => console.log(p + '%') }); console.log('Uploaded:', result); } catch (err) { console.error('Failed:', err); } ``` --- # Lazy-Loading Vendor Scripts ## Problem Large vendor scripts (Sortable.js ~118KB) were bundled directly into modules, forcing all users to download them even if they were just viewing the page. ## Solution Conditionally load heavy vendor scripts only when in edit mode via dynamically injected ` ``` 4. **Vendor script loads and initializes** 5. **On save**: `save-remove` attribute causes script tag to be stripped from saved HTML ### Code Pattern Wrappers use a shared utility (`utilities/loadVendorScript.js`): ```js import { isEditMode } from "../core/isAdminOfCurrentResource.js"; import { loadVendorScript, getVendorUrl } from "../utilities/loadVendorScript.js"; function init() { if (!isEditMode) return; loadVendorScript(getVendorUrl(import.meta.url, 'example.vendor.js')); } init(); ``` For scripts that need the loaded global: ```js async function init() { if (!isEditMode) return; const Lib = await loadVendorScript(url, 'LibGlobalName'); // Use Lib... } ``` ## File Naming Convention | File | Purpose | |------|---------| | `module.js` | Lightweight wrapper (~1-3KB) in module graph | | `module.vendor.js` | Heavy vendor script, loaded via script tag | ## Results | Module | Before | After | Savings | |--------|--------|-------|---------| | sortable | 118.1KB | 3.1KB | ~115KB | ## Benefits - **Viewers** never download heavy vendor scripts - **Editors** get full functionality when needed - **Saved pages** stay clean (script tags stripped via `save-remove`) - **CDN caching** still works for vendor scripts - **Module graph** stays small and fast ## Files Changed - `vendor/Sortable.js` → `vendor/Sortable.vendor.js` - `utilities/loadVendorScript.js` (shared utility) - `custom-attributes/sortable.js` (rewritten as wrapper) - `build/generate-dependency-graph.js` (updated module definitions) --- # view-mode-excludes-edit-modules A feature flag that skips edit-only modules when the page is in view mode, reducing bundle size for viewers. ## Usage Add `view-mode-excludes-edit-modules` to your features list: ```html ``` Or with custom features: ```html ``` ## How It Works 1. **Detects edit mode** using the same logic as hyperclay: - URL parameter `?editmode=true` (highest priority) - Cookie `isAdminOfCurrentResource` (fallback) 2. **In edit mode**: All requested modules load normally 3. **In view mode**: Modules marked as `isEditModeOnly` are automatically excluded ## Edit-Only Modules These modules are skipped in view mode when this feature is enabled: | Module | Description | |--------|-------------| | `save-core` | Basic save function | | `save-system` | CMD+S, [trigger-save] button | | `autosave` | Auto-save on DOM changes | | `unsaved-warning` | Warn before leaving with unsaved changes | | `save-toast` | Toast notifications for save events | | `edit-mode-helpers` | [viewmode:disabled], [editmode:resource], [editmode:onclick] | | `persist` | Persist input/select/textarea values to DOM | | `snapshot` | DOM snapshots for save and sync | | `sortable` | Drag-drop sorting | | `onaftersave` | Run JS when save status changes | | `cache-bust` | Cache-bust href/src attributes | | `file-upload` | File upload with progress | | `live-sync` | Real-time DOM sync across browsers | ## Size Savings When using the `standard` preset: | Mode | Modules Loaded | Approximate Size | |------|----------------|------------------| | Edit mode | All standard modules | ~50KB | | View mode | Only view-compatible modules | ~15KB | ## When to Use - **Production sites** where most visitors are viewers, not editors - **Performance-critical pages** where every KB matters - **Sites with heavy edit features** (live-sync, sortable, file-upload) ## When NOT to Use - **Development** - you want full functionality regardless of mode - **Admin-only pages** - all users are editors anyway - **Simple sites** - if you're already using minimal preset, savings are small