Draft
feat: add mask() method to input field components for masking library integration#161
Conversation
Copilot created this pull request from a session on behalf of
bitifet
April 17, 2026 23:21
View session
@rollup/plugin-babel@7.1.0 only supports @babel/core@^7.0.0. Downgrade back to Babel 7.29.x until the plugin ecosystem catches up.
… integration - Add mask(callback) method to SmarkField (via input.type.js): - Changes <input> type to "text" for non-text inputs (required by iMask.js etc.) - Stores original type in _originalType for reference - Calls callback(targetFieldNode) and stores result as _maskInstance - Delegates to inner field for singletons, returns `me` for chaining - Update _setTargetFieldValue() to dispatch "input" event when mask is active, so masking libraries re-process the new value on programmatic import() - Update export() to use _maskInstance.unmaskedValue when available, so derived types (number, date, etc.) still parse/return the correct semantic type - Add comprehensive tests in test/mask.tests.js (13 scenarios) Agent-Logs-Url: https://github.com/bitifet/SmarkForm/sessions/72c9bcc2-72c5-42a0-8d32-9fe27015b3da Co-authored-by: bitifet <1643647+bitifet@users.noreply.github.com>
- Primary credit card example showing space-separated 4-digit groups - Price example demonstrating number field type with decimal/thousands separators - Singleton masking pattern with inner field delegation - _originalType restoration for native HTML5 input behavior - Validation integration using mask library events - Updated AGENTS.md with masking feature details
…sages - Add deep isSerializable() validation in setNodeOptions that checks for functions, symbols, undefined, non-finite numbers, and circular references - Throw renderError(INVALID_OPTIONS_OBJECT) instead of cryptic JSON.stringify errors - Convert field_masking.md static code blocks to interactive sampletabs playgrounds
- Restore original general tests (document loaded, focus behavior, default values focus race, basic introspection) - Add new tests for options serialization validation (cyclic refs, functions) - These validate the INVALID_OPTIONS_OBJECT renderError behavior
- Remove 'undefined' check from isSerializable() — JSON.stringify silently drops undefined property values, so they are serializable - Guard getPath() against unset parents (called during construction when renderError is thrown before parents is initialized) - Update masking docs: replace 'restores original type' with 'masking is permanent' to match current implementation
…try/catch - Resolve string selectors (e.g., '#payment') to DOM nodes in the SmarkForm constructor so string-based instantiation works - Remove the debugging try/catch wrapper from setNodeOptions — the isSerializable pre-check throws clear errors that propagate naturally through the constructor - Update validation tests to match new error behavior
…ix credit card blocks, add tests=false
… CC validation, phone list with singleton, remove validation/singleton sections
All 5 runs from the improvement plan, completed in this commit:
Run 1: Fix Custom Mask UNKNOWN_TYPE bug (type:"text" → "input")
Run 2: All examples use id="myForm$$" wrapper + document.getElementById("myForm$$")
- mask-singleton: switch from singleton to regular form (timing issue with validateInputType)
- mask-custom: fix test path /custom/phone → /contacts/phones
Run 3: Credit card factory returns null for unmaskedValue when invalid; remove Validation section
Run 4: Custom Mask → phone list with singleton+list; remove Singleton section; improve all notes
Run 5: Evaluate DOM error indicator smoke test — deferred (redundant with console.error)
Also: updated AGENTS/Documentation-Examples.md with singleton+list pattern docs,
updated AGENTS.md masking docs, created docs/masking_docs_plan.md.
…tside wrapper, fix custom mask list example - number.type.js: guard data.length against null from mask unmaskedValue - co_located_tests.tests.js: isFormRoot() skips leading <script> tags - field_masking.md: move CDN <script> tags outside id="myForm$$" wrapper - field_masking.md: fix mask-custom include using mask_price_* vars instead of mask_custom_* - field_masking.md: fix buttons to use action instead of type - field_masking.md: use inline list template (external <template> not supported) - field_masking.md: add min_items:0 to empty phone list - AGENTS/Documentation-Examples.md: update CDN script placement docs
…ineProperty Object.defineProperty on IMask's native unmaskedValue can fail silently (make the field export fall back to the formatted string with spaces, which Number() parses as NaN). Returning the IMask instance directly lets SmarkForm read its unmaskedValue as intended.
…s as-you-type - Credit card: wrap IMask in plain object with unmaskedValue getter that checks imask.masked.isComplete instead of using Object.defineProperty (which fails because IMask's unmaskedValue is non-configurable). - Phone list: custom mask now updates node.value in real-time inside the input handler so non-digit characters are stripped as the user types, with space-grouping for readability.
…underscores, inputmode numeric - Remove placeholder from HTML (now set in factory) - IMask with lazy: false + placeholderChar: "_" fills unfilled positions with underscores, keeping cursor at the right place - Set inputMode = "numeric" for mobile numeric keyboards
c2c68a8 to
993c66e
Compare
…d+blur autocomplete - Credit card: IMask starts lazy:true (native placeholder empty), on first digit updates to lazy:false+placeholderChar:"_" (underscores fill) - Price: factory sets placeholder "0.00" and inputMode "decimal" plus lazy:false+placeholderChar:"_" for decimal hint. Blur handler auto-completes missing decimals with zeros and prepends zero when starting with decimal separator.
…ples, enhance CC section - Via JavaScript: now a sampletabs example with HTML+JS+Notes, JS tab default - Via Declarative HTML: now a sampletabs example with HTML+Notes, HTML tab default - Removed standalone 'Applying a Mask to a Field' (folded into the examples) - Credit Card section text now frames it as an evolution of the basic examples - Credit Card JS: added inline comments explaining each step - Credit Card Notes: expanded to document placeholder, lazy switching, null-export - Both CC and Price examples now default to JS tab
…inputMode to custom mask - Tab selection: "js" not "javascript" (matches template line 239) - Restored "Applying a Mask to a Field" section BEFORE "Registering", reusing common captures (DRY) - All new examples have showEditor=true so playground buttons appear - Custom mask (digits) now sets node.inputMode = "numeric"
- Applying a Mask: HTML now just the form (no CDN script), JS uses async IIFE that dynamically loads IMask. Keeps HTML tab clean and focused. - Via JavaScript: uses separate CDN+form HTML capture - Restored min_items:0 on phone list (removal broke co-located test) - async JS uses 'let myForm' at top scope so test framework's IIFE can capture it
…ve min_items:0
- Price section removed (redundant — CC example already demonstrates number→text
conversion). TOC updated.
- CC notes simplified: now 2 sentences on wrapper and lazy switching, vs the
8-line list that repeated the section text verbatim.
- via-js notes simplified: focused on type conversion insight not in section text.
- Restored let myForm (required by test framework's IIFE wrapper).
- Removed min_items:0 — list starts with 1 item by default.
Co-located test updated to expect [{phone: ""}].
- Added under Advanced UX Improvements, after Type coercion - Credit card example with IMask, showing declarative masking via data-smark - Includes jsHead with registerMask() + wrapper for null export on incomplete - DemoValue: pre-filled Visa test number (4111111111111111) - Cross-reference link to advanced concepts/field_masking page - TOC updated
…case to full CC example - Credit Card factory: red boxShadow blink when IMask rejects a keystroke - Custom Mask (digits) factory: same blink when non-digit chars are filtered - Showcase: replaced simplified demo with full CC example (lazy switching, placeholder factory-side, blink detection, null-export wrapper). Notes now link to Field Masking page for deeper explanation.
- CC factory: setCustomValidity() marks field :invalid when partially filled (has content but not complete). Distinguishable from blink (orange vs red). - Blink color changed to orange (#f80) to not conflict with :invalid red. - Preview iframes: added drag-to-resize handle (bottom bar, ns-resize cursor). Hover to reveal, mousedown to drag, clamps at min 75px. - Notes and Showcase copy updated to mention :invalid state.
The drag-to-resize code was placed after 'if (!editToggle || !runBtn) return;' which skipped it for all examples without edit buttons.
- Drag: switch from mouse events to Pointer Events with setPointerCapture.
mouseup on document never fires when cursor is over the iframe, so drag
continued indefinitely. Pointer capture delivers all events to the handle
regardless of cursor position.
- :invalid styling: added 'input:invalid{outline:2px solid red}' to baseCss
so setCustomValidity produces a visible red outline in preview iframes.
- Removed input:invalid from global baseCss (was affecting all examples). - Added mask_cc_css capture with gentle amber outline (#d4c070, 1px) for incomplete intermediate state — distinct from the orange blink for errors. - Applied via cssSource parameter on CC examples in both field_masking.md and showcase.md so the CSS tab shows it to the reader. - Fixed 'red blink' → 'orange blink' in showcase notes.
When the cursor is at a mask literal separator (space between card groups), Backspace/Delete cannot remove it — IMask restores immediately. The value does not change, which triggered a false blink. Now we track the last key and skip blink for Backspace/Delete (valid no-ops on separators).
…ages New pages created: - keyboard_navigation.md (from Smooth navigation + Collapsible sections prose) - animations.md (from Animations section prose) - value_coercion.md (from Smart value coercion prose) Showcase trimmed: - All documentation-only sections reduced to 1-3 sentences + link to new pages - Deleted captures restored (simple_list_hotkeys_with_context) - Collapsible sections heading restored (was accidentally overwritten) - All demos preserved with minimal prose and cross-references
…r prose, add examples to new pages - Restored simple_list_hotkeys_with_context demo (was deleted during trimming) - Removed duplicate 'A note on empty values' heading and leftover prose - keyboard_navigation.md: added Enter navigation demo - animations.md: added list slide-in/out animation demo - value_coercion.md: added coercion demo (number/date/JSON/email list)
…SON hint - Each enumerated feature now links to its canonical documentation page - Added hint callout noting the demo uses pre-loaded JSON data and CSS
…+Enter demos to keyboard navigation - Showcase: 'Multiple-Level Nesting' conveys unlimited depth - Keyboard nav: new nav-tabflow demo with hotkey-equipped inner/outer buttons - Keyboard nav: new nav-details demo with collapsed <details> section
…Concepts New 'Working with Forms' category (7 pages): - form_traversing, keyboard_navigation, hotkeys, data_import_and_export, value_coercion, field_masking, the_api_interface Kept in Advanced Concepts (6 pages): - the_smarkform_constructor, events, mixin_types, security_considerations, error_codes, animations - All permalinks, nav_orders updated - Every internal cross-reference fixed across entire docs/ tree - _config.yml: new collection at nav_order 4
- 'Defining a Hotkey' now a sampletabs demo (was static HTML block) - 'Hotkey Reveal' now a sampletabs demo (was static CSS block) - Added 2nd-level hotkeys demo to 'Further Examples' section - 3 new playable examples, 100 total
…ion page - simple_list_hotkeys_with_context: captures were defined AFTER the include that uses them. Moved before the include — round-trip now passes. - value_coercion.md: replaced simple combo demo with two showcase demos (smart_value_coercion + type_coercion), with tests and notes. - Removed duplicate captures.
Showcase coercion sections now only have brief prose + link to value_coercion.md which hosts both full demos (smart_value_coercion and type_coercion) with their tests and notes.
…nstructor page Source changes: - Constructor extracts on_* and smark_* options (constructor-only), passes everything else as pass-through to root form. - maskConfig.throwOnMissing → smark_mask_throwOnMissing (constructor-only, stored in root._ctorOptions, read by input.type.js). - SmarkForm.registerCustomAction(name, handler) — static method for global custom action registration, merged with per-instance customActions. - Removed SmarkForm.maskConfig static default (replaced by ctor default). Constructor page restructured: - Pass-Through Options promoted to §2 with scope column and merging rules. - Constructor-Only Options §3: explains on_/smark_ naming convention. - Static Members §4: registerMask() and registerCustomAction(). Updated: field_masking.md (maskConfig→smark_mask_throwOnMissing), AGENTS.md, declarative_mask.tests.js. All 183 tests pass.
…etNodeOptions - on_* options must flow through to the component decorator which extracts them and registers event handlers. Only smark_* is constructor-only. - setNodeOptions rejects functions in options — filter on_* and smark_* before serialization (pre-existing bug, now fixed). - general.tests.js: cyclic options test now matches both 'circular' and 'cyclic' (Safari uses a different error message).
- Moved on* event handlers from Constructor-Only Options to Pass-Through table - Clarified naming convention: on_* flows to component decorator - TOC updated
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a public
.mask(callback)method to SmarkForm field components that provides a clean integration scaffold for input-masking libraries (like iMask.js) without adding any hard dependency.Changes
src/types/input.type.jsmask(callback)method (added to theinputclass, inherited bynumber,date,time,datetime-local,color, etc.):<input type>to"text"for any non-text input, because masking libraries like iMask.js requiretype="text". The original type is preserved in_originalType.callback(targetFieldNode)with the raw DOM element as the sole argument — this is the integration point where the caller attaches iMask or any other library._maskInstance._maskInstancelives whereexport()reads it).thisfor chaining._setTargetFieldValue()updated:inputevent on the element when a mask instance is active. This allows masking libraries to re-process the value whenimport()is called programmatically.export()updated:_maskInstance.unmaskedValueis defined, uses it as the raw value instead ofnodeFld.value. This ensures that derived types (number,date,time, etc.) receive the raw unmasked string and can still apply their own semantic type conversion (e.g.Number("1234.56")→1234.56rather thanNumber("1,234.56")→NaN).test/mask.tests.js(new)13 Playwright test cases covering:
_originalTypeis stored correctlymask()returnsthisfor chainingexport()usesunmaskedValuewhen providednumberafter maskingnodeFld.valuewhen mask has nounmaskedValue(or returns null)_maskInstancelands on the inner fieldimport()dispatchesinputevent on masked fields (and does NOT dispatch it on unmasked fields)Usage example (iMask.js)
Testing
All 284 existing tests continue to pass. All 13 new mask tests pass.