From c61dda467458d67046188d9d7702fef6ca31bbe4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:20:18 +0000 Subject: [PATCH] feat: add mask() method to input field components for masking library integration - Add mask(callback) method to SmarkField (via input.type.js): - Changes 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> --- dist/SmarkForm.esm.js | 2 +- dist/SmarkForm.esm.js.map | 2 +- dist/SmarkForm.umd.js | 2 +- dist/SmarkForm.umd.js.map | 2 +- src/types/input.type.js | 30 +++ test/mask.tests.js | 382 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 test/mask.tests.js diff --git a/dist/SmarkForm.esm.js b/dist/SmarkForm.esm.js index 8da9ef2f..3b97e194 100644 --- a/dist/SmarkForm.esm.js +++ b/dist/SmarkForm.esm.js @@ -1 +1 @@ -function e(e,t,n){if("function"==typeof e?e===t:e.has(t))return arguments.length<3?t:n;throw new TypeError("Private element is not present on this object")}function t(e){if(Object(e)!==e)throw TypeError("right-hand side of 'in' should be an object, got "+(null!==e?typeof e:"null"));return e}function n(e,t){(function(e,t){if(t.has(e))throw new TypeError("Cannot initialize the same private elements twice on an object")})(e,t),t.add(e)}function r(e,t,n){return(t=l(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0;E--){var _;void 0!==(b=r(w[E],o,f,u,l,c,d,h,p))&&(i(l,b),0===l?_=b:1===l?(_=b.init,v=b.get||h.get,N=b.set||h.set,h={get:v,set:N}):h=b,void 0!==_&&(void 0===g?g=_:"function"==typeof g?g=[g,_]:g.push(_)))}if(0===l||1===l){if(void 0===g)g=function(e,t){return t};else if("function"!=typeof g){var S=g;g=function(e,t){for(var n=t,r=0;r3,v=h>=5,N=r;if(v?(g=e,0!=(h-=5)&&(m=i=i||[]),b&&!a&&(a=function(n){return t(n)===e}),N=a):(g=e.prototype,0!==h&&(m=o=o||[])),0!==h&&!b){var w=v?d:l,E=w.get(y)||0;if(!0===E||3===E&&4!==h||4===E&&3!==h)throw Error("Attempted to decorate a public method/accessor that has the same name as a previously decorated public method/accessor. This is not currently supported by the decorators plugin. Property name was: "+y);!E&&h>2?w.set(y,h):w.set(y,!0)}c(s,g,f,y,h,v,b,m,N)}}return u(s,o),u(s,i),s}function u(e,t){t&&e.push(function(e){for(var n=0;n0){for(var r=[],o=t,a=t.name,s=n.length-1;s>=0;s--){var l={v:!1};try{var c=n[s](o,{kind:"class",name:a,addInitializer:e(r,l)})}finally{l.v=!0}void 0!==c&&(i(10,c),o=c)}return[o,function(){for(var e=0;enull===e:t=>null===t||t.isSameNode(e);return[...e.querySelectorAll(t)].filter(e=>n(e.parentNode.closest(t)))}function u(e,t){let n=e.parentNode;const r=t>=0?1:-1;for(;n;){if(n.scrollHeight>n.clientHeight*r){var o=n.scrollHeight-n.clientHeight*r;if(t<=o*r)return void(n.scrollTop+=t);n.scrollTop=o,t-=o}n=n.parentNode}}function p(){return Math.random().toString(36).substring(2)}function f(e){try{return JSON.parse(e)}catch(e){}}function g(e){if(5===e.length&&":"===e[2]){const t=parseInt(e.substring(0,2),10),n=parseInt(e.substring(3,5),10);if(t>=0&&t<=23&&n>=0&&n<=59)return e+":00"}if(8===e.length&&":"===e[2]&&":"===e[5]){const t=parseInt(e.substring(0,2),10),n=parseInt(e.substring(3,5),10),r=parseInt(e.substring(6,8),10);if(t>=0&&t<=23&&n>=0&&n<=59&&r>=0&&r<=59)return e}if(6===e.length){const t=parseInt(e.substring(0,2),10),n=parseInt(e.substring(2,4),10),r=parseInt(e.substring(4,6),10);if(t>=0&&t<=23&&n>=0&&n<=59&&r>=0&&r<=59)return[e.substring(0,2),e.substring(2,4),e.substring(4,6)].join(":")}if(4===e.length){const t=parseInt(e.substring(0,2),10),n=parseInt(e.substring(2,4),10);if(t>=0&&t<=23&&n>=0&&n<=59)return[e.substring(0,2),e.substring(2,4),"00"].join(":")}return null}function m(e){if(15===e.length&&"T"===e[8]){const t=[e.substring(0,4),e.substring(4,6),e.substring(6,8)].join("-"),n=[e.substring(9,11),e.substring(11,13),e.substring(13,15)].join(":");return new Date("".concat(t,"T").concat(n))}if(13===e.length&&"T"===e[8]){const t=[e.substring(0,4),e.substring(4,6),e.substring(6,8)].join("-"),n=[e.substring(9,11),e.substring(11,13),"00"].join(":");return new Date("".concat(t,"T").concat(n))}if(19===e.length&&"-"===e[4]&&"-"===e[7]&&"T"===e[10]&&":"===e[13]&&":"===e[16])return new Date(e);if(16===e.length&&"-"===e[4]&&"-"===e[7]&&"T"===e[10]&&":"===e[13])return new Date(e+":00");return e.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/)?new Date(e):NaN}function h(e){let t=null==e?void 0:e.parentElement;for(;t;){if("DETAILS"===t.tagName&&!t.open){const n=t.querySelector(":scope > summary");if(!n||!n.contains(e))return!0}t=t.parentElement}return!1}function y(e,t,n,r){const o=e.tagName,i=e.getAttribute("type");if("INPUT"!=o||(i||t).toLowerCase()!=t){const e=new Error(r);throw e.code=n,e}i||(e.type=t)}const b=Symbol("Events"),v=Symbol("onEvents"),N=Symbol("allEvents"),w=/^on(?:Before|After)Action_/,E=/^onLocal_/,_=/^on_/,S=/^onAll_/,x={keydown:{bubbles:!0},keyup:{bubbles:!0},keypress:{bubbles:!0},beforeinput:{bubbles:!0},input:{bubbles:!0},change:{bubbles:!0},focus:{bubbles:!1},blur:{bubbles:!1},focusin:{bubbles:!0},focusout:{bubbles:!0},click:{bubbles:!0},dblclick:{bubbles:!0},contextmenu:{bubbles:!0},mousedown:{bubbles:!0},mouseup:{bubbles:!0},mousemove:{bubbles:!0},mouseenter:{bubbles:!1},mouseleave:{bubbles:!1},mouseover:{bubbles:!0},mouseout:{bubbles:!0},focusenter:{bubbles:!0,synthetic:!0},focusleave:{bubbles:!0,synthetic:!0}};function T(e,t,n){return e.has(t)||e.set(t,[]),e.get(t).push(n.bind(this)),this}async function A(e,t){const n=e?[e,...e.parents]:[],r=t?[t,...t.parents]:[],o=function(e,t){var n;const r=new Set(t);return null!==(n=e.find(e=>r.has(e)))&&void 0!==n?n:null}(n,r);for(const e of n){if(e===o)break;await e.emit("focusleave",{type:"focusleave",context:e})}for(const e of r){if(e===o)break;await e.emit("focusenter",{type:"focusenter",context:e})}}const k=Symbol("smarkform_legacy_prevent");var I={disEnhance(e){"form"!==e.targetNode.tagName.toLowerCase()||e.targetNode[k]||(e.targetNode[k]=!0,e.targetNode.addEventListener("submit",function(e){e.preventDefault()}))}};const O=["type"],L=new Map,C=new Set;function D(e){const t=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT);let n=t.currentNode;for(;n;)n.hasAttribute("id")&&(n.setAttribute("data-id",n.getAttribute("id")),n.removeAttribute("id")),n=t.nextNode()}function P(e){try{return new URL(e).origin!==location.origin}catch(e){return!1}}async function F(e,t,n){const r=t.type,o=r.indexOf("#"),s=r.slice(0,o),l=r.slice(o+1);if(!l)throw n.renderError("MIXIN_TYPE_MISSING_FRAGMENT",'Mixin type reference "'.concat(r,'" must include a non-empty')+" #templateId fragment.");let c,d;if(s){var u;d=new URL(s,document.baseURI).href;const e=null!==(u=n.root.options.allowExternalMixins)&&void 0!==u?u:"block";if("block"===e)throw n.renderError("MIXIN_EXTERNAL_FETCH_BLOCKED",'Mixin type "'.concat(r,'" references an external URL but')+' allowExternalMixins is "block" (the default). Set allowExternalMixins to "same-origin" or "allow" on the root SmarkForm instance to permit external mixin loading.');if("same-origin"===e&&P(d))throw n.renderError("MIXIN_CROSS_ORIGIN_FETCH_BLOCKED",'Mixin type "'.concat(r,'" references a cross-origin URL')+" (".concat(new URL(d).origin,") but allowExternalMixins")+' is "same-origin". Set allowExternalMixins to "allow" to permit cross-origin mixin loading.');L.has(d)||L.set(d,fetch(d).then(e=>{if(!e.ok)throw Object.assign(new Error("Failed to fetch mixin source: ".concat(d)+" (HTTP ".concat(e.status,")")),{code:"MIXIN_FETCH_ERROR"});return e.text()}).then(e=>(new DOMParser).parseFromString(e,"text/html"))),c=await L.get(d)}else c=document,d=document.baseURI;const p="".concat(d,"#").concat(l),g=n._mixinChain||new Set;if(g.has(p))throw n.renderError("MIXIN_CIRCULAR_DEPENDENCY",'Circular mixin dependency detected: "'.concat(p,'"')+" is already being expanded in this rendering chain.");const m=c.getElementById(l);if(!m||"template"!==m.tagName.toLowerCase())throw n.renderError("MIXIN_TEMPLATE_NOT_FOUND","Mixin template #".concat(l," not found")+" in ".concat(s||"the current document","."));const h=[...m.content.childNodes].filter(e=>e.nodeType===Node.ELEMENT_NODE),y=h.filter(e=>"script"===e.tagName.toLowerCase()),b=h.filter(e=>"style"===e.tagName.toLowerCase()),v=h.filter(e=>{const t=e.tagName.toLowerCase();return"script"!==t&&"style"!==t});if(1!==v.length)throw n.renderError("MIXIN_TEMPLATE_INVALID_ROOT","Mixin template #".concat(l," must contain exactly one root")+" element (found ".concat(v.length,")."));const N=v[0],w=f(N.getAttribute("data-smark"))||{};if(void 0!==w.name)throw n.renderError("MIXIN_TEMPLATE_ROOT_HAS_NAME","Mixin template #".concat(l," root element must not specify")+' a "name" in its data-smark options. The name must be set on the placeholder (usage site).');const E=N.cloneNode(!0),_=[...e.children].filter(e=>e.hasAttribute("data-for"));_.length>0&&function(e,t){for(const n of t){const t=n.getAttribute("data-for"),r=e.querySelector('[id="'.concat(CSS.escape(t),'"]'));if(!r)continue;const o=n.cloneNode(!0);D(o),o.removeAttribute("data-for"),r.replaceWith(o)}}(E,_),function(e,t){if(e.querySelector("script"))throw t.renderError("MIXIN_NESTED_SCRIPT_DISALLOWED","Mixin template root subtree must not contain \n // \n // is valid.\n const templateContentEls = [...template.content.childNodes]\n .filter(n => n.nodeType === Node.ELEMENT_NODE);\n const topLevelScripts = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'script');\n const topLevelStyles = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'style');\n const rootElements = templateContentEls\n .filter(n => {\n const tag = n.tagName.toLowerCase();\n return tag !== 'script' && tag !== 'style';\n });\n if (rootElements.length !== 1) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_INVALID_ROOT'\n , `Mixin template #${templateId} must contain exactly one root`\n + ` element (found ${rootElements.length}).`\n );\n }\n\n const templateRoot = rootElements[0];\n\n // Validate: template root must not set \"name\" in data-smark:\n const templateRootOptions = parseJSON(\n templateRoot.getAttribute('data-smark')\n ) || {};\n if (templateRootOptions.name !== undefined) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_ROOT_HAS_NAME'\n , `Mixin template #${templateId} root element must not specify`\n + ' a \"name\" in its data-smark options.'\n + ' The name must be set on the placeholder (usage site).'\n );\n }\n\n // Deep-clone the template root:\n const clone = templateRoot.cloneNode(true);\n\n // Collect snippet parameter nodes: direct children of the placeholder\n // that carry a `data-for` attribute, referencing elements inside the clone\n // by their id. These are consumed (not rendered as children).\n const params = [...node.children].filter(el => el.hasAttribute('data-for'));\n\n // Apply snippet parameter substitutions on the clone:\n if (params.length > 0) {\n applySnippetParams(clone, params);\n }\n\n // Enforce no nested \n // \n // is valid.\n const templateContentEls = [...template.content.childNodes]\n .filter(n => n.nodeType === Node.ELEMENT_NODE);\n const topLevelScripts = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'script');\n const topLevelStyles = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'style');\n const rootElements = templateContentEls\n .filter(n => {\n const tag = n.tagName.toLowerCase();\n return tag !== 'script' && tag !== 'style';\n });\n if (rootElements.length !== 1) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_INVALID_ROOT'\n , `Mixin template #${templateId} must contain exactly one root`\n + ` element (found ${rootElements.length}).`\n );\n }\n\n const templateRoot = rootElements[0];\n\n // Validate: template root must not set \"name\" in data-smark:\n const templateRootOptions = parseJSON(\n templateRoot.getAttribute('data-smark')\n ) || {};\n if (templateRootOptions.name !== undefined) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_ROOT_HAS_NAME'\n , `Mixin template #${templateId} root element must not specify`\n + ' a \"name\" in its data-smark options.'\n + ' The name must be set on the placeholder (usage site).'\n );\n }\n\n // Deep-clone the template root:\n const clone = templateRoot.cloneNode(true);\n\n // Collect snippet parameter nodes: direct children of the placeholder\n // that carry a `data-for` attribute, referencing elements inside the clone\n // by their id. These are consumed (not rendered as children).\n const params = [...node.children].filter(el => el.hasAttribute('data-for'));\n\n // Apply snippet parameter substitutions on the clone:\n if (params.length > 0) {\n applySnippetParams(clone, params);\n }\n\n // Enforce no nested \n // \n // is valid.\n const templateContentEls = [...template.content.childNodes]\n .filter(n => n.nodeType === Node.ELEMENT_NODE);\n const topLevelScripts = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'script');\n const topLevelStyles = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'style');\n const rootElements = templateContentEls\n .filter(n => {\n const tag = n.tagName.toLowerCase();\n return tag !== 'script' && tag !== 'style';\n });\n if (rootElements.length !== 1) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_INVALID_ROOT'\n , `Mixin template #${templateId} must contain exactly one root`\n + ` element (found ${rootElements.length}).`\n );\n }\n\n const templateRoot = rootElements[0];\n\n // Validate: template root must not set \"name\" in data-smark:\n const templateRootOptions = parseJSON(\n templateRoot.getAttribute('data-smark')\n ) || {};\n if (templateRootOptions.name !== undefined) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_ROOT_HAS_NAME'\n , `Mixin template #${templateId} root element must not specify`\n + ' a \"name\" in its data-smark options.'\n + ' The name must be set on the placeholder (usage site).'\n );\n }\n\n // Deep-clone the template root:\n const clone = templateRoot.cloneNode(true);\n\n // Collect snippet parameter nodes: direct children of the placeholder\n // that carry a `data-for` attribute, referencing elements inside the clone\n // by their id. These are consumed (not rendered as children).\n const params = [...node.children].filter(el => el.hasAttribute('data-for'));\n\n // Apply snippet parameter substitutions on the clone:\n if (params.length > 0) {\n applySnippetParams(clone, params);\n }\n\n // Enforce no nested \n // \n // is valid.\n const templateContentEls = [...template.content.childNodes]\n .filter(n => n.nodeType === Node.ELEMENT_NODE);\n const topLevelScripts = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'script');\n const topLevelStyles = templateContentEls\n .filter(n => n.tagName.toLowerCase() === 'style');\n const rootElements = templateContentEls\n .filter(n => {\n const tag = n.tagName.toLowerCase();\n return tag !== 'script' && tag !== 'style';\n });\n if (rootElements.length !== 1) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_INVALID_ROOT'\n , `Mixin template #${templateId} must contain exactly one root`\n + ` element (found ${rootElements.length}).`\n );\n }\n\n const templateRoot = rootElements[0];\n\n // Validate: template root must not set \"name\" in data-smark:\n const templateRootOptions = parseJSON(\n templateRoot.getAttribute('data-smark')\n ) || {};\n if (templateRootOptions.name !== undefined) {\n throw component.renderError(\n 'MIXIN_TEMPLATE_ROOT_HAS_NAME'\n , `Mixin template #${templateId} root element must not specify`\n + ' a \"name\" in its data-smark options.'\n + ' The name must be set on the placeholder (usage site).'\n );\n }\n\n // Deep-clone the template root:\n const clone = templateRoot.cloneNode(true);\n\n // Collect snippet parameter nodes: direct children of the placeholder\n // that carry a `data-for` attribute, referencing elements inside the clone\n // by their id. These are consumed (not rendered as children).\n const params = [...node.children].filter(el => el.hasAttribute('data-for'));\n\n // Apply snippet parameter substitutions on the clone:\n if (params.length > 0) {\n applySnippetParams(clone, params);\n }\n\n // Enforce no nested