@digitalwalletcorp/vue-core 1.2.1 → 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.
- package/README.md +79 -3
- package/lib/accordion-fieldset.d.cts +14 -4
- package/lib/accordion-fieldset.d.ts +14 -4
- package/lib/accordion-section.d.cts +20 -10
- package/lib/accordion-section.d.ts +20 -10
- package/lib/components-2zQ2SZuY.js +26 -0
- package/lib/components-HaiHcseF.cjs +1 -0
- package/lib/components.d.cts +10 -0
- package/lib/components.d.ts +10 -0
- package/lib/copy-button.d.cts +34 -0
- package/lib/copy-button.d.ts +34 -0
- package/lib/dark-mode-selector.d.cts +32 -0
- package/lib/dark-mode-selector.d.ts +32 -0
- package/lib/data-table.d.cts +30 -0
- package/lib/data-table.d.ts +30 -0
- package/lib/help-button.d.cts +39 -0
- package/lib/help-button.d.ts +39 -0
- package/lib/help-popper.d.cts +39 -0
- package/lib/help-popper.d.ts +39 -0
- package/lib/helpers/data-table-csv.d.cts +17 -0
- package/lib/helpers/data-table-csv.d.ts +17 -0
- package/lib/helpers/show-floating-component.d.cts +82 -0
- package/lib/helpers/show-floating-component.d.ts +82 -0
- package/lib/helpers/show-modal-dialog.d.cts +73 -0
- package/lib/helpers/show-modal-dialog.d.ts +73 -0
- package/lib/helpers/show-overlay-box.d.cts +53 -0
- package/lib/helpers/show-overlay-box.d.ts +53 -0
- package/lib/helpers/show-screen-loading.d.cts +20 -0
- package/lib/helpers/show-screen-loading.d.ts +20 -0
- package/lib/helpers/show-snackbar.d.cts +47 -0
- package/lib/helpers/show-snackbar.d.ts +47 -0
- package/lib/hourglass-loading.d.cts +1 -1
- package/lib/hourglass-loading.d.ts +1 -1
- package/lib/index.cjs +1 -1
- package/lib/index.d.cts +34 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.js +445 -18
- package/lib/internal/anchored-popover.d.cts +35 -0
- package/lib/internal/anchored-popover.d.ts +35 -0
- package/lib/internal/anchored-position.d.cts +50 -0
- package/lib/internal/anchored-position.d.ts +50 -0
- package/lib/internal/data-table-controls.d.cts +42 -0
- package/lib/internal/data-table-controls.d.ts +42 -0
- package/lib/internal/data-table-value.d.cts +32 -0
- package/lib/internal/data-table-value.d.ts +32 -0
- package/lib/internal/floating-component.d.cts +52 -0
- package/lib/internal/floating-component.d.ts +52 -0
- package/lib/internal/icon-popper.d.cts +32 -0
- package/lib/internal/icon-popper.d.ts +32 -0
- package/lib/internal/mount-window.d.cts +39 -0
- package/lib/internal/mount-window.d.ts +39 -0
- package/lib/internal/movable-window.d.cts +55 -0
- package/lib/internal/movable-window.d.ts +55 -0
- package/lib/internal/opened-overlay-boxes.d.cts +2 -0
- package/lib/internal/opened-overlay-boxes.d.ts +2 -0
- package/lib/internal/pagination.d.cts +22 -0
- package/lib/internal/pagination.d.ts +22 -0
- package/lib/internal/snackbar-container.d.cts +20 -0
- package/lib/internal/snackbar-container.d.ts +20 -0
- package/lib/internal/snackbar-item.d.cts +17 -0
- package/lib/internal/snackbar-item.d.ts +17 -0
- package/lib/internal/tab-group-context.d.cts +4 -2
- package/lib/internal/tab-group-context.d.ts +4 -2
- package/lib/internal/use-movable-window.d.cts +44 -0
- package/lib/internal/use-movable-window.d.ts +44 -0
- package/lib/internal/window-order.d.cts +6 -0
- package/lib/internal/window-order.d.ts +6 -0
- package/lib/intersection-lazy-component.d.cts +32 -0
- package/lib/intersection-lazy-component.d.ts +32 -0
- package/lib/label-button.d.cts +2 -2
- package/lib/label-button.d.ts +2 -2
- package/lib/label-icon.d.cts +2 -2
- package/lib/label-icon.d.ts +2 -2
- package/lib/modal-dialog.d.cts +67 -0
- package/lib/modal-dialog.d.ts +67 -0
- package/lib/nuxt.cjs +1 -1
- package/lib/nuxt.js +1 -1
- package/lib/overlay-box.d.cts +52 -0
- package/lib/overlay-box.d.ts +52 -0
- package/lib/page-section.d.cts +2 -2
- package/lib/page-section.d.ts +2 -2
- package/lib/page-title.d.cts +2 -2
- package/lib/page-title.d.ts +2 -2
- package/lib/register.cjs +1 -1
- package/lib/register.js +1 -1
- package/lib/screen-loading.d.cts +7 -0
- package/lib/screen-loading.d.ts +7 -0
- package/lib/style.css +1 -1
- package/lib/tab-content.d.cts +5 -5
- package/lib/tab-content.d.ts +5 -5
- package/lib/tab-group.d.cts +7 -10
- package/lib/tab-group.d.ts +7 -10
- package/lib/types/data-table.d.cts +185 -0
- package/lib/types/data-table.d.ts +185 -0
- package/lib/types/modal-dialog.d.cts +13 -0
- package/lib/types/modal-dialog.d.ts +13 -0
- package/lib/types/movable-window.d.cts +16 -0
- package/lib/types/movable-window.d.ts +16 -0
- package/lib/warning-popper.d.cts +39 -0
- package/lib/warning-popper.d.ts +39 -0
- package/lib/warning-popper.vue_vue_type_style_index_0_lang-CxVzqsbn.cjs +1 -0
- package/lib/warning-popper.vue_vue_type_style_index_0_lang-zt-jCsvz.js +1833 -0
- package/package.json +40 -14
- package/src/accordion-fieldset.vue +24 -19
- package/src/accordion-section.vue +33 -28
- package/src/components.ts +10 -0
- package/src/copy-button.vue +89 -0
- package/src/dark-mode-selector.vue +212 -0
- package/src/data-table.vue +643 -0
- package/src/help-button.vue +128 -0
- package/src/help-popper.vue +70 -0
- package/src/helpers/data-table-csv.ts +55 -0
- package/src/helpers/show-floating-component.ts +169 -0
- package/src/helpers/show-modal-dialog.ts +121 -0
- package/src/helpers/show-overlay-box.ts +106 -0
- package/src/helpers/show-screen-loading.ts +37 -0
- package/src/helpers/show-snackbar.ts +153 -0
- package/src/hourglass-loading.vue +1 -1
- package/src/index.ts +34 -0
- package/src/internal/anchored-popover.vue +192 -0
- package/src/internal/anchored-position.ts +135 -0
- package/src/internal/data-table-controls.vue +203 -0
- package/src/internal/data-table-value.ts +99 -0
- package/src/internal/floating-component.vue +202 -0
- package/src/internal/icon-popper.vue +185 -0
- package/src/internal/mount-window.ts +110 -0
- package/src/internal/movable-window.vue +315 -0
- package/src/internal/opened-overlay-boxes.ts +2 -0
- package/src/internal/pagination.vue +325 -0
- package/src/internal/snackbar-container.vue +258 -0
- package/src/internal/snackbar-item.ts +20 -0
- package/src/internal/tab-group-context.ts +4 -2
- package/src/internal/use-movable-window.ts +208 -0
- package/src/internal/window-order.ts +11 -0
- package/src/intersection-lazy-component.vue +84 -0
- package/src/label-button.vue +9 -6
- package/src/label-icon.vue +2 -2
- package/src/modal-dialog.vue +287 -0
- package/src/overlay-box.vue +218 -0
- package/src/page-section.vue +7 -7
- package/src/page-title.vue +4 -4
- package/src/screen-loading.vue +90 -0
- package/src/tab-content.vue +16 -15
- package/src/tab-group.vue +34 -36
- package/src/types/data-table.ts +196 -0
- package/src/types/modal-dialog.ts +16 -0
- package/src/types/movable-window.ts +18 -0
- package/src/warning-popper.vue +70 -0
- package/lib/accordion-fieldset.cjs +0 -1
- package/lib/accordion-fieldset.js +0 -4
- package/lib/accordion-fieldset.vue_vue_type_style_index_0_lang-Cf4czDXz.js +0 -66
- package/lib/accordion-fieldset.vue_vue_type_style_index_0_lang-DoM5QjpJ.cjs +0 -1
- package/lib/accordion-section.cjs +0 -1
- package/lib/accordion-section.js +0 -4
- package/lib/accordion-section.vue_vue_type_style_index_0_lang-CFDdUEnn.cjs +0 -1
- package/lib/accordion-section.vue_vue_type_style_index_0_lang-Gvjjx02P.js +0 -78
- package/lib/components-C63HJoCn.cjs +0 -1
- package/lib/components-q4Cf97bO.js +0 -24
- package/lib/hourglass-loading.cjs +0 -1
- package/lib/hourglass-loading.js +0 -4
- package/lib/hourglass-loading.vue_vue_type_style_index_0_lang-C72UVA9W.cjs +0 -1
- package/lib/hourglass-loading.vue_vue_type_style_index_0_lang-DkUGIxQ2.js +0 -22
- package/lib/label-button.cjs +0 -1
- package/lib/label-button.js +0 -4
- package/lib/label-button.vue_vue_type_style_index_0_lang-D7ZgfZOL.cjs +0 -1
- package/lib/label-button.vue_vue_type_style_index_0_lang-DqRYddVt.js +0 -44
- package/lib/label-icon.cjs +0 -1
- package/lib/label-icon.js +0 -4
- package/lib/label-icon.vue_vue_type_style_index_0_lang-D9Cimu20.cjs +0 -1
- package/lib/label-icon.vue_vue_type_style_index_0_lang-me52HE2V.js +0 -38
- package/lib/page-section.cjs +0 -1
- package/lib/page-section.js +0 -4
- package/lib/page-section.vue_vue_type_style_index_0_lang-DEAkTLxd.cjs +0 -1
- package/lib/page-section.vue_vue_type_style_index_0_lang-DpVQe_rV.js +0 -54
- package/lib/page-title.cjs +0 -1
- package/lib/page-title.js +0 -4
- package/lib/page-title.vue_vue_type_style_index_0_lang-BqIsIks5.cjs +0 -1
- package/lib/page-title.vue_vue_type_style_index_0_lang-ChZot85K.js +0 -36
- package/lib/tab-content.cjs +0 -1
- package/lib/tab-content.js +0 -4
- package/lib/tab-content.vue_vue_type_style_index_0_lang-BeCi7Xkp.cjs +0 -1
- package/lib/tab-content.vue_vue_type_style_index_0_lang-CcQLHmIF.js +0 -44
- package/lib/tab-group.cjs +0 -1
- package/lib/tab-group.js +0 -4
- package/lib/tab-group.vue_vue_type_style_index_0_lang-C84mDEOC.js +0 -103
- package/lib/tab-group.vue_vue_type_style_index_0_lang-CMaufRJV.cjs +0 -1
- package/lib/use-shrinkable-CS2bmsJJ.js +0 -17
- package/lib/use-shrinkable-Cu9JQZll.cjs +0 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
DataTable の上下に置くコントロール
|
|
3
|
+
ページネーション・総件数・1ページあたりの表示件数・ダウンロードボタンを並べる。
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div class="dwui-data-table-controls">
|
|
7
|
+
<Pagination
|
|
8
|
+
v-if="props.pagination"
|
|
9
|
+
:current="props.current"
|
|
10
|
+
:total-count="props.totalCount"
|
|
11
|
+
:limit="props.limit"
|
|
12
|
+
:max-pagination="props.maxPagination"
|
|
13
|
+
:show-page-select="props.showPageSelect"
|
|
14
|
+
:labels="props.labels"
|
|
15
|
+
@emit:change-page="emit('emit:changePage', $event)"
|
|
16
|
+
/>
|
|
17
|
+
<div class="dwui-data-table-tools">
|
|
18
|
+
<span
|
|
19
|
+
v-if="props.showTotal"
|
|
20
|
+
class="dwui-data-table-total"
|
|
21
|
+
data-test-total-count
|
|
22
|
+
>
|
|
23
|
+
<span>{{ props.labels.totalCount }}</span>
|
|
24
|
+
<span class="dwui-data-table-total-count">{{ formattedTotalCount }}</span>
|
|
25
|
+
<span>{{ props.labels.totalUnit }}</span>
|
|
26
|
+
</span>
|
|
27
|
+
<label
|
|
28
|
+
v-if="props.showLimit"
|
|
29
|
+
class="dwui-data-table-limit"
|
|
30
|
+
>
|
|
31
|
+
<span>{{ props.labels.limit }}</span>
|
|
32
|
+
<input
|
|
33
|
+
type="number"
|
|
34
|
+
:value="props.limit"
|
|
35
|
+
min="1"
|
|
36
|
+
:max="props.maxLimit"
|
|
37
|
+
step="1"
|
|
38
|
+
data-test-view-count
|
|
39
|
+
@change="onChangeLimit"
|
|
40
|
+
>
|
|
41
|
+
</label>
|
|
42
|
+
<button
|
|
43
|
+
v-if="props.showDownloadPage"
|
|
44
|
+
type="button"
|
|
45
|
+
:title="props.labels.downloadPage"
|
|
46
|
+
:aria-label="props.labels.downloadPage"
|
|
47
|
+
data-test-download-page
|
|
48
|
+
@click="emit('emit:downloadPage')"
|
|
49
|
+
>
|
|
50
|
+
<SvgFolderDownload />
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
v-if="props.showDownloadAll"
|
|
54
|
+
type="button"
|
|
55
|
+
:title="props.labels.downloadAll"
|
|
56
|
+
:aria-label="props.labels.downloadAll"
|
|
57
|
+
data-test-download-all
|
|
58
|
+
@click="emit('emit:downloadAll')"
|
|
59
|
+
>
|
|
60
|
+
<SvgCloudDownload />
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script setup lang="ts">
|
|
67
|
+
import { computed } from 'vue';
|
|
68
|
+
import { SvgCloudDownload, SvgFolderDownload } from '@digitalwalletcorp/vue-svg-icons';
|
|
69
|
+
import Pagination from '@/internal/pagination.vue';
|
|
70
|
+
import type { DataTableResolvedLabels } from '@/types/data-table';
|
|
71
|
+
|
|
72
|
+
interface Props {
|
|
73
|
+
/** ページネーションを表示するか */
|
|
74
|
+
pagination: boolean;
|
|
75
|
+
/** 総件数を表示するか */
|
|
76
|
+
showTotal: boolean;
|
|
77
|
+
/** 1ページあたりの表示件数の入力を表示するか */
|
|
78
|
+
showLimit: boolean;
|
|
79
|
+
/** ページ番号の選択を表示するか */
|
|
80
|
+
showPageSelect: boolean;
|
|
81
|
+
/** ページダウンロードボタンを表示するか */
|
|
82
|
+
showDownloadPage: boolean;
|
|
83
|
+
/** 全件ダウンロードボタンを表示するか */
|
|
84
|
+
showDownloadAll: boolean;
|
|
85
|
+
/** 現在ページ */
|
|
86
|
+
current: number;
|
|
87
|
+
/** 1ページあたりの表示件数 */
|
|
88
|
+
limit: number;
|
|
89
|
+
/** 1ページあたりの表示件数の上限 */
|
|
90
|
+
maxLimit: number;
|
|
91
|
+
/** 並べるページ番号の最大数 */
|
|
92
|
+
maxPagination: number;
|
|
93
|
+
/** 総件数 */
|
|
94
|
+
totalCount: number;
|
|
95
|
+
/** 表示する文言 */
|
|
96
|
+
labels: DataTableResolvedLabels;
|
|
97
|
+
/** 総件数の整形。未指定なら en-US の桁区切り */
|
|
98
|
+
formatTotalCount?: (count: number) => string;
|
|
99
|
+
}
|
|
100
|
+
const props = defineProps<Props>();
|
|
101
|
+
|
|
102
|
+
interface Emits {
|
|
103
|
+
(e: 'emit:changePage', page: number): void;
|
|
104
|
+
(e: 'emit:changeLimit', limit: number): void;
|
|
105
|
+
(e: 'emit:downloadPage'): void;
|
|
106
|
+
(e: 'emit:downloadAll'): void;
|
|
107
|
+
}
|
|
108
|
+
const emit = defineEmits<Emits>();
|
|
109
|
+
|
|
110
|
+
// 既定の書式は SSR とクライアントで同じ文字列になるよう、ロケールを固定する
|
|
111
|
+
const numberFormat = new Intl.NumberFormat('en-US');
|
|
112
|
+
|
|
113
|
+
const formattedTotalCount = computed((): string => {
|
|
114
|
+
return props.formatTotalCount ? props.formatTotalCount(props.totalCount) : numberFormat.format(props.totalCount);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 入力された表示件数を 1 〜 maxLimit に収めて通知する。入力欄にも収めた値を戻す
|
|
119
|
+
*/
|
|
120
|
+
const onChangeLimit = (event: Event): void => {
|
|
121
|
+
const input = event.target as HTMLInputElement;
|
|
122
|
+
const limit = Math.min(Math.max(Math.trunc(Number(input.value)) || 1, 1), props.maxLimit);
|
|
123
|
+
input.value = String(limit);
|
|
124
|
+
if (limit !== props.limit) {
|
|
125
|
+
emit('emit:changeLimit', limit);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<style>
|
|
131
|
+
:where(.dwui-data-table-controls) {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-wrap: wrap;
|
|
134
|
+
/* ページネーションとツールを同じ高さにし、中のボタンをその高さまで伸ばす */
|
|
135
|
+
align-items: stretch;
|
|
136
|
+
justify-content: space-between;
|
|
137
|
+
gap: 0.33em 0.67em;
|
|
138
|
+
margin: 0.33em 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
:where(.dwui-data-table-tools) {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: flex-end;
|
|
145
|
+
gap: 0.33em;
|
|
146
|
+
margin-left: auto;
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
:where(.dwui-data-table-total) {
|
|
151
|
+
margin-right: 0.67em;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
:where(.dwui-data-table-total-count) {
|
|
155
|
+
margin: 0 0.5em;
|
|
156
|
+
color: var(--dwui-color-text-data-table-total-count, inherit);
|
|
157
|
+
font-weight: var(--dwui-font-weight-data-table-total-count, bold);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
:where(.dwui-data-table-limit) {
|
|
161
|
+
display: inline-flex;
|
|
162
|
+
align-self: stretch;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 0.33em;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
:where(.dwui-data-table-limit input) {
|
|
168
|
+
align-self: stretch;
|
|
169
|
+
width: 60px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
:where(.dwui-data-table-tools button) {
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
align-self: stretch;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: center;
|
|
177
|
+
box-sizing: border-box;
|
|
178
|
+
/* 高さは行にそろえ、幅は高さと同じにして正方形にする */
|
|
179
|
+
aspect-ratio: 1;
|
|
180
|
+
padding: 0;
|
|
181
|
+
border: 1px solid var(--dwui-border-color-data-table-button, light-dark(#d3d3d3, #5a5a5a));
|
|
182
|
+
border-radius: 3px;
|
|
183
|
+
background: var(--dwui-background-data-table-button, light-dark(#fafafa, #2a2a2a));
|
|
184
|
+
color: var(--dwui-color-text-data-table-button, inherit);
|
|
185
|
+
font: inherit;
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
:where(.dwui-data-table-tools button svg) {
|
|
190
|
+
/* アイコンはボタンの内側に対する比率で描く */
|
|
191
|
+
width: auto;
|
|
192
|
+
height: 85%;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
:where(.dwui-data-table-tools button:hover) {
|
|
196
|
+
background: var(--dwui-background-data-table-button-hover, light-dark(#e8e8e8, #3a3a3a));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
:where(.dwui-data-table-tools button:focus-visible) {
|
|
200
|
+
outline: var(--dwui-outline-width-focus-ring, 2px) solid var(--dwui-outline-color-focus-ring, Highlight);
|
|
201
|
+
outline-offset: var(--dwui-outline-offset-focus-ring, 2px);
|
|
202
|
+
}
|
|
203
|
+
</style>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { DataTableValueType } from '@/types/data-table';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ドット区切りのパスで入れ子の値を取り出す。途中が null なら undefined
|
|
5
|
+
*
|
|
6
|
+
* @param {unknown} obj
|
|
7
|
+
* @param {string} path 'a.b.c' の形。'a[0].b' は 'a.0.b' と同じ
|
|
8
|
+
* @returns {unknown}
|
|
9
|
+
*/
|
|
10
|
+
export const getValueByPath = (obj: unknown, path: string): unknown => {
|
|
11
|
+
let value: unknown = obj;
|
|
12
|
+
for (const key of path.replace(/\[(\w+)\]/g, '.$1').split('.')) {
|
|
13
|
+
if (value == null) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
value = (value as Record<string, unknown>)[key];
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 文字列または文字列を返す関数から文字列を得る
|
|
23
|
+
*
|
|
24
|
+
* @param {string | (() => string)} label
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export const resolveLabel = (label: string | (() => string) | undefined): string => {
|
|
28
|
+
if (typeof label === 'function') {
|
|
29
|
+
return label();
|
|
30
|
+
}
|
|
31
|
+
return label ?? '';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const toBoolean = (value: unknown): boolean => {
|
|
35
|
+
return value === true || value === 'true' || value === '1' || value === 'on' || value === 1;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* カンマ付きの文字列も数値として扱うため、符号・数字・小数点だけを残して数値化する
|
|
40
|
+
*/
|
|
41
|
+
const toNumber = (value: unknown): number => {
|
|
42
|
+
if (typeof value === 'number') {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
return Number(String(value).replace(/[^+\-\d.]/g, ''));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 型に応じて2つの値を比較する。null / undefined は最小として扱う
|
|
50
|
+
*
|
|
51
|
+
* @param {unknown} a
|
|
52
|
+
* @param {unknown} b
|
|
53
|
+
* @param {DataTableValueType} type
|
|
54
|
+
* @returns {number} a が前なら負の数、後ろなら正の数、等しければ 0
|
|
55
|
+
*/
|
|
56
|
+
export const compareValues = (a: unknown, b: unknown, type: DataTableValueType): number => {
|
|
57
|
+
if (a == null && b == null) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
if (a == null) {
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
|
63
|
+
if (b == null) {
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
if (a === b) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
switch (type) {
|
|
70
|
+
case 'boolean':
|
|
71
|
+
return (toBoolean(a) ? 1 : 0) - (toBoolean(b) ? 1 : 0);
|
|
72
|
+
case 'number': {
|
|
73
|
+
const numA = toNumber(a);
|
|
74
|
+
const numB = toNumber(b);
|
|
75
|
+
if (Number.isNaN(numA) || Number.isNaN(numB)) {
|
|
76
|
+
return (Number.isNaN(numA) ? 0 : 1) - (Number.isNaN(numB) ? 0 : 1);
|
|
77
|
+
}
|
|
78
|
+
return numA - numB;
|
|
79
|
+
}
|
|
80
|
+
case 'object':
|
|
81
|
+
return stringifyValue(a).localeCompare(stringifyValue(b));
|
|
82
|
+
default:
|
|
83
|
+
return String(a).localeCompare(String(b));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 行の同一性の判定に使う文字列にする。bigint は文字列にし、循環参照は空文字にする
|
|
89
|
+
*
|
|
90
|
+
* @param {unknown} value
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export const stringifyValue = (value: unknown): string => {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.stringify(value, (_, v) => typeof v === 'bigint' ? v.toString() : v) ?? '';
|
|
96
|
+
} catch {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
フローティングコンポーネント
|
|
3
|
+
ページの上に、見出しと閉じるボタンの付いたウィンドウを浮かせて表示する。暗幕は掛けず、背面のページも操作できる。
|
|
4
|
+
位置はページ基準の絶対座標で決めるため、showFloatingComponent / showFloatingMessage から body 直下に描画して使う。
|
|
5
|
+
OverlayBox とは開いた順に重なり、後から開いたものが前面に出る。クリックすると最前面に出る。
|
|
6
|
+
-->
|
|
7
|
+
<template>
|
|
8
|
+
<Transition
|
|
9
|
+
name="dwui-floating-component-fade"
|
|
10
|
+
@after-leave="emit('emit:close')"
|
|
11
|
+
>
|
|
12
|
+
<MovableWindow
|
|
13
|
+
v-if="props.open"
|
|
14
|
+
ref="refWindow"
|
|
15
|
+
class="dwui-floating-component"
|
|
16
|
+
:class="{ 'dwui-lock-scroll': props.lockScroll }"
|
|
17
|
+
name="dwui-floating-component"
|
|
18
|
+
:style="{ '--dwui-order-window': order }"
|
|
19
|
+
:drag-mode="props.dragMode"
|
|
20
|
+
:loading="props.loading"
|
|
21
|
+
:close-label="props.closeLabel"
|
|
22
|
+
:clamp-bottom="false"
|
|
23
|
+
:header-class="props.headerClass"
|
|
24
|
+
:header-style="props.headerStyle"
|
|
25
|
+
@pointerdown.capture="onPointerDownWindow"
|
|
26
|
+
@dblclick="onDblClickWindow"
|
|
27
|
+
@emit:close="emit('update:open', false)"
|
|
28
|
+
>
|
|
29
|
+
<template #title>
|
|
30
|
+
{{ props.title }}
|
|
31
|
+
</template>
|
|
32
|
+
<template
|
|
33
|
+
v-if="props.copyValue != null"
|
|
34
|
+
#actions
|
|
35
|
+
>
|
|
36
|
+
<CopyButton
|
|
37
|
+
class="dwui-floating-component-copy-button"
|
|
38
|
+
:value="props.copyValue"
|
|
39
|
+
:aria-label="props.copyLabel"
|
|
40
|
+
/>
|
|
41
|
+
</template>
|
|
42
|
+
<slot />
|
|
43
|
+
</MovableWindow>
|
|
44
|
+
</Transition>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup lang="ts">
|
|
48
|
+
import { onMounted, ref } from 'vue';
|
|
49
|
+
import type { ClassValue, StyleValue } from 'vue';
|
|
50
|
+
import CopyButton from '@/copy-button.vue';
|
|
51
|
+
import { computeAnchoredPosition } from '@/internal/anchored-position';
|
|
52
|
+
import MovableWindow from '@/internal/movable-window.vue';
|
|
53
|
+
import { nextWindowOrder } from '@/internal/window-order';
|
|
54
|
+
import type { MovableWindowDragMode } from '@/types/movable-window';
|
|
55
|
+
|
|
56
|
+
interface Props {
|
|
57
|
+
/** 表示するか */
|
|
58
|
+
open: boolean;
|
|
59
|
+
/** 見出し */
|
|
60
|
+
title?: string;
|
|
61
|
+
/** この要素の下に開く。下に収まらなければ上に開く。未指定ならページの表示中の左上に開く */
|
|
62
|
+
anchor?: HTMLElement;
|
|
63
|
+
/** ドラッグで移動するときに掴む範囲。未指定なら移動できない */
|
|
64
|
+
dragMode?: MovableWindowDragMode;
|
|
65
|
+
/** ウィンドウに暗幕とスピナーを重ね、中身を操作できなくする */
|
|
66
|
+
loading: boolean;
|
|
67
|
+
/** 閉じるボタンの読み上げ名 */
|
|
68
|
+
closeLabel: string;
|
|
69
|
+
/** 見出しにコピーボタンを置き、この値をコピーする */
|
|
70
|
+
copyValue?: string | number | (() => string | number);
|
|
71
|
+
/** コピーボタンの読み上げ名 */
|
|
72
|
+
copyLabel: string;
|
|
73
|
+
/** ウィンドウのダブルクリックで閉じる */
|
|
74
|
+
closeOnDblClick: boolean;
|
|
75
|
+
/** 開いている間、背面のページをスクロールさせない */
|
|
76
|
+
lockScroll: boolean;
|
|
77
|
+
/** 初期の幅・高さ(px)。未指定なら中身に合わせる */
|
|
78
|
+
initialSize?: {
|
|
79
|
+
width?: number;
|
|
80
|
+
height?: number;
|
|
81
|
+
};
|
|
82
|
+
/** 見出しに付けるクラス */
|
|
83
|
+
headerClass?: ClassValue;
|
|
84
|
+
/** 見出しに付けるスタイル */
|
|
85
|
+
headerStyle?: StyleValue;
|
|
86
|
+
}
|
|
87
|
+
const props = defineProps<Props>();
|
|
88
|
+
|
|
89
|
+
interface Emits {
|
|
90
|
+
/** 閉じる操作がされた */
|
|
91
|
+
(e: 'update:open', value: boolean): void;
|
|
92
|
+
/** 閉じるアニメーションが終わり、ウィンドウが取り除かれた */
|
|
93
|
+
(e: 'emit:close'): void;
|
|
94
|
+
}
|
|
95
|
+
const emit = defineEmits<Emits>();
|
|
96
|
+
|
|
97
|
+
const refWindow = ref<InstanceType<typeof MovableWindow>>();
|
|
98
|
+
const order = ref(nextWindowOrder());
|
|
99
|
+
|
|
100
|
+
const onPointerDownWindow = (): void => {
|
|
101
|
+
order.value = nextWindowOrder();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onDblClickWindow = (): void => {
|
|
105
|
+
if (props.closeOnDblClick) {
|
|
106
|
+
emit('update:open', false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 初期の寸法と位置を決める。寸法は画面に収まるよう 90% までに抑える
|
|
112
|
+
*/
|
|
113
|
+
const place = (): void => {
|
|
114
|
+
const movable = refWindow.value;
|
|
115
|
+
if (!movable?.element) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const rect = movable.element.getBoundingClientRect();
|
|
119
|
+
let width = props.initialSize?.width ?? rect.width;
|
|
120
|
+
if (window.innerWidth < width) {
|
|
121
|
+
width = window.innerWidth * 0.9;
|
|
122
|
+
}
|
|
123
|
+
let height = props.initialSize?.height ?? -1;
|
|
124
|
+
if (window.innerHeight < height) {
|
|
125
|
+
height = window.innerHeight * 0.9;
|
|
126
|
+
}
|
|
127
|
+
movable.size.width = width;
|
|
128
|
+
// 高さは指定があるときだけ固定する。未指定なら、幅が確定した後のレイアウトで中身に合わせた高さになる
|
|
129
|
+
movable.size.height = height;
|
|
130
|
+
|
|
131
|
+
const scrollLeft = document.documentElement.scrollLeft;
|
|
132
|
+
const scrollTop = document.documentElement.scrollTop;
|
|
133
|
+
if (props.anchor) {
|
|
134
|
+
// 寸法の指定はまだ描画に反映されていないため、指定があればその値で位置を決める
|
|
135
|
+
const panel = {
|
|
136
|
+
width,
|
|
137
|
+
height: 0 <= height ? height : rect.height
|
|
138
|
+
};
|
|
139
|
+
const viewport = {
|
|
140
|
+
width: document.documentElement.clientWidth,
|
|
141
|
+
height: document.documentElement.clientHeight
|
|
142
|
+
};
|
|
143
|
+
const position = computeAnchoredPosition(props.anchor.getBoundingClientRect(), panel, viewport, {
|
|
144
|
+
placement: 'bottom-start',
|
|
145
|
+
offset: 0,
|
|
146
|
+
margin: 0,
|
|
147
|
+
arrowMargin: 0
|
|
148
|
+
});
|
|
149
|
+
movable.position.x = position.left + scrollLeft;
|
|
150
|
+
movable.position.y = position.top + scrollTop;
|
|
151
|
+
} else {
|
|
152
|
+
movable.position.x = 10 + scrollLeft;
|
|
153
|
+
movable.position.y = 10 + scrollTop;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
onMounted(() => {
|
|
158
|
+
if (props.open) {
|
|
159
|
+
place();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<style>
|
|
165
|
+
:where(html:has(.dwui-floating-component.dwui-lock-scroll), html:has(.dwui-floating-component.dwui-lock-scroll) > body) {
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
:where(.dwui-floating-component) {
|
|
170
|
+
z-index: calc(var(--dwui-z-index-floating-component, 5000) + var(--dwui-order-window, 0));
|
|
171
|
+
--dwui-min-width-movable-window: var(--dwui-min-width-floating-component, 200px);
|
|
172
|
+
--dwui-max-height-movable-window: var(--dwui-max-height-floating-component, 90vh);
|
|
173
|
+
--dwui-border-color-movable-window: var(--dwui-border-color-floating-component, light-dark(#8f8f8f, #6b6f76));
|
|
174
|
+
--dwui-radius-movable-window: var(--dwui-radius-floating-component, 4px);
|
|
175
|
+
--dwui-background-movable-window: var(--dwui-background-floating-component, Canvas);
|
|
176
|
+
--dwui-color-text-movable-window: var(--dwui-color-text-floating-component, CanvasText);
|
|
177
|
+
--dwui-box-shadow-movable-window: var(--dwui-box-shadow-floating-component, 0 2px 8px rgba(0, 0, 0, 0.33));
|
|
178
|
+
--dwui-padding-movable-window-header: var(--dwui-padding-floating-component-header, 0.17em 0.33em);
|
|
179
|
+
--dwui-background-movable-window-header: var(--dwui-background-floating-component-header, transparent);
|
|
180
|
+
--dwui-color-text-movable-window-header: var(--dwui-color-text-floating-component-header, inherit);
|
|
181
|
+
--dwui-background-movable-window-close-button-hover: var(--dwui-background-floating-component-close-button-hover, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.12)));
|
|
182
|
+
--dwui-padding-movable-window-body: var(--dwui-padding-floating-component-body, 0.4em);
|
|
183
|
+
--dwui-color-text-movable-window-resizer: var(--dwui-color-text-floating-component-resizer, GrayText);
|
|
184
|
+
--dwui-background-movable-window-loading: var(--dwui-background-floating-component-loading, light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.4)));
|
|
185
|
+
--dwui-size-movable-window-spinner: var(--dwui-size-floating-component-spinner, 48px);
|
|
186
|
+
--dwui-border-color-movable-window-spinner: var(--dwui-border-color-floating-component-spinner, light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.2)));
|
|
187
|
+
--dwui-border-color-movable-window-spinner-accent: var(--dwui-border-color-floating-component-spinner-accent, light-dark(rgba(47, 90, 74, 0.8), rgba(140, 200, 170, 0.8)));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
:where(.dwui-floating-component-message) {
|
|
191
|
+
overflow-wrap: anywhere;
|
|
192
|
+
white-space: pre-wrap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
:where(.dwui-floating-component-fade-enter-active, .dwui-floating-component-fade-leave-active) {
|
|
196
|
+
transition: opacity 0.25s ease;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
:where(.dwui-floating-component-fade-enter-from, .dwui-floating-component-fade-leave-to) {
|
|
200
|
+
opacity: 0;
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
アイコンにマウスを載せている、またはフォーカスしている間だけ中身を表示する(HelpPopper / WarningPopper の共通部分)
|
|
3
|
+
パネルにポインタを移している間も開いたままにし、中のリンクや文字の選択を操作できるようにする。
|
|
4
|
+
タッチ端末ではタップで開閉し、外をタップすると閉じる。
|
|
5
|
+
-->
|
|
6
|
+
<template>
|
|
7
|
+
<span
|
|
8
|
+
ref="refTrigger"
|
|
9
|
+
class="dwui-icon-popper"
|
|
10
|
+
tabindex="0"
|
|
11
|
+
:aria-label="props.label"
|
|
12
|
+
:aria-describedby="panelId"
|
|
13
|
+
v-bind="attrs"
|
|
14
|
+
@pointerenter="onPointerEnter"
|
|
15
|
+
@pointerleave="onPointerLeave"
|
|
16
|
+
@pointerdown="onPointerDown"
|
|
17
|
+
@focus="onFocus"
|
|
18
|
+
@blur="onBlur"
|
|
19
|
+
@keydown.esc="onKeydownEsc"
|
|
20
|
+
>
|
|
21
|
+
<slot name="icon" />
|
|
22
|
+
</span>
|
|
23
|
+
<AnchoredPopover
|
|
24
|
+
ref="refPopover"
|
|
25
|
+
:id="panelId"
|
|
26
|
+
class="dwui-icon-popper-popover"
|
|
27
|
+
:class="props.popoverClass"
|
|
28
|
+
role="tooltip"
|
|
29
|
+
mode="manual"
|
|
30
|
+
:anchor="refTrigger"
|
|
31
|
+
:placement="props.placement"
|
|
32
|
+
:arrow="props.arrow"
|
|
33
|
+
@pointerenter="onPointerEnterPanel"
|
|
34
|
+
@pointerleave="onPointerLeave"
|
|
35
|
+
@emit:open="onOpen"
|
|
36
|
+
@emit:close="onClose"
|
|
37
|
+
>
|
|
38
|
+
<slot />
|
|
39
|
+
</AnchoredPopover>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { onBeforeUnmount, ref, useAttrs, useId } from 'vue';
|
|
44
|
+
import AnchoredPopover from '@/internal/anchored-popover.vue';
|
|
45
|
+
import type { PopoverPlacement } from '@/internal/anchored-position';
|
|
46
|
+
|
|
47
|
+
// ルートがトリガーとパネルの2つなので、属性はトリガーに明示的に渡す
|
|
48
|
+
defineOptions({ inheritAttrs: false });
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
/** アイコンに対するパネルの表示位置。収まらないときは反対側に出す */
|
|
52
|
+
placement: PopoverPlacement;
|
|
53
|
+
/** アイコンを指す矢印をパネルに付ける */
|
|
54
|
+
arrow: boolean;
|
|
55
|
+
/** パネルに付けるクラス。利用側のコンポーネントがパネルのスタイル変数を割り当てるのに使う */
|
|
56
|
+
popoverClass: string;
|
|
57
|
+
/** アイコンの読み上げ名。利用側が aria-label 属性を渡したときはそちらが優先される */
|
|
58
|
+
label: string;
|
|
59
|
+
}
|
|
60
|
+
const props = defineProps<Props>();
|
|
61
|
+
|
|
62
|
+
interface Emits {
|
|
63
|
+
(e: 'emit:open'): void;
|
|
64
|
+
(e: 'emit:close'): void;
|
|
65
|
+
}
|
|
66
|
+
const emit = defineEmits<Emits>();
|
|
67
|
+
|
|
68
|
+
const attrs = useAttrs();
|
|
69
|
+
|
|
70
|
+
const refTrigger = ref<HTMLElement>();
|
|
71
|
+
const refPopover = ref<InstanceType<typeof AnchoredPopover>>();
|
|
72
|
+
const open = ref(false);
|
|
73
|
+
|
|
74
|
+
// スクリーンリーダーがフォーカス時にパネルの文章を読み上げられるように、トリガーとパネルを aria-describedby で結ぶ
|
|
75
|
+
const panelId = useId();
|
|
76
|
+
|
|
77
|
+
// アイコンとパネルの隙間をポインタが横切る間に閉じないよう、閉じるのを少し待つ
|
|
78
|
+
const HIDE_DELAY = 150;
|
|
79
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
|
80
|
+
|
|
81
|
+
const cancelHide = (): void => {
|
|
82
|
+
if (hideTimer !== undefined) {
|
|
83
|
+
clearTimeout(hideTimer);
|
|
84
|
+
hideTimer = undefined;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const show = (): void => {
|
|
89
|
+
cancelHide();
|
|
90
|
+
refPopover.value?.show();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const hide = (): void => {
|
|
94
|
+
cancelHide();
|
|
95
|
+
refPopover.value?.hide();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const scheduleHide = (): void => {
|
|
99
|
+
cancelHide();
|
|
100
|
+
hideTimer = setTimeout(() => {
|
|
101
|
+
hideTimer = undefined;
|
|
102
|
+
refPopover.value?.hide();
|
|
103
|
+
}, HIDE_DELAY);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// タッチ端末ではトリガーとパネルの外をタップしたら閉じる
|
|
107
|
+
const onPointerDownOutside = (event: PointerEvent): void => {
|
|
108
|
+
const target = event.target as Node;
|
|
109
|
+
if (!refTrigger.value?.contains(target) && !refPopover.value?.element?.contains(target)) {
|
|
110
|
+
hide();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// タップ後にブラウザが互換のために発行するマウス系のイベントで開き直さないように、タッチはホバーとして扱わない
|
|
115
|
+
const onPointerEnter = (event: PointerEvent): void => {
|
|
116
|
+
if (event.pointerType !== 'touch') {
|
|
117
|
+
show();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const onPointerLeave = (event: PointerEvent): void => {
|
|
122
|
+
if (event.pointerType !== 'touch') {
|
|
123
|
+
scheduleHide();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const onPointerEnterPanel = (event: PointerEvent): void => {
|
|
128
|
+
if (event.pointerType !== 'touch') {
|
|
129
|
+
cancelHide();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const onPointerDown = (event: PointerEvent): void => {
|
|
134
|
+
if (event.pointerType === 'touch') {
|
|
135
|
+
if (open.value) {
|
|
136
|
+
hide();
|
|
137
|
+
} else {
|
|
138
|
+
show();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const onFocus = (): void => {
|
|
144
|
+
show();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const onBlur = (): void => {
|
|
148
|
+
hide();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const onKeydownEsc = (): void => {
|
|
152
|
+
hide();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const onOpen = (): void => {
|
|
156
|
+
open.value = true;
|
|
157
|
+
document.addEventListener('pointerdown', onPointerDownOutside, true);
|
|
158
|
+
emit('emit:open');
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const onClose = (): void => {
|
|
162
|
+
open.value = false;
|
|
163
|
+
document.removeEventListener('pointerdown', onPointerDownOutside, true);
|
|
164
|
+
emit('emit:close');
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
onBeforeUnmount(() => {
|
|
168
|
+
cancelHide();
|
|
169
|
+
document.removeEventListener('pointerdown', onPointerDownOutside, true);
|
|
170
|
+
});
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<style>
|
|
174
|
+
:where(.dwui-icon-popper) {
|
|
175
|
+
display: inline-flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
vertical-align: middle;
|
|
178
|
+
cursor: help;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
:where(.dwui-icon-popper:focus-visible) {
|
|
182
|
+
outline: var(--dwui-outline-width-focus-ring, 2px) solid var(--dwui-outline-color-focus-ring, Highlight);
|
|
183
|
+
outline-offset: var(--dwui-outline-offset-focus-ring, 2px);
|
|
184
|
+
}
|
|
185
|
+
</style>
|