@digitalwalletcorp/vue-core 1.2.1 → 1.3.1

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.
Files changed (188) hide show
  1. package/README.md +79 -3
  2. package/lib/accordion-fieldset.d.cts +14 -4
  3. package/lib/accordion-fieldset.d.ts +14 -4
  4. package/lib/accordion-section.d.cts +20 -10
  5. package/lib/accordion-section.d.ts +20 -10
  6. package/lib/components-BuTJ1wX8.js +26 -0
  7. package/lib/components-CKql5lWg.cjs +1 -0
  8. package/lib/components.d.cts +10 -0
  9. package/lib/components.d.ts +10 -0
  10. package/lib/copy-button.d.cts +34 -0
  11. package/lib/copy-button.d.ts +34 -0
  12. package/lib/dark-mode-selector.d.cts +32 -0
  13. package/lib/dark-mode-selector.d.ts +32 -0
  14. package/lib/data-table.d.cts +29 -0
  15. package/lib/data-table.d.ts +29 -0
  16. package/lib/help-button.d.cts +39 -0
  17. package/lib/help-button.d.ts +39 -0
  18. package/lib/help-popper.d.cts +39 -0
  19. package/lib/help-popper.d.ts +39 -0
  20. package/lib/helpers/data-table-csv.d.cts +17 -0
  21. package/lib/helpers/data-table-csv.d.ts +17 -0
  22. package/lib/helpers/show-floating-component.d.cts +86 -0
  23. package/lib/helpers/show-floating-component.d.ts +86 -0
  24. package/lib/helpers/show-modal-dialog.d.cts +73 -0
  25. package/lib/helpers/show-modal-dialog.d.ts +73 -0
  26. package/lib/helpers/show-overlay-box.d.cts +53 -0
  27. package/lib/helpers/show-overlay-box.d.ts +53 -0
  28. package/lib/helpers/show-screen-loading.d.cts +20 -0
  29. package/lib/helpers/show-screen-loading.d.ts +20 -0
  30. package/lib/helpers/show-snackbar.d.cts +47 -0
  31. package/lib/helpers/show-snackbar.d.ts +47 -0
  32. package/lib/hourglass-loading.d.cts +1 -1
  33. package/lib/hourglass-loading.d.ts +1 -1
  34. package/lib/index.cjs +1 -1
  35. package/lib/index.d.cts +34 -0
  36. package/lib/index.d.ts +34 -0
  37. package/lib/index.js +445 -18
  38. package/lib/internal/anchored-popover.d.cts +35 -0
  39. package/lib/internal/anchored-popover.d.ts +35 -0
  40. package/lib/internal/anchored-position.d.cts +50 -0
  41. package/lib/internal/anchored-position.d.ts +50 -0
  42. package/lib/internal/data-table-controls.d.cts +42 -0
  43. package/lib/internal/data-table-controls.d.ts +42 -0
  44. package/lib/internal/data-table-value.d.cts +32 -0
  45. package/lib/internal/data-table-value.d.ts +32 -0
  46. package/lib/internal/floating-component.d.cts +52 -0
  47. package/lib/internal/floating-component.d.ts +52 -0
  48. package/lib/internal/icon-popper.d.cts +32 -0
  49. package/lib/internal/icon-popper.d.ts +32 -0
  50. package/lib/internal/mount-window.d.cts +39 -0
  51. package/lib/internal/mount-window.d.ts +39 -0
  52. package/lib/internal/movable-window.d.cts +55 -0
  53. package/lib/internal/movable-window.d.ts +55 -0
  54. package/lib/internal/opened-overlay-boxes.d.cts +2 -0
  55. package/lib/internal/opened-overlay-boxes.d.ts +2 -0
  56. package/lib/internal/pagination.d.cts +22 -0
  57. package/lib/internal/pagination.d.ts +22 -0
  58. package/lib/internal/snackbar-container.d.cts +20 -0
  59. package/lib/internal/snackbar-container.d.ts +20 -0
  60. package/lib/internal/snackbar-item.d.cts +17 -0
  61. package/lib/internal/snackbar-item.d.ts +17 -0
  62. package/lib/internal/tab-group-context.d.cts +4 -2
  63. package/lib/internal/tab-group-context.d.ts +4 -2
  64. package/lib/internal/use-movable-window.d.cts +44 -0
  65. package/lib/internal/use-movable-window.d.ts +44 -0
  66. package/lib/internal/window-order.d.cts +6 -0
  67. package/lib/internal/window-order.d.ts +6 -0
  68. package/lib/intersection-lazy-component.d.cts +32 -0
  69. package/lib/intersection-lazy-component.d.ts +32 -0
  70. package/lib/label-button.d.cts +2 -2
  71. package/lib/label-button.d.ts +2 -2
  72. package/lib/label-icon.d.cts +2 -2
  73. package/lib/label-icon.d.ts +2 -2
  74. package/lib/modal-dialog.d.cts +67 -0
  75. package/lib/modal-dialog.d.ts +67 -0
  76. package/lib/nuxt.cjs +1 -1
  77. package/lib/nuxt.js +1 -1
  78. package/lib/overlay-box.d.cts +52 -0
  79. package/lib/overlay-box.d.ts +52 -0
  80. package/lib/page-section.d.cts +2 -2
  81. package/lib/page-section.d.ts +2 -2
  82. package/lib/page-title.d.cts +2 -2
  83. package/lib/page-title.d.ts +2 -2
  84. package/lib/register.cjs +1 -1
  85. package/lib/register.js +1 -1
  86. package/lib/screen-loading.d.cts +7 -0
  87. package/lib/screen-loading.d.ts +7 -0
  88. package/lib/style.css +1 -1
  89. package/lib/tab-content.d.cts +5 -5
  90. package/lib/tab-content.d.ts +5 -5
  91. package/lib/tab-group.d.cts +7 -10
  92. package/lib/tab-group.d.ts +7 -10
  93. package/lib/types/data-table.d.cts +185 -0
  94. package/lib/types/data-table.d.ts +185 -0
  95. package/lib/types/modal-dialog.d.cts +13 -0
  96. package/lib/types/modal-dialog.d.ts +13 -0
  97. package/lib/types/movable-window.d.cts +16 -0
  98. package/lib/types/movable-window.d.ts +16 -0
  99. package/lib/warning-popper.d.cts +39 -0
  100. package/lib/warning-popper.d.ts +39 -0
  101. package/lib/warning-popper.vue_vue_type_style_index_0_lang-BKV_FX6k.js +1837 -0
  102. package/lib/warning-popper.vue_vue_type_style_index_0_lang-CHmkjVCO.cjs +1 -0
  103. package/package.json +40 -14
  104. package/src/accordion-fieldset.vue +24 -19
  105. package/src/accordion-section.vue +33 -28
  106. package/src/components.ts +10 -0
  107. package/src/copy-button.vue +89 -0
  108. package/src/dark-mode-selector.vue +212 -0
  109. package/src/data-table.vue +648 -0
  110. package/src/help-button.vue +128 -0
  111. package/src/help-popper.vue +70 -0
  112. package/src/helpers/data-table-csv.ts +55 -0
  113. package/src/helpers/show-floating-component.ts +177 -0
  114. package/src/helpers/show-modal-dialog.ts +121 -0
  115. package/src/helpers/show-overlay-box.ts +106 -0
  116. package/src/helpers/show-screen-loading.ts +37 -0
  117. package/src/helpers/show-snackbar.ts +153 -0
  118. package/src/hourglass-loading.vue +1 -1
  119. package/src/index.ts +34 -0
  120. package/src/internal/anchored-popover.vue +192 -0
  121. package/src/internal/anchored-position.ts +135 -0
  122. package/src/internal/data-table-controls.vue +203 -0
  123. package/src/internal/data-table-value.ts +99 -0
  124. package/src/internal/floating-component.vue +202 -0
  125. package/src/internal/icon-popper.vue +185 -0
  126. package/src/internal/mount-window.ts +110 -0
  127. package/src/internal/movable-window.vue +315 -0
  128. package/src/internal/opened-overlay-boxes.ts +2 -0
  129. package/src/internal/pagination.vue +325 -0
  130. package/src/internal/snackbar-container.vue +258 -0
  131. package/src/internal/snackbar-item.ts +20 -0
  132. package/src/internal/tab-group-context.ts +4 -2
  133. package/src/internal/use-movable-window.ts +208 -0
  134. package/src/internal/window-order.ts +11 -0
  135. package/src/intersection-lazy-component.vue +84 -0
  136. package/src/label-button.vue +9 -6
  137. package/src/label-icon.vue +2 -2
  138. package/src/modal-dialog.vue +287 -0
  139. package/src/overlay-box.vue +218 -0
  140. package/src/page-section.vue +7 -7
  141. package/src/page-title.vue +4 -4
  142. package/src/screen-loading.vue +90 -0
  143. package/src/tab-content.vue +16 -15
  144. package/src/tab-group.vue +34 -36
  145. package/src/types/data-table.ts +196 -0
  146. package/src/types/modal-dialog.ts +16 -0
  147. package/src/types/movable-window.ts +18 -0
  148. package/src/warning-popper.vue +70 -0
  149. package/lib/accordion-fieldset.cjs +0 -1
  150. package/lib/accordion-fieldset.js +0 -4
  151. package/lib/accordion-fieldset.vue_vue_type_style_index_0_lang-Cf4czDXz.js +0 -66
  152. package/lib/accordion-fieldset.vue_vue_type_style_index_0_lang-DoM5QjpJ.cjs +0 -1
  153. package/lib/accordion-section.cjs +0 -1
  154. package/lib/accordion-section.js +0 -4
  155. package/lib/accordion-section.vue_vue_type_style_index_0_lang-CFDdUEnn.cjs +0 -1
  156. package/lib/accordion-section.vue_vue_type_style_index_0_lang-Gvjjx02P.js +0 -78
  157. package/lib/components-C63HJoCn.cjs +0 -1
  158. package/lib/components-q4Cf97bO.js +0 -24
  159. package/lib/hourglass-loading.cjs +0 -1
  160. package/lib/hourglass-loading.js +0 -4
  161. package/lib/hourglass-loading.vue_vue_type_style_index_0_lang-C72UVA9W.cjs +0 -1
  162. package/lib/hourglass-loading.vue_vue_type_style_index_0_lang-DkUGIxQ2.js +0 -22
  163. package/lib/label-button.cjs +0 -1
  164. package/lib/label-button.js +0 -4
  165. package/lib/label-button.vue_vue_type_style_index_0_lang-D7ZgfZOL.cjs +0 -1
  166. package/lib/label-button.vue_vue_type_style_index_0_lang-DqRYddVt.js +0 -44
  167. package/lib/label-icon.cjs +0 -1
  168. package/lib/label-icon.js +0 -4
  169. package/lib/label-icon.vue_vue_type_style_index_0_lang-D9Cimu20.cjs +0 -1
  170. package/lib/label-icon.vue_vue_type_style_index_0_lang-me52HE2V.js +0 -38
  171. package/lib/page-section.cjs +0 -1
  172. package/lib/page-section.js +0 -4
  173. package/lib/page-section.vue_vue_type_style_index_0_lang-DEAkTLxd.cjs +0 -1
  174. package/lib/page-section.vue_vue_type_style_index_0_lang-DpVQe_rV.js +0 -54
  175. package/lib/page-title.cjs +0 -1
  176. package/lib/page-title.js +0 -4
  177. package/lib/page-title.vue_vue_type_style_index_0_lang-BqIsIks5.cjs +0 -1
  178. package/lib/page-title.vue_vue_type_style_index_0_lang-ChZot85K.js +0 -36
  179. package/lib/tab-content.cjs +0 -1
  180. package/lib/tab-content.js +0 -4
  181. package/lib/tab-content.vue_vue_type_style_index_0_lang-BeCi7Xkp.cjs +0 -1
  182. package/lib/tab-content.vue_vue_type_style_index_0_lang-CcQLHmIF.js +0 -44
  183. package/lib/tab-group.cjs +0 -1
  184. package/lib/tab-group.js +0 -4
  185. package/lib/tab-group.vue_vue_type_style_index_0_lang-C84mDEOC.js +0 -103
  186. package/lib/tab-group.vue_vue_type_style_index_0_lang-CMaufRJV.cjs +0 -1
  187. package/lib/use-shrinkable-CS2bmsJJ.js +0 -17
  188. package/lib/use-shrinkable-Cu9JQZll.cjs +0 -1
@@ -0,0 +1,648 @@
1
+ <!--
2
+ データテーブル
3
+ 以下の要素を描画する
4
+ ・テーブルヘッダ
5
+ ・ページネーション
6
+ ・総件数
7
+ ・表示件数
8
+ ・ダウンロードボタン
9
+ ・全件ダウンロードボタン
10
+ 以下の機能を提供する
11
+ ・行クリックによる行選択
12
+ ・カーソルキーでの選択行移動
13
+ ・ヘッダクリックによるソート
14
+ ・ヘッダドラッグによる列入れ替え(制限事項あり)
15
+ ・ヘッダ列幅変更
16
+
17
+ 一覧のデータ部(tbody内のコンテンツ)は利用者が定義する
18
+ -->
19
+ <template>
20
+ <section
21
+ class="dwui-data-table"
22
+ :class="{ 'dwui-selectable': props.selectable }"
23
+ >
24
+ <DataTableControls
25
+ v-if="props.controls === 'both' || props.controls === 'top'"
26
+ v-bind="controlProps"
27
+ :pagination="props.pagination === 'both' || props.pagination === 'top'"
28
+ @emit:change-page="emit('emit:changePage', $event)"
29
+ @emit:change-limit="emit('emit:changeLimit', $event)"
30
+ @emit:download-page="emit('emit:downloadPage')"
31
+ @emit:download-all="emit('emit:downloadAll')"
32
+ />
33
+ <div
34
+ ref="refContent"
35
+ class="dwui-data-table-content"
36
+ :class="[{ 'dwui-loading': props.loading }, props.contentClass]"
37
+ :style="props.contentStyle"
38
+ tabindex="-1"
39
+ @keydown="onKeyDown"
40
+ >
41
+ <table
42
+ :class="props.tableClass"
43
+ :style="props.tableStyle"
44
+ >
45
+ <thead ref="refThead">
46
+ <tr>
47
+ <th
48
+ v-for="(header, index) in innerHeaders"
49
+ :key="header.id"
50
+ :rowspan="header.rowspan"
51
+ :colspan="header.colspan"
52
+ :title="header.title"
53
+ :style="headerStyle(header)"
54
+ :aria-sort="ariaSort(header.id)"
55
+ scope="col"
56
+ >
57
+ <div class="dwui-data-table-header">
58
+ <span
59
+ class="dwui-data-table-sort"
60
+ :role="props.sortable ? 'button' : undefined"
61
+ :tabindex="props.sortable ? 0 : undefined"
62
+ @click="onClickSort(header)"
63
+ @keydown.enter.prevent="onClickSort(header)"
64
+ @keydown.space.prevent="onClickSort(header)"
65
+ >
66
+ <SvgPointingTriangle
67
+ v-if="caretDirection(header.id)"
68
+ :direction="caretDirection(header.id)"
69
+ size="0.7em"
70
+ />
71
+ <span
72
+ v-if="header.html"
73
+ v-html="resolveLabel(header.label)"
74
+ />
75
+ <span v-else>{{ resolveLabel(header.label) }}</span>
76
+ </span>
77
+ </div>
78
+ <div
79
+ v-if="props.resizable && index < innerHeaders.length - 1"
80
+ class="dwui-data-table-resizer"
81
+ @pointerdown="onResizeStart($event, index)"
82
+ @pointermove="onResizing"
83
+ @pointerup="onResizeEnd"
84
+ @pointercancel="onResizeEnd"
85
+ />
86
+ </th>
87
+ </tr>
88
+ </thead>
89
+ <tbody ref="refTbody">
90
+ <template v-if="!props.loading">
91
+ <slot
92
+ :rows="rows"
93
+ :click-row="clickRow"
94
+ :dblclick-row="dblclickRow"
95
+ :row-number="rowNumber"
96
+ :selected-index="selectedIndex"
97
+ />
98
+ <tr
99
+ v-if="props.dataList.length === 0 && props.showNoData"
100
+ class="dwui-data-table-no-data"
101
+ >
102
+ <td :colspan="totalColSize">
103
+ {{ labels.noData }}
104
+ </td>
105
+ </tr>
106
+ </template>
107
+ </tbody>
108
+ </table>
109
+ <div
110
+ v-if="props.loading"
111
+ class="dwui-data-table-loading"
112
+ >
113
+ <slot name="loading">
114
+ <div class="dwui-data-table-loading-spinner" />
115
+ </slot>
116
+ </div>
117
+ </div>
118
+ <DataTableControls
119
+ v-if="props.controls === 'both' || props.controls === 'bottom'"
120
+ v-bind="controlProps"
121
+ :pagination="props.pagination === 'both' || props.pagination === 'bottom'"
122
+ @emit:change-page="emit('emit:changePage', $event)"
123
+ @emit:change-limit="emit('emit:changeLimit', $event)"
124
+ @emit:download-page="emit('emit:downloadPage')"
125
+ @emit:download-all="emit('emit:downloadAll')"
126
+ />
127
+ </section>
128
+ </template>
129
+
130
+ <script setup lang="ts" generic="T">
131
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
132
+ import type { StyleValue } from 'vue';
133
+ import { SvgPointingTriangle } from '@digitalwalletcorp/vue-svg-icons';
134
+ import DataTableControls from '@/internal/data-table-controls.vue';
135
+ import { compareValues, getValueByPath, resolveLabel, stringifyValue } from '@/internal/data-table-value';
136
+ import type { DataTableEmits, DataTableHeader, DataTableLabels, DataTableProps, DataTableResolvedLabels, DataTableSlots, DataTableSortContext } from '@/types/data-table';
137
+
138
+ const props = withDefaults(defineProps<DataTableProps<T>>(), {
139
+ loading: false,
140
+ totalCount: undefined,
141
+ current: 1,
142
+ limit: 100,
143
+ maxLimit: 9999,
144
+ maxPagination: 7,
145
+ selectable: true,
146
+ controls: 'both',
147
+ pagination: 'both',
148
+ showDownloadPage: true,
149
+ showDownloadAll: true,
150
+ showTotal: true,
151
+ showLimit: true,
152
+ showPageSelect: false,
153
+ showNoData: true,
154
+ resizable: true,
155
+ sortable: true,
156
+ sortMode: 'remote',
157
+ formatTotalCount: undefined,
158
+ contentClass: undefined,
159
+ contentStyle: undefined,
160
+ tableClass: undefined,
161
+ tableStyle: undefined,
162
+ labels: undefined
163
+ });
164
+
165
+ const emit = defineEmits<DataTableEmits<T>>();
166
+
167
+ defineSlots<DataTableSlots<T>>();
168
+
169
+ const refContent = ref<HTMLDivElement>();
170
+ const refThead = ref<HTMLTableSectionElement>();
171
+ const refTbody = ref<HTMLTableSectionElement>();
172
+
173
+ /** 列幅の変更で決めた幅(px)。id をキーにする */
174
+ const widths = ref<Record<string, number>>({});
175
+ /** ソートしているヘッダの id */
176
+ const sortKey = ref('');
177
+ /** 降順でソートしているか */
178
+ const descending = ref(false);
179
+ /** 列幅の変更中の、対象の表示位置とポインタの X 座標 */
180
+ const resizing = ref<{ index: number; x: number }>();
181
+
182
+ let rowObserver: MutationObserver | undefined;
183
+
184
+ /** 列幅の変更を反映したヘッダ */
185
+ const innerHeaders = computed((): DataTableHeader<T>[] => {
186
+ return props.headers.map(header => widths.value[header.id] == null ? header : { ...header, width: widths.value[header.id] });
187
+ });
188
+
189
+ const labels = computed((): DataTableResolvedLabels => {
190
+ const merged: Required<DataTableLabels> = {
191
+ totalCount: 'Total',
192
+ totalUnit: '',
193
+ limit: 'Rows per page',
194
+ noData: 'No data found',
195
+ pageNumber: 'Page',
196
+ downloadPage: 'Download this page',
197
+ downloadAll: 'Download all',
198
+ firstPage: 'First page',
199
+ previousPage: 'Previous page',
200
+ previousPages: 'Previous pages',
201
+ nextPages: 'Next pages',
202
+ nextPage: 'Next page',
203
+ lastPage: 'Last page',
204
+ ...props.labels
205
+ };
206
+ const resolved = {} as DataTableResolvedLabels;
207
+ for (const key of Object.keys(merged) as (keyof DataTableLabels)[]) {
208
+ const label = merged[key];
209
+ resolved[key] = typeof label === 'function' ? label() : label;
210
+ }
211
+ return resolved;
212
+ });
213
+
214
+ const limit = computed((): number => {
215
+ return Math.min(props.limit, props.maxLimit);
216
+ });
217
+
218
+ const controlProps = computed(() => {
219
+ return {
220
+ showTotal: props.showTotal,
221
+ showLimit: props.showLimit,
222
+ showPageSelect: props.showPageSelect,
223
+ showDownloadPage: props.showDownloadPage,
224
+ showDownloadAll: props.showDownloadAll,
225
+ current: props.current,
226
+ limit: limit.value,
227
+ maxLimit: props.maxLimit,
228
+ maxPagination: props.maxPagination,
229
+ totalCount: props.totalCount ?? props.dataList.length,
230
+ formatTotalCount: props.formatTotalCount,
231
+ labels: labels.value
232
+ };
233
+ });
234
+
235
+ /** 選択している行。行のオブジェクトが差し替わっても内容が同じなら選択を保てるよう、行そのものを持つ */
236
+ const selectedRow = shallowRef<T | undefined>(undefined);
237
+
238
+ /** 表示する行。ページ内ソート中は dataList を並べ替えたコピー、それ以外は dataList そのもの */
239
+ const rows = computed((): T[] => {
240
+ const header = props.sortMode === 'page' ? props.headers.find(header => header.id === sortKey.value) : undefined;
241
+ if (!header) {
242
+ return props.dataList;
243
+ }
244
+ const compare = header.compare ?? ((a: T, b: T): number => {
245
+ const key = header.sortKey ?? header.id;
246
+ return compareValues(getValueByPath(a, key), getValueByPath(b, key), header.type ?? 'string');
247
+ });
248
+ return [...props.dataList].sort((a, b) => descending.value ? compare(b, a) : compare(a, b));
249
+ });
250
+
251
+ /** 選択している行の、表示順でのインデックス。同じ参照が無ければ内容で探す */
252
+ const selectedIndex = computed((): number => {
253
+ const selected = selectedRow.value;
254
+ if (selected == null) {
255
+ return -1;
256
+ }
257
+ const index = rows.value.indexOf(selected);
258
+ if (index !== -1) {
259
+ return index;
260
+ }
261
+ const json = stringifyValue(selected);
262
+ if (json === '') {
263
+ return -1;
264
+ }
265
+ return rows.value.findIndex(row => stringifyValue(row) === json);
266
+ });
267
+
268
+ /** データが無い行の colspan。colspan を持つヘッダはその数で数える */
269
+ const totalColSize = computed((): number => {
270
+ return props.headers.reduce((acc, cur) => acc + (cur.colspan == null ? 1 : Number(cur.colspan)), 0);
271
+ });
272
+
273
+ const toCssSize = (size: string | number | undefined): string | undefined => {
274
+ if (size == null) {
275
+ return undefined;
276
+ }
277
+ return typeof size === 'number' ? `${size}px` : size;
278
+ };
279
+
280
+ const headerStyle = (header: DataTableHeader<T>): StyleValue => {
281
+ return {
282
+ width: toCssSize(header.width),
283
+ minWidth: toCssSize(header.minWidth),
284
+ maxWidth: toCssSize(header.maxWidth)
285
+ };
286
+ };
287
+
288
+ const ariaSort = (id: string): 'ascending' | 'descending' | undefined => {
289
+ if (sortKey.value !== id) {
290
+ return undefined;
291
+ }
292
+ return descending.value ? 'descending' : 'ascending';
293
+ };
294
+
295
+ const caretDirection = (id: string): 'up' | 'down' | undefined => {
296
+ if (!props.sortable || sortKey.value !== id) {
297
+ return undefined;
298
+ }
299
+ return descending.value ? 'down' : 'up';
300
+ };
301
+
302
+ const rowNumber = (index: number): number => {
303
+ return (props.current - 1) * limit.value + index + 1;
304
+ };
305
+
306
+ /** スロットが描画した行の要素。データが無い行は含めない */
307
+ const rowElements = (): HTMLTableRowElement[] => {
308
+ return Array.from(refTbody.value?.children ?? [])
309
+ .filter((el): el is HTMLTableRowElement => el.tagName === 'TR' && !el.classList.contains('dwui-data-table-no-data'));
310
+ };
311
+
312
+ const cellsOf = (tr: HTMLTableRowElement): HTMLTableCellElement[] => {
313
+ return Array.from(tr.children).filter((el): el is HTMLTableCellElement => el.tagName === 'TD' || el.tagName === 'TH');
314
+ };
315
+
316
+ const emitHeaders = (): void => {
317
+ emit('emit:updateHeader', innerHeaders.value.map(header => ({ ...header })));
318
+ };
319
+
320
+ /**
321
+ * 行に選択状態の class を付ける。
322
+ * 行はスロットで描画されるため、描画後の DOM に対してここで反映する
323
+ */
324
+ const syncRows = (): void => {
325
+ rowElements().forEach((tr, index) => tr.classList.toggle('dwui-selected', index === selectedIndex.value));
326
+ };
327
+
328
+ /**
329
+ * 選択している行がスクロール領域に収まるようにスクロールする。sticky なヘッダの下に隠れないようにする
330
+ */
331
+ const scrollToSelectedRow = (): void => {
332
+ const pane = refContent.value;
333
+ const tr = rowElements()[selectedIndex.value];
334
+ if (!pane || !tr) {
335
+ return;
336
+ }
337
+ const headerHeight = refThead.value?.offsetHeight ?? 0;
338
+ const top = tr.offsetTop;
339
+ const bottom = top + tr.offsetHeight;
340
+ if (top < pane.scrollTop + headerHeight) {
341
+ pane.scrollTop = top - headerHeight;
342
+ } else if (pane.scrollTop + pane.clientHeight < bottom) {
343
+ pane.scrollTop = bottom - pane.clientHeight;
344
+ }
345
+ };
346
+
347
+ /**
348
+ * 選択を変えて通知する。解除は row に undefined、index に -1 を渡す
349
+ */
350
+ const select = (row: T | undefined, index: number): void => {
351
+ selectedRow.value = row;
352
+ emit('emit:selectRow', row, index);
353
+ };
354
+
355
+ /**
356
+ * 行を選択する。選択している行なら選択を解除する
357
+ */
358
+ const clickRow = (row: T, index: number): void => {
359
+ if (!props.selectable) {
360
+ return;
361
+ }
362
+ if (index === selectedIndex.value) {
363
+ select(undefined, -1);
364
+ } else {
365
+ select(row, index);
366
+ // カーソルキーでの移動は、この領域にフォーカスがある間だけ受け付ける
367
+ refContent.value?.focus({ preventScroll: true });
368
+ }
369
+ };
370
+
371
+ const dblclickRow = (row: T, index: number): void => {
372
+ if (index !== selectedIndex.value) {
373
+ clickRow(row, index);
374
+ }
375
+ emit('emit:dblclickRow', row, index);
376
+ };
377
+
378
+ /**
379
+ * 行を選択している間、カーソルキー(↑↓)で選択を移動する。
380
+ * 行内の入力欄などにフォーカスがあるときは、その要素のキー操作を妨げないよう何もしない
381
+ */
382
+ const onKeyDown = (event: KeyboardEvent): void => {
383
+ if (event.target !== event.currentTarget || selectedIndex.value < 0) {
384
+ return;
385
+ }
386
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
387
+ return;
388
+ }
389
+ const index = selectedIndex.value + (event.key === 'ArrowUp' ? -1 : 1);
390
+ if (index < 0 || rows.value.length <= index) {
391
+ return;
392
+ }
393
+ event.preventDefault();
394
+ select(rows.value[index], index);
395
+ };
396
+
397
+ const onClickSort = (header: DataTableHeader<T>): void => {
398
+ if (!props.sortable || props.dataList.length === 0) {
399
+ return;
400
+ }
401
+ descending.value = sortKey.value === header.id ? !descending.value : false;
402
+ sortKey.value = header.id;
403
+ emit('emit:changeSort', {
404
+ sortKey: sortKey.value,
405
+ descending: descending.value
406
+ });
407
+ };
408
+
409
+ /**
410
+ * 列幅の変更を始める。すべての列の幅を今の表示幅で固定してから変更する
411
+ */
412
+ const onResizeStart = (event: PointerEvent, index: number): void => {
413
+ if (!props.resizable) {
414
+ return;
415
+ }
416
+ event.preventDefault();
417
+ const headerRow = refThead.value?.firstElementChild as HTMLTableRowElement | null;
418
+ const cells = headerRow ? cellsOf(headerRow) : [];
419
+ for (const [i, cell] of cells.entries()) {
420
+ widths.value[props.headers[i].id] = cell.getBoundingClientRect().width;
421
+ }
422
+ resizing.value = { index, x: event.clientX };
423
+ (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
424
+ emitHeaders();
425
+ };
426
+
427
+ /**
428
+ * 対象の列の幅をポインタの移動量だけ変え、右側の列に均等に打ち消す
429
+ */
430
+ const onResizing = (event: PointerEvent): void => {
431
+ if (!resizing.value) {
432
+ return;
433
+ }
434
+ const moved = event.clientX - resizing.value.x;
435
+ if (moved === 0) {
436
+ return;
437
+ }
438
+ resizing.value.x = event.clientX;
439
+ const index = resizing.value.index;
440
+ const ids = props.headers.map(header => header.id);
441
+ widths.value[ids[index]] += moved;
442
+ const share = moved / (ids.length - index - 1);
443
+ for (const id of ids.slice(index + 1)) {
444
+ widths.value[id] -= share;
445
+ }
446
+ };
447
+
448
+ const onResizeEnd = (): void => {
449
+ if (resizing.value) {
450
+ resizing.value = undefined;
451
+ emitHeaders();
452
+ }
453
+ };
454
+
455
+ onMounted(() => {
456
+ // 行の描画は利用側のタイミングで行われるため、tbody の行の増減を監視して反映する
457
+ rowObserver = new MutationObserver(syncRows);
458
+ if (refTbody.value) {
459
+ rowObserver.observe(refTbody.value, { childList: true });
460
+ }
461
+ syncRows();
462
+ });
463
+
464
+ onBeforeUnmount(() => {
465
+ rowObserver?.disconnect();
466
+ });
467
+
468
+ watch(() => props.headers, (newValue, oldValue) => {
469
+ // 列が入れ替わったときは、変更した列幅を引き継がない
470
+ const newIds = newValue.map(header => header.id);
471
+ const oldIds = oldValue.map(header => header.id);
472
+ if (newIds.length !== oldIds.length || newIds.some(id => !oldIds.includes(id))) {
473
+ widths.value = {};
474
+ }
475
+ });
476
+
477
+ watch(() => props.loading, (newValue, oldValue) => {
478
+ // 読み込みが終わったら、前のデータに対する選択は引き継がない。
479
+ // ページ内ソートも同様だが、remote のソート条件は再取得後も有効なので保つ
480
+ if (oldValue && !newValue) {
481
+ if (props.sortMode === 'page') {
482
+ sortKey.value = '';
483
+ descending.value = false;
484
+ }
485
+ if (selectedRow.value != null) {
486
+ select(undefined, -1);
487
+ }
488
+ }
489
+ });
490
+
491
+ watch(selectedIndex, (newValue) => {
492
+ if (0 <= newValue) {
493
+ scrollToSelectedRow();
494
+ }
495
+ }, { flush: 'post' });
496
+
497
+ watch(selectedIndex, syncRows, { flush: 'post' });
498
+
499
+ defineExpose({
500
+ /** ソートの状態を返す */
501
+ getSortContext: (): DataTableSortContext => {
502
+ return {
503
+ sortKey: sortKey.value,
504
+ descending: descending.value
505
+ };
506
+ }
507
+ });
508
+ </script>
509
+
510
+ <style>
511
+ :where(.dwui-data-table-content) {
512
+ /* 読み込み中の表示と行のスクロール位置の基準にする */
513
+ position: relative;
514
+ overflow: auto;
515
+ }
516
+
517
+ :where(.dwui-data-table-content:focus-visible) {
518
+ outline: var(--dwui-outline-width-focus-ring, 2px) solid var(--dwui-outline-color-focus-ring, Highlight);
519
+ outline-offset: calc(var(--dwui-outline-width-focus-ring, 2px) * -1);
520
+ }
521
+
522
+ :where(.dwui-data-table-content.dwui-loading) {
523
+ min-height: var(--dwui-height-data-table-loading, 120px);
524
+ }
525
+
526
+ :where(.dwui-data-table table) {
527
+ /* collapse にするとスクロール時に sticky なヘッダの境界線が動いて行が透けるため separate にする */
528
+ border-collapse: separate;
529
+ border-spacing: 0;
530
+ width: 100%;
531
+ }
532
+
533
+ :where(.dwui-data-table th) {
534
+ position: sticky;
535
+ top: 0;
536
+ z-index: 1;
537
+ padding: 0.33em 0;
538
+ border: 1px solid var(--dwui-border-color-data-table, light-dark(#818181, #5a5a5a));
539
+ border-right: none;
540
+ background: var(--dwui-background-data-table-header, light-dark(#e8e8e8, #3a3a3a));
541
+ color: var(--dwui-color-text-data-table-header, inherit);
542
+ text-align: center;
543
+ white-space: nowrap;
544
+ }
545
+
546
+ :where(.dwui-data-table th:last-child) {
547
+ border-right: 1px solid var(--dwui-border-color-data-table, light-dark(#818181, #5a5a5a));
548
+ }
549
+
550
+ :where(.dwui-data-table-sort) {
551
+ display: inline-flex;
552
+ align-items: center;
553
+ gap: 0.17em;
554
+ }
555
+
556
+ :where(.dwui-data-table-sort[role='button']) {
557
+ cursor: pointer;
558
+ }
559
+
560
+ :where(.dwui-data-table-sort:focus-visible) {
561
+ outline: var(--dwui-outline-width-focus-ring, 2px) solid var(--dwui-outline-color-focus-ring, Highlight);
562
+ outline-offset: var(--dwui-outline-offset-focus-ring, 2px);
563
+ }
564
+
565
+ :where(.dwui-data-table-resizer) {
566
+ position: absolute;
567
+ top: 0;
568
+ right: 0;
569
+ width: 10px;
570
+ height: 100%;
571
+ cursor: col-resize;
572
+ touch-action: none;
573
+ }
574
+
575
+ :where(.dwui-data-table td) {
576
+ padding: 0.25em;
577
+ border-top: 1px dotted var(--dwui-border-color-data-table, light-dark(#818181, #5a5a5a));
578
+ border-left: 1px dotted var(--dwui-border-color-data-table, light-dark(#818181, #5a5a5a));
579
+ line-height: 1.2em;
580
+ word-break: break-all;
581
+ }
582
+
583
+ :where(.dwui-data-table tbody tr:first-child td) {
584
+ border-top: none;
585
+ }
586
+
587
+ :where(.dwui-data-table tbody tr:last-child td) {
588
+ border-bottom: 1px dotted var(--dwui-border-color-data-table, light-dark(#818181, #5a5a5a));
589
+ }
590
+
591
+ :where(.dwui-data-table td:last-child) {
592
+ border-right: 1px dotted var(--dwui-border-color-data-table, light-dark(#818181, #5a5a5a));
593
+ }
594
+
595
+ :where(.dwui-data-table tbody tr:nth-child(odd)) {
596
+ background: var(--dwui-background-data-table-row-odd, light-dark(#ffffff, #1e1e1e));
597
+ transition: background-color 1s ease;
598
+ }
599
+
600
+ :where(.dwui-data-table tbody tr:nth-child(even)) {
601
+ background: var(--dwui-background-data-table-row-even, light-dark(#f4f4f4, #262626));
602
+ transition: background-color 1s ease;
603
+ }
604
+
605
+ :where(.dwui-data-table.dwui-selectable tbody tr) {
606
+ cursor: pointer;
607
+ }
608
+
609
+ :where(.dwui-data-table tbody tr:not(.dwui-selected, .dwui-data-table-no-data):hover) {
610
+ background: var(--dwui-background-data-table-row-hover, light-dark(#e0f7fa, #2f4f4f));
611
+ }
612
+
613
+ :where(.dwui-data-table tbody tr.dwui-selected) {
614
+ background: var(--dwui-background-data-table-row-selected, light-dark(#fff3b0, #5a4a00));
615
+ }
616
+
617
+ :where(.dwui-data-table-no-data td) {
618
+ padding: 0.83em 0;
619
+ color: var(--dwui-color-text-data-table-no-data, light-dark(#808080, #a0a0a0));
620
+ text-align: center;
621
+ cursor: default;
622
+ }
623
+
624
+ :where(.dwui-data-table-loading) {
625
+ position: absolute;
626
+ inset: 0;
627
+ z-index: 2;
628
+ display: flex;
629
+ align-items: center;
630
+ justify-content: center;
631
+ background: var(--dwui-overlay-color-loading, light-dark(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.45)));
632
+ }
633
+
634
+ :where(.dwui-data-table-loading-spinner) {
635
+ width: var(--dwui-size-loading-spinner, 40px);
636
+ height: var(--dwui-size-loading-spinner, 40px);
637
+ border: 4px solid var(--dwui-border-color-loading-spinner, rgba(255, 255, 255, 0.4));
638
+ border-top-color: var(--dwui-border-color-loading-spinner-accent, #ffffff);
639
+ border-radius: 50%;
640
+ animation: dwui-data-table-loading-spin 0.8s linear infinite;
641
+ }
642
+
643
+ @keyframes dwui-data-table-loading-spin {
644
+ to {
645
+ transform: rotate(360deg);
646
+ }
647
+ }
648
+ </style>