text-input-guard 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -288,6 +288,26 @@ class SwapState {
288
288
  * @property {any} [detail] - 追加情報(制限値など)
289
289
  */
290
290
 
291
+ /**
292
+ * バリデーションがどの評価タイミングから呼び出されたかを表す識別子
293
+ *
294
+ * - "input" : 入力中の評価(inputイベントなど)
295
+ * - "commit" : 確定時の評価(blurなど)
296
+ *
297
+ * @typedef {"input"|"commit"} ValidateSource
298
+ */
299
+
300
+ /**
301
+ * バリデーション結果を表すオブジェクト
302
+ * - 各ルールの評価が完了したタイミングでコールバックに渡される
303
+ *
304
+ * @typedef {Object} ValidateResult
305
+ * @property {Guard} guard - この結果を発生させた Guard インスタンス
306
+ * @property {ValidateSource} source - 評価が実行されたタイミング(input / commit)
307
+ * @property {TigError[]} errors - 発生したエラー一覧
308
+ * @property {boolean} isValid - エラーが存在しない場合は true
309
+ */
310
+
291
311
  /**
292
312
  * setValue で設定できる値型
293
313
  * - number は String に変換して設定する
@@ -380,6 +400,7 @@ class SwapState {
380
400
  * @property {boolean} [warn] - 非対応ルールなどを console.warn するか
381
401
  * @property {string} [invalidClass] - エラー時に付けるclass名
382
402
  * @property {SeparateValueOptions} [separateValue] - 表示値と内部値の分離設定
403
+ * @property {(result: ValidateResult) => void} [onValidate] - 評価完了時の通知(input/commitごと)
383
404
  */
384
405
 
385
406
  /**
@@ -434,7 +455,7 @@ function warnLog(msg, warn) {
434
455
  function attach(element, options = {}) {
435
456
  const guard = new InputGuard(element, options);
436
457
  guard.init();
437
- return guard.toGuard();
458
+ return guard.getGuard();
438
459
  }
439
460
 
440
461
  /**
@@ -514,6 +535,12 @@ class InputGuard {
514
535
  */
515
536
  this.rules = Array.isArray(options.rules) ? options.rules : [];
516
537
 
538
+ /**
539
+ * attach時に登録されたバリデーション結果コールバック
540
+ * @type {(result: ValidateResult) => void | undefined}
541
+ */
542
+ this.onValidate = options.onValidate;
543
+
517
544
  /**
518
545
  * 実際に送信を担う要素(swap時は hidden(raw) 側)
519
546
  * swapしない場合は originalElement と同一
@@ -549,6 +576,12 @@ class InputGuard {
549
576
  */
550
577
  this.errors = [];
551
578
 
579
+ /**
580
+ * attach の返却値
581
+ * @type {Guard|null}
582
+ */
583
+ this._guard = null;
584
+
552
585
  // --------------------------------------------------
553
586
  // pipeline(フェーズごとのルール配列)
554
587
  // --------------------------------------------------
@@ -921,8 +954,8 @@ class InputGuard {
921
954
  * ルール実行に渡すコンテキストを作る(pushErrorで errors に積める)
922
955
  * @returns {GuardContext}
923
956
  */
924
- createCtx() {
925
- const snap = this.beforeInputSnapshot;
957
+ createCtx({ useSnapshot = true } = {}) {
958
+ const snap = useSnapshot ? this.beforeInputSnapshot : null;
926
959
  const inputType = snap?.inputType ?? "";
927
960
  const insertedText = snap?.insertedText ?? "";
928
961
 
@@ -1018,6 +1051,31 @@ class InputGuard {
1018
1051
  this.errors = [];
1019
1052
  }
1020
1053
 
1054
+ /**
1055
+ * バリデーション結果をコールバックへ通知する
1056
+ *
1057
+ * - attach() の options.onValidate が指定されている場合のみ呼び出す
1058
+ * - evaluateInput / evaluateCommit の評価完了時に実行される
1059
+ * - エラーが存在しない場合でも呼び出される
1060
+ *
1061
+ * @param {ValidateSource} source - 評価が実行されたタイミング("input" | "commit")
1062
+ * @returns {void}
1063
+ */
1064
+ notifyValidate(source) {
1065
+ if (!this.onValidate) {
1066
+ return;
1067
+ }
1068
+
1069
+ const errors = this.getErrors();
1070
+
1071
+ this.onValidate({
1072
+ guard: this.getGuard(),
1073
+ source,
1074
+ errors,
1075
+ isValid: errors.length === 0
1076
+ });
1077
+ }
1078
+
1021
1079
  /**
1022
1080
  * normalize.char フェーズを実行する(文字の正規化)
1023
1081
  * @param {string} value
@@ -1202,6 +1260,8 @@ class InputGuard {
1202
1260
  const current = display.value;
1203
1261
 
1204
1262
  const ctx = this.createCtx();
1263
+
1264
+ ctx.beforeText = "";
1205
1265
  ctx.afterText = current;
1206
1266
 
1207
1267
  let v = current;
@@ -1385,6 +1445,9 @@ class InputGuard {
1385
1445
  // 受理値は常にrawとして保存(revert先・getRawValueの一貫性)
1386
1446
  this.lastAcceptedValue = raw;
1387
1447
  this.lastAcceptedSelection = this.readSelection(display);
1448
+
1449
+ // コールバック関数処理
1450
+ this.notifyValidate("input");
1388
1451
  }
1389
1452
 
1390
1453
  /**
@@ -1402,10 +1465,11 @@ class InputGuard {
1402
1465
  this.revertRequest = null;
1403
1466
 
1404
1467
  const display = /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement);
1405
- const ctx = this.createCtx();
1468
+ const ctx = this.createCtx({ useSnapshot: false });
1406
1469
 
1407
1470
  // 1) raw候補(displayから取得)
1408
1471
  let raw = display.value;
1472
+ ctx.beforeText = "";
1409
1473
  ctx.afterText = raw;
1410
1474
 
1411
1475
  // 2) 正規化(rawとして扱う形に揃える)
@@ -1448,6 +1512,9 @@ class InputGuard {
1448
1512
  // 8) 受理値は raw を保持(revertやgetRawValueが安定する)
1449
1513
  this.lastAcceptedValue = raw;
1450
1514
  this.lastAcceptedSelection = this.readSelection(display);
1515
+
1516
+ // コールバック関数処理
1517
+ this.notifyValidate("commit");
1451
1518
  }
1452
1519
 
1453
1520
  /**
@@ -1523,8 +1590,13 @@ class InputGuard {
1523
1590
  * - InputGuard 自体を公開せず、最小の操作だけを渡す
1524
1591
  * @returns {Guard}
1525
1592
  */
1526
- toGuard() {
1527
- return {
1593
+ getGuard() {
1594
+ if (this._guard) {
1595
+ return this._guard;
1596
+ }
1597
+
1598
+ // ここで “this” を閉じ込めた関数群を一度だけ作る
1599
+ this._guard = {
1528
1600
  detach: () => this.detach(),
1529
1601
  isValid: () => this.isValid(),
1530
1602
  getErrors: () => this.getErrors(),
@@ -1536,6 +1608,8 @@ class InputGuard {
1536
1608
  commit: () => this.evaluateCommit(),
1537
1609
  setValue: (value, mode) => this.setValue(value, mode)
1538
1610
  };
1611
+
1612
+ return this._guard;
1539
1613
  }
1540
1614
  }
1541
1615
 
@@ -2165,8 +2239,8 @@ numeric.fromDataset = function fromDataset(dataset, _el) {
2165
2239
  * @property {boolean} [countLeadingZeros=true] - 整数部の先頭ゼロを桁数に含める
2166
2240
  * @property {"none"|"truncateLeft"|"truncateRight"|"clamp"} [fixIntOnBlur="none"] - blur時の整数部補正
2167
2241
  * @property {"none"|"truncate"|"round"} [fixFracOnBlur="none"] - blur時の小数部補正
2168
- * @property {"none"|"block"} [overflowInputInt="none"] - 入力中:整数部が最大桁を超える入力をブロックする
2169
- * @property {"none"|"block"} [overflowInputFrac="none"] - 入力中:小数部が最大桁を超える入力をブロックする
2242
+ * @property {"block"|"error"} [modeInt="block"] - 整数部が最大桁を超える入力の挙動
2243
+ * @property {"block"|"error"} [modeFrac="block"] - 小数部が最大桁を超える入力の挙動
2170
2244
  * @property {boolean} [forceFracOnBlur=false] - blur時に小数部を必ず表示(frac桁まで0埋め)
2171
2245
  */
2172
2246
 
@@ -2308,8 +2382,8 @@ function digits(options = {}) {
2308
2382
  countLeadingZeros: options.countLeadingZeros ?? true,
2309
2383
  fixIntOnBlur: options.fixIntOnBlur ?? "none",
2310
2384
  fixFracOnBlur: options.fixFracOnBlur ?? "none",
2311
- overflowInputInt: options.overflowInputInt ?? "none",
2312
- overflowInputFrac: options.overflowInputFrac ?? "none",
2385
+ modeInt: options.modeInt ?? "block",
2386
+ modeFrac: options.modeFrac ?? "block",
2313
2387
  forceFracOnBlur: options.forceFracOnBlur ?? false
2314
2388
  };
2315
2389
 
@@ -2336,7 +2410,7 @@ function digits(options = {}) {
2336
2410
  const intDigits = countIntDigits(intPart, opt.countLeadingZeros);
2337
2411
  if (intDigits > opt.int) {
2338
2412
  // 入力ブロック(int)
2339
- if (opt.overflowInputInt === "block") {
2413
+ if (opt.modeInt === "block") {
2340
2414
  ctx.requestRevert({
2341
2415
  reason: "digits.int_overflow",
2342
2416
  detail: { limit: opt.int, actual: intDigits }
@@ -2359,7 +2433,7 @@ function digits(options = {}) {
2359
2433
  const fracDigits = (fracPart ?? "").length;
2360
2434
  if (fracDigits > opt.frac) {
2361
2435
  // 入力ブロック(frac)
2362
- if (opt.overflowInputFrac === "block") {
2436
+ if (opt.modeFrac === "block") {
2363
2437
  ctx.requestRevert({
2364
2438
  reason: "digits.frac_overflow",
2365
2439
  detail: { limit: opt.frac, actual: fracDigits }
@@ -2476,8 +2550,8 @@ function digits(options = {}) {
2476
2550
  * - data-tig-rules-digits-count-leading-zeros -> dataset.tigRulesDigitsCountLeadingZeros
2477
2551
  * - data-tig-rules-digits-fix-int-on-blur -> dataset.tigRulesDigitsFixIntOnBlur
2478
2552
  * - data-tig-rules-digits-fix-frac-on-blur -> dataset.tigRulesDigitsFixFracOnBlur
2479
- * - data-tig-rules-digits-overflow-input-int -> dataset.tigRulesDigitsOverflowInputInt
2480
- * - data-tig-rules-digits-overflow-input-frac -> dataset.tigRulesDigitsOverflowInputFrac
2553
+ * - data-tig-rules-digits-mode-int -> dataset.tigRulesDigitsModeInt
2554
+ * - data-tig-rules-digits-mode-frac -> dataset.tigRulesDigitsModeFrac
2481
2555
  * - data-tig-rules-digits-force-frac-on-blur -> dataset.tigRulesDigitsForceFracOnBlur
2482
2556
  *
2483
2557
  * @param {DOMStringMap} dataset
@@ -2530,15 +2604,15 @@ digits.fromDataset = function fromDataset(dataset, _el) {
2530
2604
  options.fixFracOnBlur = fixFrac;
2531
2605
  }
2532
2606
 
2533
- // overflowInputInt / overflowInputFrac
2534
- const ovInt = parseDatasetEnum(dataset.tigRulesDigitsOverflowInputInt, ["none", "block"]);
2535
- if (ovInt != null) {
2536
- options.overflowInputInt = ovInt;
2607
+ // modeInt / modeFrac
2608
+ const modeInt = parseDatasetEnum(dataset.tigRulesDigitsModeInt, ["block", "error"]);
2609
+ if (modeInt != null) {
2610
+ options.modeInt = modeInt;
2537
2611
  }
2538
2612
 
2539
- const ovFrac = parseDatasetEnum(dataset.tigRulesDigitsOverflowInputFrac, ["none", "block"]);
2540
- if (ovFrac != null) {
2541
- options.overflowInputFrac = ovFrac;
2613
+ const modeFrac = parseDatasetEnum(dataset.tigRulesDigitsModeFrac, ["block", "error"]);
2614
+ if (modeFrac != null) {
2615
+ options.modeFrac = modeFrac;
2542
2616
  }
2543
2617
 
2544
2618
  // forceFracOnBlur
@@ -5768,13 +5842,10 @@ function kana(options = {}) {
5768
5842
  }
5769
5843
  s = Mojix.toKatakana(s);
5770
5844
  if (opt.target === "katakana-full") {
5771
- s = Mojix.toFullWidthSpace(s);
5772
5845
  s = Mojix.toFullWidthKana(s);
5773
5846
  } else if (opt.target === "katakana-half") {
5774
- s = Mojix.toHalfWidthSpace(s);
5775
5847
  s = Mojix.toHalfWidthKana(s);
5776
5848
  } else {
5777
- s = Mojix.toFullWidthSpace(s);
5778
5849
  s = Mojix.toFullWidthKana(s);
5779
5850
  s = Mojix.toHiragana(s);
5780
5851
  }
@@ -5918,6 +5989,23 @@ ascii.fromDataset = function fromDataset(dataset, _el) {
5918
5989
  */
5919
5990
 
5920
5991
 
5992
+ /**
5993
+ * filter ルールのオプション
5994
+ * - category は和集合で扱う(複数指定OK)
5995
+ * - allow は追加許可(和集合)
5996
+ * - deny は除外(差集合)
5997
+ *
5998
+ * allowed = (category の和集合 ∪ allow) − deny
5999
+ *
6000
+ * @typedef {Object} FilterRuleOptions
6001
+ * @property {"block"|"error"} [mode="block"] - 不要文字を入力中した場合の挙動
6002
+ * @property {FilterCategory[]} [category] - カテゴリ(配列)
6003
+ * @property {RegExp|string} [allow] - 追加で許可する正規表現(1文字にマッチさせる想定)
6004
+ * @property {string} [allowFlags] - allow が文字列のときの flags("iu" など。g/y は無視)
6005
+ * @property {RegExp|string} [deny] - 除外する正規表現(1文字にマッチさせる想定)
6006
+ * @property {string} [denyFlags] - deny が文字列のときの flags("iu" など。g/y は無視)
6007
+ */
6008
+
5921
6009
  /**
5922
6010
  * filter ルールのカテゴリ名
5923
6011
  *
@@ -5956,28 +6044,6 @@ const FILTER_CATEGORIES = [
5956
6044
  "single-codepoint-only"
5957
6045
  ];
5958
6046
 
5959
- /**
5960
- * filter ルールの動作モード
5961
- * @typedef {"drop"|"error"} FilterMode
5962
- */
5963
-
5964
- /**
5965
- * filter ルールのオプション
5966
- * - category は和集合で扱う(複数指定OK)
5967
- * - allow は追加許可(和集合)
5968
- * - deny は除外(差集合)
5969
- *
5970
- * allowed = (category の和集合 ∪ allow) − deny
5971
- *
5972
- * @typedef {Object} FilterRuleOptions
5973
- * @property {FilterMode} [mode="drop"] - drop: 不要文字を削除 / error: 削除せずエラーを積む
5974
- * @property {FilterCategory[]} [category] - カテゴリ(配列)
5975
- * @property {RegExp|string} [allow] - 追加で許可する正規表現(1文字にマッチさせる想定)
5976
- * @property {string} [allowFlags] - allow が文字列のときの flags("iu" など。g/y は無視)
5977
- * @property {RegExp|string} [deny] - 除外する正規表現(1文字にマッチさせる想定)
5978
- * @property {string} [denyFlags] - deny が文字列のときの flags("iu" など。g/y は無視)
5979
- */
5980
-
5981
6047
  /**
5982
6048
  * /g や /y は lastIndex の罠があるので除去して使う
5983
6049
  * @param {string} flags
@@ -6179,16 +6245,13 @@ const scanByAllowed = function (value, isAllowed, maxInvalidChars = 20) {
6179
6245
 
6180
6246
  /**
6181
6247
  * filter ルールを生成する
6182
- * - mode="drop": 不要文字を落とすだけ
6183
- * - mode="error": 文字は落とさず validate でエラーを積む
6184
- *
6185
6248
  * @param {FilterRuleOptions} [options]
6186
6249
  * @returns {Rule}
6187
6250
  */
6188
6251
  function filter(options = {}) {
6189
6252
  /** @type {FilterRuleOptions} */
6190
6253
  const opt = {
6191
- mode: options.mode ?? "drop",
6254
+ mode: options.mode ?? "block",
6192
6255
  category: options.category ?? [],
6193
6256
  allow: options.allow,
6194
6257
  allowFlags: options.allowFlags,
@@ -6273,7 +6336,7 @@ function filter(options = {}) {
6273
6336
  *
6274
6337
  * 対応する data 属性(dataset 名)
6275
6338
  * - data-tig-rules-filter -> dataset.tigRulesFilter
6276
- * - data-tig-rules-filter-mode -> dataset.tigRulesFilterMode ("drop"|"error")
6339
+ * - data-tig-rules-filter-mode -> dataset.tigRulesFilterMode
6277
6340
  * - data-tig-rules-filter-category -> dataset.tigRulesFilterCategory ("a,b,c")
6278
6341
  * - data-tig-rules-filter-allow -> dataset.tigRulesFilterAllow
6279
6342
  * - data-tig-rules-filter-allow-flags -> dataset.tigRulesFilterAllowFlags
@@ -6292,7 +6355,7 @@ filter.fromDataset = function fromDataset(dataset, _el) {
6292
6355
  /** @type {FilterRuleOptions} */
6293
6356
  const options = {};
6294
6357
 
6295
- const mode = parseDatasetEnum(dataset.tigRulesFilterMode, ["drop", "error"]);
6358
+ const mode = parseDatasetEnum(dataset.tigRulesFilterMode, ["block", "error"]);
6296
6359
  if (mode != null) {
6297
6360
  options.mode = mode;
6298
6361
  }
@@ -6348,11 +6411,8 @@ filter.fromDataset = function fromDataset(dataset, _el) {
6348
6411
  * length ルールのオプション
6349
6412
  * @typedef {Object} LengthRuleOptions
6350
6413
  * @property {number} [max] - 最大長(グラフェム数)。未指定なら制限なし
6351
- * @property {"block"|"error"} [overflowInput="block"] - 入力中に最大長を超えたときの挙動
6414
+ * @property {"block"|"error"} [mode="block"] - 入力中に最大長を超えたときの挙動
6352
6415
  * @property {"grapheme"|"utf-16"|"utf-32"} [unit="grapheme"] - 長さの単位
6353
- *
6354
- * block : 最大長を超える部分を切る
6355
- * error : エラーを積むだけ(値は変更しない)
6356
6416
  */
6357
6417
 
6358
6418
  /**
@@ -6386,7 +6446,7 @@ const getTextLengthByUnit = function(text, unit) {
6386
6446
  * @param {number} max
6387
6447
  * @returns {string}
6388
6448
  */
6389
- const cutTextByUnit = function(text, unit, max) {
6449
+ const cutTextByUnit$1 = function(text, unit, max) {
6390
6450
  /**
6391
6451
  * グラフェムの配列
6392
6452
  * @type {Grapheme[]}
@@ -6446,13 +6506,13 @@ const cutTextByUnit = function(text, unit, max) {
6446
6506
  * @returns {string} 追加するテキストを切ったもの(切る必要がない場合は insertedText をそのまま返す)
6447
6507
  */
6448
6508
  const cutLength = function(beforeText, insertedText, unit, max) {
6449
- const orgLen = getTextLengthByUnit(beforeText, unit);
6509
+ const beforeTextLen = getTextLengthByUnit(beforeText, unit);
6450
6510
 
6451
6511
  // すでに最大長を超えている場合は追加のテキストを全て切る
6452
- if (orgLen >= max) { return ""; }
6512
+ if (beforeTextLen >= max) { return ""; }
6453
6513
 
6454
- const addLen = getTextLengthByUnit(insertedText, unit);
6455
- const totalLen = orgLen + addLen;
6514
+ const insertedTextLen = getTextLengthByUnit(insertedText, unit);
6515
+ const totalLen = beforeTextLen + insertedTextLen;
6456
6516
 
6457
6517
  if (totalLen <= max) {
6458
6518
  // 今回の追加で範囲内に収まるなら何もしない
@@ -6460,8 +6520,8 @@ const cutLength = function(beforeText, insertedText, unit, max) {
6460
6520
  }
6461
6521
 
6462
6522
  // 超える場合は追加のテキストを切る
6463
- const allowedAddLen = max - orgLen;
6464
- return cutTextByUnit(insertedText, unit, allowedAddLen);
6523
+ const allowedAddLen = max - beforeTextLen;
6524
+ return cutTextByUnit$1(insertedText, unit, allowedAddLen);
6465
6525
  };
6466
6526
 
6467
6527
  /**
@@ -6473,7 +6533,7 @@ function length(options = {}) {
6473
6533
  /** @type {LengthRuleOptions} */
6474
6534
  const opt = {
6475
6535
  max: typeof options.max === "number" ? options.max : undefined,
6476
- overflowInput: options.overflowInput ?? "block",
6536
+ mode: options.mode ?? "block",
6477
6537
  unit: options.unit ?? "grapheme"
6478
6538
  };
6479
6539
 
@@ -6483,7 +6543,7 @@ function length(options = {}) {
6483
6543
 
6484
6544
  normalizeChar(value, ctx) {
6485
6545
  // block 以外は何もしない
6486
- if (opt.overflowInput !== "block") {
6546
+ if (opt.mode !== "block") {
6487
6547
  return value;
6488
6548
  }
6489
6549
  // max 未指定なら制限なし
@@ -6491,20 +6551,14 @@ function length(options = {}) {
6491
6551
  return value;
6492
6552
  }
6493
6553
 
6494
- const beforeText = ctx.beforeText;
6495
- const insertedText = ctx.insertedText;
6496
- if (insertedText === "") {
6497
- return value;
6498
- }
6499
-
6500
- const cutText = cutLength(beforeText, insertedText, opt.unit, opt.max);
6554
+ const cutText = cutLength(ctx.beforeText, value, opt.unit, opt.max);
6501
6555
  return cutText;
6502
6556
  },
6503
6557
 
6504
6558
  validate(value, ctx) {
6505
6559
  // error 以外は何もしない
6506
- if (opt.overflowInput !== "error") {
6507
- return value;
6560
+ if (opt.mode !== "error") {
6561
+ return;
6508
6562
  }
6509
6563
  // max 未指定なら制限なし
6510
6564
  if (typeof opt.max !== "number") {
@@ -6517,7 +6571,7 @@ function length(options = {}) {
6517
6571
  code: "length.max_overflow",
6518
6572
  rule: "length",
6519
6573
  phase: "validate",
6520
- detail: { max: opt.max, actual: len }
6574
+ detail: { limit: opt.max, actual: len }
6521
6575
  });
6522
6576
  }
6523
6577
  }
@@ -6532,7 +6586,7 @@ function length(options = {}) {
6532
6586
  * 対応する data 属性(dataset 名)
6533
6587
  * - data-tig-rules-length -> dataset.tigRulesLength
6534
6588
  * - data-tig-rules-length-max -> dataset.tigRulesLengthMax
6535
- * - data-tig-rules-length-overflow-input -> dataset.tigRulesLengthOverflowInput
6589
+ * - data-tig-rules-length-mode -> dataset.tigRulesLengthMode
6536
6590
  * - data-tig-rules-length-unit -> dataset.tigRulesLengthUnit
6537
6591
  *
6538
6592
  * @param {DOMStringMap} dataset
@@ -6553,12 +6607,9 @@ length.fromDataset = function fromDataset(dataset, _el) {
6553
6607
  options.max = max;
6554
6608
  }
6555
6609
 
6556
- const overflowInput = parseDatasetEnum(
6557
- dataset.tigRulesLengthOverflowInput,
6558
- ["block", "error"]
6559
- );
6560
- if (overflowInput != null) {
6561
- options.overflowInput = overflowInput;
6610
+ const mode = parseDatasetEnum(dataset.tigRulesLengthMode, ["block", "error"]);
6611
+ if (mode != null) {
6612
+ options.mode = mode;
6562
6613
  }
6563
6614
 
6564
6615
  const unit = parseDatasetEnum(
@@ -6572,6 +6623,364 @@ length.fromDataset = function fromDataset(dataset, _el) {
6572
6623
  return length(options);
6573
6624
  };
6574
6625
 
6626
+ /**
6627
+ * The script is part of TextInputGuard.
6628
+ *
6629
+ * AUTHOR:
6630
+ * natade-jp (https://github.com/natade-jp)
6631
+ *
6632
+ * LICENSE:
6633
+ * The MIT license https://opensource.org/licenses/MIT
6634
+ */
6635
+
6636
+
6637
+ /**
6638
+ * width ルールのオプション
6639
+ * @typedef {Object} WidthRuleOptions
6640
+ * @property {number} [max] - 最大長(全角は2, 半角は1)
6641
+ * @property {"block"|"error"} [mode="block"] - 入力中に最大長を超えたときの挙動
6642
+ */
6643
+
6644
+ /**
6645
+ * width ルールを生成する
6646
+ * @param {WidthRuleOptions} [options]
6647
+ * @returns {Rule}
6648
+ */
6649
+ function width(options = {}) {
6650
+ /** @type {WidthRuleOptions} */
6651
+ const opt = {
6652
+ max: typeof options.max === "number" ? options.max : undefined,
6653
+ mode: options.mode ?? "block"
6654
+ };
6655
+
6656
+ return {
6657
+ name: "length",
6658
+ targets: ["input", "textarea"],
6659
+
6660
+ normalizeChar(value, ctx) {
6661
+ // block 以外は何もしない
6662
+ if (opt.mode !== "block") {
6663
+ return value;
6664
+ }
6665
+ // max 未指定なら制限なし
6666
+ if (typeof opt.max !== "number") {
6667
+ return value;
6668
+ }
6669
+
6670
+ /*
6671
+ * 指定したテキストを切り出す
6672
+ * - 0幅 ... グラフェムを構成する要素
6673
+ * (結合文字, 異体字セレクタ, スキントーン修飾子,
6674
+ * Tag Sequence 構成文字, ZWSP, ZWNJ, ZWJ, WJ)
6675
+ * - 1幅 ... ASCII文字, 半角カタカナ, Regional Indicator(単体)
6676
+ * - 2幅 ... 上記以外
6677
+ * ※ Unicode が配布してる EastAsianWidth.txt は使用していません。
6678
+ * (目的としては Shift_JIS 時代の半角全角だと思われるため)
6679
+ */
6680
+ const cutText = Mojix.cutTextForWidth(value, 0, opt.max);
6681
+ return cutText;
6682
+ },
6683
+
6684
+ validate(value, ctx) {
6685
+ // error 以外は何もしない
6686
+ if (opt.mode !== "error") {
6687
+ return;
6688
+ }
6689
+ // max 未指定なら制限なし
6690
+ if (typeof opt.max !== "number") {
6691
+ return;
6692
+ }
6693
+
6694
+ /*
6695
+ * 指定したテキストの横幅を半角/全角でカウント
6696
+ * - 0幅 ... グラフェムを構成する要素
6697
+ * (結合文字, 異体字セレクタ, スキントーン修飾子,
6698
+ * Tag Sequence 構成文字, ZWSP, ZWNJ, ZWJ, WJ)
6699
+ * - 1幅 ... ASCII文字, 半角カタカナ, Regional Indicator(単体)
6700
+ * - 2幅 ... 上記以外
6701
+ * ※ Unicode が配布してる EastAsianWidth.txt は使用していません。
6702
+ * (目的としては Shift_JIS 時代の半角全角だと思われるため)
6703
+ */
6704
+ const len = Mojix.getWidth(value);
6705
+ if (len > opt.max) {
6706
+ ctx.pushError({
6707
+ code: "width.max_overflow",
6708
+ rule: "width",
6709
+ phase: "validate",
6710
+ detail: { limit: opt.max, actual: len }
6711
+ });
6712
+ }
6713
+ }
6714
+ };
6715
+ }
6716
+
6717
+ /**
6718
+ * datasetから length ルールを生成する
6719
+ * - data-tig-rules-length が無ければ null
6720
+ * - オプションは data-tig-rules-length-xxx から読む
6721
+ *
6722
+ * 対応する data 属性(dataset 名)
6723
+ * - data-tig-rules-length -> dataset.tigRulesWidth
6724
+ * - data-tig-rules-length-max -> dataset.tigRulesWidthMax
6725
+ * - data-tig-rules-length-mode -> dataset.tigRulesWidthMode
6726
+ *
6727
+ * @param {DOMStringMap} dataset
6728
+ * @param {HTMLInputElement|HTMLTextAreaElement} _el
6729
+ * @returns {Rule|null}
6730
+ */
6731
+ width.fromDataset = function fromDataset(dataset, _el) {
6732
+ // ON判定
6733
+ if (dataset.tigRulesWidth == null) {
6734
+ return null;
6735
+ }
6736
+
6737
+ /** @type {WidthRuleOptions} */
6738
+ const options = {};
6739
+
6740
+ const max = parseDatasetNumber(dataset.tigRulesWidthMax);
6741
+ if (max != null) {
6742
+ options.max = max;
6743
+ }
6744
+
6745
+ const mode = parseDatasetEnum(dataset.tigRulesWidthMode, ["block", "error"]);
6746
+ if (mode != null) {
6747
+ options.mode = mode;
6748
+ }
6749
+
6750
+ return width(options);
6751
+ };
6752
+
6753
+ /**
6754
+ * The script is part of TextInputGuard.
6755
+ *
6756
+ * AUTHOR:
6757
+ * natade-jp (https://github.com/natade-jp)
6758
+ *
6759
+ * LICENSE:
6760
+ * The MIT license https://opensource.org/licenses/MIT
6761
+ */
6762
+
6763
+
6764
+ /**
6765
+ * bytes ルールのオプション
6766
+ * @typedef {Object} BytesRuleOptions
6767
+ * @property {number} [max] - 最大長(グラフェム数)。未指定なら制限なし
6768
+ * @property {"block"|"error"} [mode="block"] - 入力中に最大長を超えたときの挙動
6769
+ * @property {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} [unit="utf-8"] - サイズの単位(sjis系を使用する場合はfilterも必須)
6770
+ */
6771
+
6772
+ /**
6773
+ * グラフェム(1グラフェムは、UTF-32の配列)
6774
+ * @typedef {number[]} Grapheme
6775
+ */
6776
+
6777
+ /**
6778
+ * グラフェム/UTF-16コード単位/UTF-32コード単位の長さを調べる
6779
+ * @param {string} text
6780
+ * @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
6781
+ * @returns {number}
6782
+ */
6783
+ const getTextBytesByUnit = function(text, unit) {
6784
+ if (text.length === 0) {
6785
+ return 0;
6786
+ }
6787
+ if (unit === "utf-8") {
6788
+ return Mojix.toUTF8Array(text).length;
6789
+ } else if (unit === "utf-16") {
6790
+ return Mojix.toUTF16Array(text).length * 2;
6791
+ } else if (unit === "utf-32") {
6792
+ return Mojix.toUTF32Array(text).length * 4;
6793
+ } else if (unit === "sjis" || unit === "cp932") {
6794
+ return Mojix.encode(text, "Shift_JIS").length;
6795
+ } else {
6796
+ // ここには来ない
6797
+ throw new Error(`Invalid unit: ${unit}`);
6798
+ }
6799
+ };
6800
+
6801
+ /**
6802
+ * グラフェム/UTF-16コード単位/UTF-32コード単位でテキストを切る
6803
+ * @param {string} text
6804
+ * @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
6805
+ * @param {number} max
6806
+ * @returns {string}
6807
+ */
6808
+ const cutTextByUnit = function(text, unit, max) {
6809
+ /**
6810
+ * グラフェムの配列
6811
+ * @type {Grapheme[]}
6812
+ */
6813
+ const graphemeArray = Mojix.toMojiArrayFromString(text);
6814
+
6815
+ /**
6816
+ * 現在の位置
6817
+ */
6818
+ let count = 0;
6819
+
6820
+ /**
6821
+ * グラフェムの配列(出力用)
6822
+ * @type {Grapheme[]}
6823
+ */
6824
+ const outputGraphemeArray = [];
6825
+
6826
+ for (let i = 0; i < graphemeArray.length; i++) {
6827
+ const g = graphemeArray[i];
6828
+
6829
+ // 1グラフェムあたりの長さ
6830
+ let byteCount = 0;
6831
+ if (unit === "utf-8") {
6832
+ byteCount = Mojix.toUTF8Array(Mojix.toStringFromMojiArray([g])).length;
6833
+ } else if (unit === "utf-16") {
6834
+ byteCount = Mojix.toUTF16Array(Mojix.toStringFromMojiArray([g])).length * 2;
6835
+ } else if (unit === "utf-32") {
6836
+ byteCount = Mojix.toUTF32Array(Mojix.toStringFromMojiArray([g])).length * 4;
6837
+ } else if (unit === "sjis" || unit === "cp932") {
6838
+ byteCount = Mojix.encode(Mojix.toStringFromMojiArray([g]), "Shift_JIS").length;
6839
+ }
6840
+
6841
+ if (count + byteCount > max) {
6842
+ // 空配列を渡すとNUL文字を返すため、空配列のときは空文字を返す
6843
+ if (outputGraphemeArray.length === 0) {
6844
+ return "";
6845
+ }
6846
+ // 超える前の位置で文字列化して返す
6847
+ return Mojix.toStringFromMojiArray(outputGraphemeArray);
6848
+ }
6849
+
6850
+ count += byteCount;
6851
+ outputGraphemeArray.push(g);
6852
+ }
6853
+
6854
+ // 全部入るなら元の text を返す
6855
+ return text;
6856
+ };
6857
+
6858
+ /**
6859
+ * 元のテキストと追加のテキストの合計が max を超える場合、追加のテキストを切って合計が max に収まるようにする
6860
+ * @param {string} beforeText 元のテキスト
6861
+ * @param {string} insertedText 追加するテキスト
6862
+ * @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
6863
+ * @param {number} max
6864
+ * @returns {string} 追加するテキストを切ったもの(切る必要がない場合は insertedText をそのまま返す)
6865
+ */
6866
+ const cutBytes = function(beforeText, insertedText, unit, max) {
6867
+ const beforeTextLen = getTextBytesByUnit(beforeText, unit);
6868
+
6869
+ // すでに最大長を超えている場合は追加のテキストを全て切る
6870
+ if (beforeTextLen >= max) { return ""; }
6871
+
6872
+ const insertedTextLen = getTextBytesByUnit(insertedText, unit);
6873
+ const totalLen = beforeTextLen + insertedTextLen;
6874
+
6875
+ if (totalLen <= max) {
6876
+ // 今回の追加で範囲内に収まるなら何もしない
6877
+ return insertedText;
6878
+ }
6879
+
6880
+ // 超える場合は追加のテキストを切る
6881
+ const allowedAddLen = max - beforeTextLen;
6882
+ return cutTextByUnit(insertedText, unit, allowedAddLen);
6883
+ };
6884
+
6885
+ /**
6886
+ * bytes ルールを生成する
6887
+ * @param {BytesRuleOptions} [options]
6888
+ * @returns {Rule}
6889
+ */
6890
+ function bytes(options = {}) {
6891
+ /** @type {BytesRuleOptions} */
6892
+ const opt = {
6893
+ max: typeof options.max === "number" ? options.max : undefined,
6894
+ mode: options.mode ?? "block",
6895
+ unit: options.unit ?? "utf-8"
6896
+ };
6897
+
6898
+ return {
6899
+ name: "bytes",
6900
+ targets: ["input", "textarea"],
6901
+
6902
+ normalizeChar(value, ctx) {
6903
+ // block 以外は何もしない
6904
+ if (opt.mode !== "block") {
6905
+ return value;
6906
+ }
6907
+ // max 未指定なら制限なし
6908
+ if (typeof opt.max !== "number") {
6909
+ return value;
6910
+ }
6911
+
6912
+ const cutText = cutBytes(ctx.beforeText, value, opt.unit, opt.max);
6913
+ return cutText;
6914
+ },
6915
+
6916
+ validate(value, ctx) {
6917
+ // error 以外は何もしない
6918
+ if (opt.mode !== "error") {
6919
+ return;
6920
+ }
6921
+ // max 未指定なら制限なし
6922
+ if (typeof opt.max !== "number") {
6923
+ return;
6924
+ }
6925
+
6926
+ const len = getTextBytesByUnit(value, opt.unit);
6927
+ if (len > opt.max) {
6928
+ ctx.pushError({
6929
+ code: "bytes.max_overflow",
6930
+ rule: "bytes",
6931
+ phase: "validate",
6932
+ detail: { max: opt.max, actual: len }
6933
+ });
6934
+ }
6935
+ }
6936
+ };
6937
+ }
6938
+
6939
+ /**
6940
+ * datasetから bytes ルールを生成する
6941
+ * - data-tig-rules-bytes が無ければ null
6942
+ * - オプションは data-tig-rules-bytes-xxx から読む
6943
+ *
6944
+ * 対応する data 属性(dataset 名)
6945
+ * - data-tig-rules-bytes -> dataset.tigRulesBytes
6946
+ * - data-tig-rules-bytes-max -> dataset.tigRulesBytesMax
6947
+ * - data-tig-rules-bytes-mode -> dataset.tigRulesBytesMode
6948
+ * - data-tig-rules-bytes-unit -> dataset.tigRulesBytesUnit
6949
+ *
6950
+ * @param {DOMStringMap} dataset
6951
+ * @param {HTMLInputElement|HTMLTextAreaElement} _el
6952
+ * @returns {Rule|null}
6953
+ */
6954
+ bytes.fromDataset = function fromDataset(dataset, _el) {
6955
+ // ON判定
6956
+ if (dataset.tigRulesBytes == null) {
6957
+ return null;
6958
+ }
6959
+
6960
+ /** @type {BytesRuleOptions} */
6961
+ const options = {};
6962
+
6963
+ const max = parseDatasetNumber(dataset.tigRulesBytesMax);
6964
+ if (max != null) {
6965
+ options.max = max;
6966
+ }
6967
+
6968
+ const mode = parseDatasetEnum(dataset.tigRulesBytesMode, ["block", "error"]);
6969
+ if (mode != null) {
6970
+ options.mode = mode;
6971
+ }
6972
+
6973
+ const unit = parseDatasetEnum(
6974
+ dataset.tigRulesBytesUnit,
6975
+ ["utf-8", "utf-16", "utf-32", "sjis", "cp932"]
6976
+ );
6977
+ if (unit != null) {
6978
+ options.unit = unit;
6979
+ }
6980
+
6981
+ return bytes(options);
6982
+ };
6983
+
6575
6984
  /**
6576
6985
  * The script is part of TextInputGuard.
6577
6986
  *
@@ -6831,6 +7240,8 @@ const auto = new InputGuardAutoAttach(attach, [
6831
7240
  { name: "ascii", fromDataset: ascii.fromDataset },
6832
7241
  { name: "filter", fromDataset: filter.fromDataset },
6833
7242
  { name: "length", fromDataset: length.fromDataset },
7243
+ { name: "width", fromDataset: width.fromDataset },
7244
+ { name: "bytes", fromDataset: bytes.fromDataset },
6834
7245
  { name: "prefix", fromDataset: prefix.fromDataset },
6835
7246
  { name: "suffix", fromDataset: suffix.fromDataset },
6836
7247
  { name: "trim", fromDataset: trim.fromDataset }
@@ -6853,6 +7264,8 @@ const rules = {
6853
7264
  ascii,
6854
7265
  filter,
6855
7266
  length,
7267
+ width,
7268
+ bytes,
6856
7269
  prefix,
6857
7270
  suffix,
6858
7271
  trim
@@ -6860,16 +7273,17 @@ const rules = {
6860
7273
 
6861
7274
  /**
6862
7275
  * バージョン(ビルド時に置換したいならここを差し替える)
6863
- * 例: rollup replace で ""0.1.5"" を package.json の version に置換
7276
+ * 例: rollup replace で ""0.2.0"" を package.json の version に置換
6864
7277
  */
6865
7278
  // @ts-ignore
6866
7279
  // eslint-disable-next-line no-undef
6867
- const version = "0.1.5" ;
7280
+ const version = "0.2.0" ;
6868
7281
 
6869
7282
  exports.ascii = ascii;
6870
7283
  exports.attach = attach;
6871
7284
  exports.attachAll = attachAll;
6872
7285
  exports.autoAttach = autoAttach;
7286
+ exports.bytes = bytes;
6873
7287
  exports.comma = comma;
6874
7288
  exports.digits = digits;
6875
7289
  exports.filter = filter;
@@ -6881,3 +7295,4 @@ exports.rules = rules;
6881
7295
  exports.suffix = suffix;
6882
7296
  exports.trim = trim;
6883
7297
  exports.version = version;
7298
+ exports.width = width;