@mptw/skylens-ui 1.2.21 → 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.
@@ -1,5 +1,5 @@
1
1
  <template lang="pug">
2
- div
2
+ div(ref="el")
3
3
  .table(
4
4
  :class="{ 'table-responsive': responsive }"
5
5
  @mousemove="onMousemoved"
@@ -16,10 +16,12 @@
16
16
  )
17
17
  th(
18
18
  v-else
19
- :style="field._style"
20
- :class="[field._class, { sortable: field.sortable, sorting: field.key === sortBy, 'sort-desc': sortDesc}]"
21
- :colspan="headerColumnSpans[index]"
22
- :key="field.key"
19
+ :style="field._style",
20
+ :class="[field._class, { sortable: field.sortable, sorting: field.key === sortBy, 'sort-desc': sortDesc, filterable: field.filterable, filtering: filterBy !== ''}]",
21
+ :colspan="headerColumnSpans[index]",
22
+ :name="`${field.key}-header`",
23
+ :key="field.key",
24
+ @click.prevent="onClickedHeaderCol(field.key)"
23
25
  )
24
26
  .inline-flex.items-center.space-x-1
25
27
  SLElementWithTitle(v-if='field.info', :title="field.info")
@@ -27,6 +29,12 @@
27
29
  component(:is='field.sortable ? "a" : "div"', href='javascript:;' class='inline-flex items-center', @click="field.sortable && sort(field.key)")
28
30
  span {{ field.label }}
29
31
  SLIcon.sort-icon.ml-2(v-if="field.sortable" :icon="sortDesc ? 'descending' : 'ascending'")
32
+
33
+ SLIcon.filter-icon(
34
+ v-if="field.filterable"
35
+ class="ml-0.5"
36
+ :icon="filterBy !== '' ? 'filter-on' : 'filter'"
37
+ )
30
38
  tbody(:class="{ 'hoverable': hoverableRows, 'clickable': clickableRows }")
31
39
  template(v-for="(item, itemIndex) in bodyHeaderItems" :key="itemIndex")
32
40
  tr.body-header-row(
@@ -72,15 +80,38 @@
72
80
  ) {{ item[field.key] }}
73
81
  tr(v-if="items.length === 0")
74
82
  td(:colspan="fields.length")
75
- .my-28.text-base.text-gray-2.text-center {{ noItems }}
83
+ div(v-if="loading", style="min-height: 200px")
84
+ .flex.flex-col.items-center.py-8.px-4.space-y-3(v-else)
85
+ img(src="~/assets/images/commons/empty-box.png")
86
+ .text-lg.text-primary-light {{ noItems }}
87
+ .text-sm.text-gray-3(v-if="reason") 可能的原因:{{ reason }}
76
88
  SLLoading(
77
- v-if="loading"
78
- :boundaries="[{ sides: ['top'], query: 'td' }, { sides: ['bottom'], query: 'tbody' }]"
79
- )
89
+ v-if="loading"
90
+ :boundaries="[{ sides: ['top'], query: 'td' }, { sides: ['bottom'], query: 'tbody' }]"
91
+ )
92
+ .table-header-filter-popup(ref="popupEl")
93
+ .table-header-filter-popup-content
94
+ a.table-header-filter-item(
95
+ v-for="option in filterOptions"
96
+ href="javascript:;"
97
+ :class="{ active: filterBy === option.key }"
98
+ :key="option.key"
99
+ @click="onClickedFilter(option.key)"
100
+ )
101
+ .inline-flex.items-center.space-x-1
102
+ SLIcon(
103
+ v-if="option.icon"
104
+ :icon="option.icon"
105
+ :class="option._iconClass"
106
+ )
107
+ span(v-if="option.label") {{ option.label }}
80
108
  </template>
81
109
 
82
110
  <script lang="ts">
83
111
  import type { PropType } from 'vue';
112
+ import { fromEvent, Subscription } from 'rxjs';
113
+ import { createPopper } from '@popperjs/core';
114
+ import type { Instance as PopperInstance } from '@popperjs/core';
84
115
  import type ITableField from '@/interface/ITableField';
85
116
 
86
117
  export default defineComponent({
@@ -108,6 +139,10 @@
108
139
  type: Boolean,
109
140
  default: false,
110
141
  },
142
+ filterBy: {
143
+ type: String,
144
+ default: '',
145
+ },
111
146
  hoverableRows: {
112
147
  type: Boolean,
113
148
  default: false,
@@ -136,12 +171,38 @@
136
171
  type: String,
137
172
  default: 'No Items',
138
173
  },
174
+ reason: {
175
+ type: String,
176
+ default: '',
177
+ },
139
178
  },
179
+ emits: [
180
+ 'update:sortDesc',
181
+ 'update:sortBy',
182
+ 'update:filterBy',
183
+ 'row-clicked',
184
+ 'row-mouseentered',
185
+ 'row-mouseleaved',
186
+ 'table-mousemoved',
187
+ ],
140
188
  setup(props, { emit }) {
141
189
  const { fields, sortBy, sortDesc, clickableRows, hoverableRows } =
142
190
  toRefs(props);
143
191
 
192
+ const el = shallowRef<HTMLElement | null>(null);
193
+ const popupEl = shallowRef<HTMLElement | null>(null);
144
194
  const hoverRowIndex = ref(-1);
195
+ const filterOptions = ref<
196
+ {
197
+ key: string;
198
+ label?: string;
199
+ icon?: string;
200
+ }[]
201
+ >([]);
202
+
203
+ let popperInstance: PopperInstance | null = null;
204
+ let clickSubscriber: Subscription | null = null;
205
+ let filterHeaderCol: HTMLElement | null = null;
145
206
 
146
207
  const headerColumnSpans = computed(() => {
147
208
  return fields.value.map((field) => {
@@ -164,6 +225,22 @@
164
225
  emit('update:sortBy', fieldKey);
165
226
  }
166
227
  }
228
+
229
+ function onClickedHeaderCol(fieldKey: string) {
230
+ const field = fields.value.find((f) => f.key === fieldKey);
231
+ if (!field || (!field.sortable && !field.filterable)) return;
232
+
233
+ if (field.sortable) {
234
+ sort(field.key);
235
+ }
236
+
237
+ if (field.filterable) {
238
+ filterOptions.value = [...field.filterOptions];
239
+ filterHeaderCol = el.value.querySelector(`th[name=${field.key}-header]`)
240
+ .children[0] as HTMLElement;
241
+ show();
242
+ }
243
+ }
167
244
 
168
245
  function rowClicked(item: any, index: number, e: Event) {
169
246
  if (!clickableRows.value) return;
@@ -190,16 +267,103 @@
190
267
 
191
268
  emit('table-mousemoved', e);
192
269
  }
270
+
271
+ function subscribeClick() {
272
+ clickSubscriber = fromEvent(window, 'click').subscribe((e) => {
273
+ const { target } = e;
274
+ if (
275
+ filterHeaderCol &&
276
+ popupEl.value &&
277
+ !filterHeaderCol.contains(target as HTMLElement) &&
278
+ !popupEl.value.contains(target as HTMLElement)
279
+ ) {
280
+ hide();
281
+ }
282
+ });
283
+ }
284
+
285
+ function unsubscribeClick() {
286
+ if (clickSubscriber) {
287
+ clickSubscriber.unsubscribe();
288
+ }
289
+ clickSubscriber = null;
290
+ }
291
+
292
+ function show() {
293
+ if (!popupEl.value || !filterHeaderCol || !popperInstance) return;
294
+
295
+ popupEl.value.setAttribute('data-show', '');
296
+
297
+ subscribeClick();
298
+ popperInstance.state.elements.reference = filterHeaderCol;
299
+ popperInstance.setOptions({
300
+ modifiers: [
301
+ {
302
+ name: 'offset',
303
+ options: {
304
+ offset: [0, 4],
305
+ },
306
+ },
307
+ { name: 'eventListeners', enabled: true },
308
+ ],
309
+ });
310
+ popperInstance.update();
311
+ }
312
+
313
+ function hide() {
314
+ if (
315
+ !popupEl.value ||
316
+ popupEl.value.getAttribute('data-show') === null ||
317
+ !popperInstance
318
+ )
319
+ return;
320
+
321
+ unsubscribeClick();
322
+
323
+ if (popupEl.value) {
324
+ popupEl.value.removeAttribute('data-show');
325
+ }
326
+
327
+ popperInstance.setOptions({
328
+ modifiers: [{ name: 'eventListeners', enabled: false }],
329
+ });
330
+ }
331
+
332
+ function onClickedFilter(filterKey: string) {
333
+ emit('update:filterBy', filterKey);
334
+ hide();
335
+ }
336
+
337
+ onMounted(() => {
338
+ if (el.value && popupEl.value) {
339
+ popperInstance = createPopper(el.value, popupEl.value, {
340
+ placement: 'bottom-end',
341
+ modifiers: [{ name: 'eventListeners', enabled: false }],
342
+ });
343
+ }
344
+ });
345
+
346
+ onBeforeUnmount(() => {
347
+ if (popperInstance) {
348
+ popperInstance.destroy();
349
+ }
350
+ popperInstance = null;
351
+ });
193
352
 
194
353
  return {
354
+ el,
355
+ popupEl,
195
356
  hoverRowIndex,
357
+ filterOptions,
196
358
  headerColumnSpans,
197
359
  columnSpans,
360
+ onClickedHeaderCol,
198
361
  sort,
199
362
  rowClicked,
200
363
  rowMouseEntered,
201
364
  rowMouseLeaved,
202
365
  onMousemoved,
366
+ onClickedFilter,
203
367
  };
204
368
  },
205
369
  });
@@ -225,15 +389,18 @@
225
389
  th
226
390
  @apply px-2 pb-2 text-base font-normal text-primary whitespace-nowrap
227
391
 
228
- &.sortable
229
- cursor pointer
392
+ &.sortable, &.filterable
393
+ @apply cursor-pointer
230
394
  .sort-icon
231
- opacity 0
395
+ @apply opacity-0
232
396
  transition transform 0.2s ease, opacity 0.1s ease
233
397
  &:hover,
234
398
  &.sorting
235
399
  .sort-icon
236
- opacity 1
400
+ @apply opacity-100
401
+ &.filterable
402
+ .filter-icon
403
+ @apply opacity-100
237
404
 
238
405
  tbody
239
406
  &.clickable
@@ -250,5 +417,27 @@
250
417
 
251
418
  &.text-right.sortable
252
419
  @apply pr-8
420
+
421
+ .table-header-filter-popup
422
+ @apply z-10 flex border border-primary-light rounded overflow-hidden invisible pointer-events-none
423
+
424
+ &[data-show]
425
+ @apply visible pointer-events-auto
426
+
427
+ &-content
428
+ @apply flex flex-col
429
+
430
+ .table-header-filter-item
431
+ @apply inline-flex justify-center p-2 bg-white text-sm text-gray-3
432
+ @apply s('hover:bg-bg-middle')
433
+
434
+ &.active
435
+ @apply bg-bg text-primary
436
+
437
+ .icon
438
+ @apply text-primary
439
+
440
+ .icon
441
+ @apply text-base leading-none
253
442
  </style>
254
443
 
@@ -172,24 +172,22 @@ export default defineComponent({
172
172
  }
173
173
 
174
174
  function onInputCompositionEnd(e: CompositionEvent) {
175
- const target = e.target as HTMLInputElement;
176
- if (target) {
177
- if (Number.isInteger(lazy.value) || lazy.value) {
178
- debounceUpdateModelValue(target.value);
179
- }
175
+ isComposing.value = false;
180
176
 
181
- emit("update:modelValue", target.value);
182
- }
177
+ updateModelValue(e);
183
178
  }
184
179
 
185
180
  function onInputedModelValue(e: Event) {
186
181
  const inputEvent = e as InputEvent;
187
- if (isComposing.value && !inputEvent.isComposing) {
188
- isComposing.value = false;
182
+ if (isComposing.value || inputEvent.isComposing) {
189
183
  return;
190
184
  }
191
185
 
192
- const target = inputEvent.target as HTMLInputElement;
186
+ updateModelValue(e);
187
+ }
188
+
189
+ function updateModelValue(e: Event) {
190
+ const target = e.target as HTMLInputElement;
193
191
 
194
192
  if (target) {
195
193
  if (Number.isInteger(lazy.value) || lazy.value) {
@@ -0,0 +1,313 @@
1
+ <template lang="pug">
2
+ .two-layer-single-select(:class="[sizeClass]" ref="selectEl")
3
+ a(
4
+ href="javascript:;"
5
+ :class="{ selected: !!modelValue }"
6
+ @click="onClickedToggle"
7
+ ref="toggleEl"
8
+ data-cy="project-button-toggle"
9
+ ).toggle-button
10
+ span.button-title {{ placeholder }}
11
+ SLIcon(icon="caret-down")
12
+ SLTwoLayerSelect(
13
+ :search-placeholder='searchPlaceholder',
14
+ :first-layer-title='firstOptionTitle',
15
+ :second-layer-title='secondOptionTitle',
16
+ :model-value='modelValue',
17
+ :selected-first-layer-key='selectedFirstOption',
18
+ :query='queryProxy',
19
+ :first-layers='firstOptions',
20
+ :second-layers='secondOptions',
21
+ ref='popupComp',
22
+ @update:model-value="onInputedSecondOption",
23
+ @search="onSearch"
24
+ @select-first-layer="onClickedFirstOption"
25
+ )
26
+ </template>
27
+
28
+ <script lang="ts">
29
+ import type { PropType } from "vue";
30
+ import { fromEvent } from "rxjs";
31
+ import { createPopper } from "@popperjs/core";
32
+ import type { Placement } from "@popperjs/core";
33
+ import { SLTwoLayerSelect } from '#components';
34
+
35
+ type SLTwoLayerSelectType = InstanceType<typeof SLTwoLayerSelect>;
36
+
37
+ export default defineComponent({
38
+ name: "SLTwoLayerSingleSelect",
39
+ components: {},
40
+ props: {
41
+ size: {
42
+ type: String,
43
+ default: "",
44
+ validator: (val: string) => ["sm", ""].includes(val),
45
+ },
46
+ placeholder: {
47
+ type: String,
48
+ default: "",
49
+ },
50
+ searchPlaceholder: {
51
+ type: String,
52
+ default: "",
53
+ },
54
+ placement: {
55
+ type: String as PropType<Placement>,
56
+ default: "bottom-start",
57
+ },
58
+ firstOptionTitle: {
59
+ type: String,
60
+ default: "",
61
+ },
62
+ secondOptionTitle: {
63
+ type: String,
64
+ default: "",
65
+ },
66
+ modelValue: {
67
+ type: Object as PropType<{
68
+ firstLayer: string;
69
+ secondLayer: string;
70
+ } | null>,
71
+ default: () => [],
72
+ },
73
+ options: {
74
+ type: Array as PropType<
75
+ {
76
+ key: string;
77
+ name: string;
78
+ subOptions: { key: string; name: string }[];
79
+ }[]
80
+ >,
81
+ default: () => [],
82
+ },
83
+ query: {
84
+ type: String,
85
+ default: '',
86
+ }
87
+ },
88
+ emits: ["update:modelValue", "update:selecting", 'update:query'],
89
+ setup(props, { emit }) {
90
+ const { size, options, placement, query } = toRefs(props);
91
+
92
+ const selectEl = shallowRef<HTMLElement | null>(null);
93
+ const toggleEl = shallowRef<HTMLElement | null>(null);
94
+ const popupComp = shallowRef<SLTwoLayerSelectType | null>(null);
95
+ const selfQuery = ref(query.value);
96
+ const selectedFirstOption = ref<string | null>(null);
97
+ const isSelecting = ref(false);
98
+
99
+ let popperInstance: any = null;
100
+ let clickSubscriber: any = null;
101
+
102
+ const queryProxy = computed({
103
+ get() { return selfQuery.value },
104
+ set(newValue: string) {
105
+ emit('update:query', newValue);
106
+ selfQuery.value = newValue;
107
+ },
108
+ });
109
+ const sizeClass = computed(() => {
110
+ if (!size.value) return "";
111
+ return `two-layer-multi-select-${size.value}`;
112
+ });
113
+ const filteredOptions = computed(() => {
114
+ if (!queryProxy.value) {
115
+ return options.value;
116
+ }
117
+
118
+ const regexp = new RegExp(`[.]*${queryProxy.value}[.]*`, "ig");
119
+ return options.value
120
+ .map((option) => {
121
+ if (regexp.test(option.name)) {
122
+ return option;
123
+ }
124
+
125
+ return {
126
+ ...option,
127
+ subOptions: option.subOptions.filter((o) => regexp.test(o.name)),
128
+ };
129
+ })
130
+ .filter((option) => option.subOptions.length > 0);
131
+ });
132
+ const firstOptions = computed(() => {
133
+ return filteredOptions.value
134
+ });
135
+ const secondOptions = computed(() => {
136
+ const found = filteredOptions.value.find(
137
+ (option) =>
138
+ selectedFirstOption.value && option.key === selectedFirstOption.value
139
+ );
140
+ return found?.subOptions || [];
141
+ });
142
+
143
+ function onClickedFirstOption(key: string) {
144
+ selectedFirstOption.value = key;
145
+ }
146
+
147
+ function onInputedFirstOption(key: string) {
148
+ selectedFirstOption.value = key;
149
+ }
150
+
151
+ function onInputedSecondOption(newValue: {
152
+ firstLayer: string;
153
+ secondLayer: string;
154
+ } | null) {
155
+ emit("update:modelValue", newValue);
156
+ }
157
+
158
+ function onClearQuery() {
159
+ queryProxy.value = "";
160
+ }
161
+
162
+ function subscribeClick() {
163
+ clickSubscriber = fromEvent(window, "click").subscribe((e) => {
164
+ const { target } = e;
165
+ if (selectEl.value && !selectEl.value.contains(target as HTMLElement)) {
166
+ hide();
167
+ }
168
+ });
169
+ }
170
+
171
+ function unsubscribeClick() {
172
+ if (clickSubscriber) {
173
+ clickSubscriber.unsubscribe();
174
+ }
175
+ clickSubscriber = null;
176
+ }
177
+
178
+ function show() {
179
+ if (!popupComp.value?.$el) return;
180
+
181
+ isSelecting.value = true;
182
+
183
+ const body = document.querySelector("body");
184
+ if (body) {
185
+ body.classList.add("popup");
186
+ }
187
+
188
+ if (popupComp.value?.$el) {
189
+ popupComp.value.$el.setAttribute("data-show", "");
190
+ }
191
+ subscribeClick();
192
+ popperInstance.setOptions({
193
+ modifiers: [
194
+ {
195
+ name: "offset",
196
+ options: {
197
+ offset: [0, 8],
198
+ },
199
+ },
200
+ { name: "eventListeners", enabled: true },
201
+ ],
202
+ });
203
+ }
204
+
205
+ function hide() {
206
+ const body = document.querySelector("body");
207
+ if (body) {
208
+ body.classList.remove("popup");
209
+ }
210
+
211
+ if (!popupComp.value?.$el || popupComp.value?.$el.getAttribute("data-show") === null)
212
+ return;
213
+
214
+ isSelecting.value = false;
215
+
216
+ unsubscribeClick();
217
+
218
+ if (popupComp.value?.$el) {
219
+ popupComp.value?.$el.removeAttribute("data-show");
220
+ }
221
+ popperInstance.setOptions({
222
+ modifiers: [{ name: "eventListeners", enabled: false }],
223
+ });
224
+
225
+ queryProxy.value = "";
226
+ }
227
+
228
+ function onClickedToggle() {
229
+ if (!popupComp.value?.$el || popupComp.value?.$el.getAttribute("data-show") !== null) {
230
+ hide();
231
+ return;
232
+ }
233
+
234
+ show();
235
+ }
236
+
237
+ function onSearch(search: string) {
238
+ console.log("search", search === '');
239
+ queryProxy.value = search;
240
+ }
241
+
242
+ watch(isSelecting, () => {
243
+ emit("update:selecting", isSelecting.value);
244
+ });
245
+
246
+ onMounted(() => {
247
+ if (!popupComp.value?.$el) return;
248
+
249
+ if (selectEl.value && popupComp.value?.$el) {
250
+ popperInstance = createPopper(selectEl.value, popupComp.value?.$el, {
251
+ placement: placement.value as Placement,
252
+ modifiers: [{ name: "eventListeners", enabled: false }],
253
+ });
254
+ }
255
+ });
256
+ onBeforeUnmount(() => {
257
+ const body = document.querySelector("body");
258
+ if (body) {
259
+ body.classList.remove("popup");
260
+ }
261
+
262
+ if (popperInstance) {
263
+ popperInstance.destroy();
264
+ }
265
+ popperInstance = null;
266
+ });
267
+
268
+ return {
269
+ selectEl,
270
+ toggleEl,
271
+ popupComp,
272
+ queryProxy,
273
+ sizeClass,
274
+ selectedFirstOption,
275
+ firstOptions,
276
+ secondOptions,
277
+ onClickedFirstOption,
278
+ onInputedFirstOption,
279
+ onInputedSecondOption,
280
+ onClearQuery,
281
+ onClickedToggle,
282
+ onSearch,
283
+ };
284
+ },
285
+ });
286
+ </script>
287
+
288
+ <style lang="stylus">
289
+ .two-layer-single-select
290
+ @apply relative w-full
291
+
292
+ &.two-layer-single-select-sm
293
+ .toggle-button
294
+ @apply text-sm
295
+
296
+ .toggle-button
297
+ @apply relative flex items-center w-full s('p-1.75') s('space-x-1.75') border border-primary-light rounded text-base text-gray-4
298
+
299
+ &.selected
300
+ @apply text-gray-2
301
+
302
+ .button-title
303
+ @apply flex-grow
304
+
305
+ .icon
306
+ @apply flex-grow-0 flex-shrink-0 inline-flex text-xs text-gray-2 leading-none
307
+
308
+ .tw-layer-select
309
+ @apply z-10 invisible pointer-events-none
310
+
311
+ &[data-show]
312
+ @apply visible pointer-events-auto
313
+ </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mptw/skylens-ui",
3
3
  "type": "module",
4
- "version": "1.2.21",
4
+ "version": "1.3.1",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground",