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