Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
55 lines
4.4 KiB
JavaScript
55 lines
4.4 KiB
JavaScript
'use strict';
|
|
/* ============================================================================
|
|
* 숫자 입력 칸 천 단위 쉼표 (v1.4.5)
|
|
*
|
|
* 화면의 모든 숫자 입력 칸(input type=number)을 "12,000" 처럼 3자리마다 쉼표가 보이는 칸으로 바꾼다.
|
|
* - 브라우저의 number 칸은 쉼표를 넣을 수 없어, 글자 칸(type=text, inputmode=numeric/decimal)으로 바꾸고 입력할 때마다 쉼표를 다시 넣는다.
|
|
* - 기존 화면 코드(admin.js 등)는 input.value 로 값을 읽어 서버에 보낸다. 그 코드를 모두 고치지 않도록 이 칸들의 value 를
|
|
* "쉼표를 뺀 숫자 문자열" 로 돌려주고, value 에 넣는 값은 쉼표를 붙여 보이게 한다(요소별 value 속성 재정의).
|
|
* → 서버로 가는 값·검증(최소·최대)·저장하지 않은 변경 표시(data-dirty-watch)는 이전과 같다.
|
|
* - 나중에 만들어지는 칸(사이트 관리자 입력 창, 도메인 레코드 등)도 MutationObserver 로 따라가 바꾼다.
|
|
* - 쉼표가 어울리지 않는 칸(포트 번호 등)은 data-plain 속성을 붙이면 그대로 둔다.
|
|
* 전역: window.WMNum = { format(값), clean(문자열), enhance(입력칸) }
|
|
* ========================================================================== */
|
|
(function () {
|
|
var desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
|
|
/** 화면에 보일 모양: 정수부 3자리마다 쉼표, 소수부·음수 부호는 그대로 */
|
|
function format(raw) {
|
|
raw = clean(raw); if (raw === '' || raw === '-' || raw === '.') return raw;
|
|
var neg = raw.charAt(0) === '-'; var s = neg ? raw.slice(1) : raw; var dot = s.indexOf('.');
|
|
var int = dot < 0 ? s : s.slice(0, dot); var frac = dot < 0 ? '' : s.slice(dot);
|
|
int = int.replace(/^0+(?=\d)/, '').replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
return (neg ? '-' : '') + int + frac;
|
|
}
|
|
/** 쉼표·공백·숫자 아닌 글자를 뺀 값 (서버로 보낼 값). 부호는 맨 앞, 소수점은 하나만 */
|
|
function clean(v) {
|
|
var s = String(v == null ? '' : v).replace(/[^\d.\-]/g, ''); var neg = s.charAt(0) === '-'; s = s.replace(/-/g, '');
|
|
var i = s.indexOf('.'); if (i >= 0) s = s.slice(0, i + 1) + s.slice(i + 1).replace(/\./g, '');
|
|
return (neg ? '-' : '') + s;
|
|
}
|
|
/** 입력 중 쉼표를 다시 넣어도 커서가 같은 숫자 뒤에 머물도록 위치를 옮긴다 */
|
|
function reformat(inp) {
|
|
var old = desc.get.call(inp); var pos = inp.selectionStart == null ? old.length : inp.selectionStart;
|
|
var keep = old.slice(0, pos).replace(/[^\d.\-]/g, '').length; // 커서 앞의 의미 있는 글자 수
|
|
var next = format(old); if (next === old) return; desc.set.call(inp, next);
|
|
var p = 0, n = 0; while (p < next.length && n < keep) { if (/[\d.\-]/.test(next.charAt(p))) n++; p++; }
|
|
try { inp.setSelectionRange(p, p); } catch (_) {}
|
|
}
|
|
function enhance(inp) {
|
|
if (!inp || inp.__wmNum || inp.tagName !== 'INPUT' || inp.type !== 'number' || inp.hasAttribute('data-plain')) return;
|
|
inp.__wmNum = true; var cur = desc.get.call(inp); var step = inp.getAttribute('step');
|
|
inp.type = 'text'; inp.setAttribute('inputmode', step && step !== 'any' && Number(step) % 1 === 0 && !(Number(inp.getAttribute('min')) < 0) ? 'numeric' : 'decimal');
|
|
inp.setAttribute('autocomplete', 'off'); inp.classList.add('num-input');
|
|
Object.defineProperty(inp, 'value', { configurable: true, get: function () { return clean(desc.get.call(this)); }, set: function (v) { desc.set.call(this, format(v)); } });
|
|
inp.value = cur; inp.addEventListener('input', function () { reformat(inp); });
|
|
}
|
|
function scan(root) { if (!root || !root.querySelectorAll) return; if (root.tagName === 'INPUT') enhance(root); var l = root.querySelectorAll('input[type="number"]'); for (var i = 0; i < l.length; i++) enhance(l[i]); }
|
|
function start() {
|
|
scan(document.body);
|
|
new MutationObserver(function (ms) { for (var i = 0; i < ms.length; i++) { var m = ms[i]; if (m.type === 'attributes') enhance(m.target); else for (var j = 0; j < m.addedNodes.length; j++) scan(m.addedNodes[j]); } })
|
|
.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['type'] });
|
|
}
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
|
|
window.WMNum = { format: format, clean: clean, enhance: enhance };
|
|
})();
|