yamori

有給休暇計算を主目的とした簡易勤怠管理システム

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>
// SPDX-License-Identifier: AGPL-3.0-only

export interface UIComponent<
	TagName extends string,
	Element extends typeof YamoriElement,
> {
	readonly tagName: TagName;
	readonly constructor: Element;
	register(): boolean;
}

export interface UIComponentBuilder<
	TagName extends string,
	Element extends typeof YamoriElement,
> {
	readonly tagName: TagName;
	readonly constructor: Element;
	readonly dependencies?: readonly UIComponent<string, typeof YamoriElement>[];
}

/**
 * CustomElement を使いやすい I/F にラップする。
 * 渡された CustomElement は自動的に `nonUAMixin` が適用され、
 * `:is(:state(nonua), :--nonua, [nonua])` でクエリできるようになる。
 */
export function wrapElement<
	TagName extends string,
	Element extends typeof YamoriElement,
>({
	tagName,
	constructor,
	dependencies = [],
}: UIComponentBuilder<TagName, Element>): UIComponent<TagName, Element> {
	return {
		tagName,
		constructor,
		register() {
			for (const dependency of dependencies) {
				dependency.register();
			}

			if (customElements.get(tagName)) {
				return false;
			}

			customElements.define(tagName, constructor);
			return true;
		},
	};
}

/**
 * このプロジェクトで定義する全ての CustomElement の基底クラス。
 *
 * CSS では未だにビルトインのタグとユーザ定義のタグをセレクトできず、また UA のスタイルを
 * 無効化する仕様も存在しない。そのため、UA が勝手に定義したスタイルを削除するためには、
 * 1. Custom Element に識別用のマーカーを設定
 * 2. マーカーのついていないものを全てリセット
 * とするしかない。この手法だと 3rd party の要素までもリセットされてしまうが、
 * このプロジェクトにおいては 3rd party の Custom Element は利用しないためこの手法で
 * 問題はない。
 *
 * 付与するマーカーは以下の通り:
 *
 * A) CustomStateSet の `nonua` ステート (Evergreen Browser)
 * B) CustomStateSet の `--nonua` ステート (Chromium v90 <= x < v125)
 * C) `x--_nonua` 属性
 *
 * 全てのマーカーに対応するには以下のようなセレクタが必要。
 *
 * ```css
 * :is(:state(nonua), :--nonua, [nonua])
 * ```
 */
export class YamoriElement extends HTMLElement {
	internals: ElementInternals;

	setCustomState(name: string): void {
		const internals = this.internals;

		if (internals && internals.states) {
			// https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet#compatibility_with_dashed-ident_syntax
			try {
				internals.states.add(name);
			} catch {
				internals.states.add(`--${name}`);
			}
		} else {
			this.setAttribute(`--_${name}`, "");
		}
	}

	removeCustomState(name: string): void {
		const internals = this.internals;

		if (internals && internals.states) {
			try {
				internals.states.delete(name);
			} catch {
				internals.states.delete(`--${name}`);
			}
		} else {
			this.removeAttribute(`--_${name}`);
		}
	}

	constructor() {
		super();

		this.internals = this.attachInternals();

		this.setCustomState("nonua");
	}
}