UNPKG

nmask

Version:

A lightweight jQuery plugin for masking numeric inputs with separators and prefix options.

325 lines (281 loc) 10.8 kB
(function ($) { // Store original jQuery val method const originalVal = $.fn.val; // Override jQuery .val() method $.fn.val = function(value) { // If setting value if (arguments.length > 0) { return this.each(function() { const $this = $(this); // Check if element has nmask data if ($this.data('nmask-active')) { if ($this.is('input')) { // For input elements, use original behavior originalVal.call($this, value); } else { // For non-input elements, use custom setValue method const setValue = $this.data('nmask-setValue'); if (setValue) { setValue(value); } } } else { // Use original jQuery val method originalVal.call($this, value); } }); } // If getting value else { const $this = $(this.first()); // Check if element has nmask data if ($this.data('nmask-active')) { if ($this.is('input')) { // For input elements, use original behavior return originalVal.call($this); } else { // For non-input elements, use custom getValue method const getValue = $this.data('nmask-getValue'); return getValue ? getValue() : ''; } } else { // Use original jQuery val method return originalVal.call($this); } } }; $.fn.nmask = function (optionsOrMethod) { const defaultSettings = { thousandsSeparator: ".", decimalSeparator: ",", decimalDigits: 0, prefix: "", allowNegative: false, }; const escapeRegExp = (string) => { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }; if (optionsOrMethod === "destroy") { return this.each(function () { const $original = $(this); // Find visual input by data attribute instead of ID const $visual = $original.data('nmask-visual'); const $hiddenInput = $original.data('nmask-hidden'); // Remove nmask data $original.removeData('nmask-active'); $original.removeData('nmask-setValue'); $original.removeData('nmask-getValue'); $original.removeData('nmask-visual'); $original.removeData('nmask-hidden'); if ($visual && $visual.length) { $visual.off(".nmask").remove(); } if ($hiddenInput && $hiddenInput.length) { $hiddenInput.off(".nmask").remove(); } $original.off(".nmask"); if ($original.is('input')) { $original.show().css({ opacity: '', width: '', height: '', border: '', padding: '', margin: '', minWidth: '', minHeight: '' }); } }); } const settings = $.extend({}, defaultSettings, optionsOrMethod); const formatNumber = (value) => { if (!value || isNaN(value)) return ""; let num = value.toString().replace(/[^0-9\-\.]/g, ""); let isNegative = value.toString().startsWith("-"); if (isNegative) num = num.substring(1); let [intPart, decPart] = num.split("."); intPart = intPart.replace(/^0+(?=\d)/, ""); // remove leading zeros intPart = intPart.replace( /\B(?=(\d{3})+(?!\d))/g, settings.thousandsSeparator ); if (settings.decimalDigits > 0) { decPart = (decPart || "").slice(0, settings.decimalDigits); let decimalString = decPart.length > 0 ? settings.decimalSeparator + decPart : ""; return ( (isNegative ? "-" : "") + settings.prefix + intPart + decimalString ); } else { return (isNegative ? "-" : "") + settings.prefix + intPart; } }; const cleanNumber = (val) => { if (!val) return ""; if (settings.prefix) { val = val.replace(new RegExp(escapeRegExp(settings.prefix), "g"), ""); } if (settings.thousandsSeparator) { val = val.replace( new RegExp(escapeRegExp(settings.thousandsSeparator), "g"), "" ); } val = val.replace( new RegExp( `[^0-9${settings.allowNegative ? "\\-" : ""}${ settings.decimalSeparator === "." ? "" : escapeRegExp(settings.decimalSeparator) }]`, "g" ), "" ); if (settings.decimalSeparator && settings.decimalSeparator !== ".") { val = val.replace(settings.decimalSeparator, "."); } return val; }; return this.each(function () { const $original = $(this); const isInput = $original.is('input'); // Check if already initialized by looking for nmask-active data if ($original.data('nmask-active')) { return; // Skip if already initialized } // Mark element as having nmask active $original.data('nmask-active', true); // For input elements (original behavior with visual input) if (isInput) { // Auto-generate ID only if needed for input-group functionality let originalId = $original.attr("id"); if (!originalId && $original.parent().hasClass("input-group")) { originalId = "nmask_" + Math.floor(Math.random() * 10000); $original.attr("id", originalId); } const $visual = $('<input type="text" autocomplete="off">') .addClass($original.attr("class") || ""); // Set ID for visual input only if original has ID if (originalId) { $visual.attr("id", originalId + "_visual"); } if ($original.attr("placeholder")) { $visual.attr("placeholder", $original.attr("placeholder")); } ["readonly", "disabled", "required", "style"].forEach((prop) => { if ($original.prop(prop)) $visual.prop(prop, $original.prop(prop)); }); $original.css({ opacity: 0, width: 0, height: 0, border: "none", padding: 0, margin: 0, minWidth: 0, minHeight: 0, }); $original.attr("tabindex", -1); if ($original.parent().hasClass("input-group")) { $visual.insertAfter($original); $original.insertAfter($original.parent()); } else { $original.after($visual); } // Store reference to visual input in data $original.data('nmask-visual', $visual); $visual.val(formatNumber($original.val())); $original.attr("step", "any"); $visual.on("input.nmask", function (e) { let val = $(this).val(); let cleanVal = cleanNumber(val); if (cleanVal) { let parts = cleanVal.split("."); let intPart = parts[0].replace(/^(-)?0+(?=\d)/, "$1"); let decPart = parts[1] || ""; if (settings.decimalDigits > 0) { decPart = decPart.slice(0, settings.decimalDigits); let originalVal = decPart.length > 0 ? intPart + "." + decPart : intPart; $original.val(originalVal); let visualVal; if (val.endsWith(settings.decimalSeparator) && decPart.length === 0) { visualVal = val; } else { visualVal = formatNumber(originalVal); } $(this).val(visualVal); } else { $original.val(intPart); $(this).val(formatNumber(intPart)); } $original.trigger("input"); } else { $original.val(""); $visual.val(""); $original.trigger("input"); } $original.trigger("change"); }); const syncFromOriginal = () => { const originalVal = $original.val(); const visualVal = $visual.val(); const endsWithDecimalOnly = settings.decimalDigits > 0 && visualVal && visualVal.endsWith(settings.decimalSeparator); if (!endsWithDecimalOnly) { $visual.val(formatNumber(originalVal)); } }; $original.on("input.nmask change.nmask", syncFromOriginal); } // For non-input elements (display only - no ID required) else { // Create hidden input WITHOUT requiring ID const $hiddenInput = $('<input type="hidden">'); // Set name attribute from data-name or generate unique name const nameAttr = $original.attr("data-name") || $original.attr("name") || "nmask_field_" + Math.floor(Math.random() * 10000); $hiddenInput.attr("name", nameAttr); $original.after($hiddenInput); // Store reference to hidden input in data $original.data('nmask-hidden', $hiddenInput); // Set initial value from element's text, data-value, or empty const initialValue = $original.text() || $original.attr("data-value") || ""; const cleanInitial = cleanNumber(initialValue); $hiddenInput.val(cleanInitial); $original.text(formatNumber(cleanInitial)); // Create setValue and getValue methods for .val() override const setValue = function(value) { const cleanVal = cleanNumber(value.toString()); $hiddenInput.val(cleanVal); $original.text(formatNumber(cleanVal)); $original.trigger("change"); $original.trigger("nmask:change", [cleanVal]); }; const getValue = function() { return $hiddenInput.val(); }; // Store methods in jQuery data for .val() override $original.data('nmask-setValue', setValue); $original.data('nmask-getValue', getValue); } // Form submit handler const form = $original.closest("form"); if (form.length) { form.off("submit.nmask").on("submit.nmask", function () { if (isInput) { const $visual = $original.data('nmask-visual'); if ($visual && $visual.length) { $original.val(cleanNumber($visual.val())); } } // For non-input elements, hidden input already contains clean value }); } }); }; })(jQuery);