@globalbrain/sefirot 2.0.0-draft.7 → 2.0.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 (95) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +6 -6
  3. package/lib/components/SAvatar.vue +17 -17
  4. package/lib/components/SButton.vue +512 -267
  5. package/lib/components/SButtonGroup.vue +149 -0
  6. package/lib/components/SDropdown.vue +26 -150
  7. package/lib/components/SDropdownSection.vue +48 -0
  8. package/lib/components/SDropdownSectionFilter.vue +189 -0
  9. package/lib/components/SDropdownSectionFilterItem.vue +21 -0
  10. package/lib/components/SDropdownSectionFilterItemAvatar.vue +31 -0
  11. package/lib/components/SDropdownSectionFilterItemText.vue +20 -0
  12. package/lib/components/SDropdownSectionMenu.vue +39 -0
  13. package/lib/components/SIcon.vue +13 -0
  14. package/lib/components/SInputBase.vue +31 -31
  15. package/lib/components/SInputCheckbox.vue +1 -1
  16. package/lib/components/SInputCheckboxes.vue +74 -0
  17. package/lib/components/SInputDate.vue +182 -0
  18. package/lib/components/SInputDropdown.vue +158 -157
  19. package/lib/components/SInputDropdownItem.vue +46 -48
  20. package/lib/components/{SInputDropdownItemUserTag.vue → SInputDropdownItemAvatar.vue} +43 -44
  21. package/lib/components/SInputDropdownItemText.vue +79 -16
  22. package/lib/components/SInputFile.vue +55 -60
  23. package/lib/components/SInputHMS.vue +120 -110
  24. package/lib/components/SInputNumber.vue +38 -9
  25. package/lib/components/SInputRadio.vue +39 -36
  26. package/lib/components/SInputRadios.vue +40 -53
  27. package/lib/components/SInputSelect.vue +3 -3
  28. package/lib/components/SInputSwitch.vue +193 -0
  29. package/lib/components/SInputSwitches.vue +88 -0
  30. package/lib/components/SInputText.vue +206 -62
  31. package/lib/components/SInputTextarea.vue +46 -32
  32. package/lib/components/SInputYMD.vue +123 -126
  33. package/lib/components/SMarkdown.vue +52 -0
  34. package/lib/components/SModal.vue +25 -63
  35. package/lib/components/SMount.vue +19 -0
  36. package/lib/components/SSheet.vue +49 -55
  37. package/lib/components/SSheetFooter.vue +1 -1
  38. package/lib/components/SSheetFooterAction.vue +24 -17
  39. package/lib/components/SSheetFooterActions.vue +1 -4
  40. package/lib/components/SSheetForm.vue +15 -0
  41. package/lib/components/SSheetMedium.vue +8 -10
  42. package/lib/components/SSheetTitle.vue +7 -14
  43. package/lib/components/SSnackbar.vue +55 -45
  44. package/lib/components/{SPortalSnackbars.vue → SSnackbars.vue} +17 -20
  45. package/lib/components/SStep.vue +106 -0
  46. package/lib/components/SSteps.vue +59 -0
  47. package/lib/components/STable.vue +241 -0
  48. package/lib/components/STableCell.vue +82 -0
  49. package/lib/components/STableCellAvatar.vue +69 -0
  50. package/lib/components/STableCellAvatars.vue +93 -0
  51. package/lib/components/STableCellDay.vue +40 -0
  52. package/lib/components/STableCellPill.vue +84 -0
  53. package/lib/components/STableCellText.vue +102 -0
  54. package/lib/components/STableColumn.vue +255 -0
  55. package/lib/components/STableFooter.vue +115 -0
  56. package/lib/components/STableHeader.vue +74 -0
  57. package/lib/components/STableItem.vue +38 -0
  58. package/lib/components/STooltip.vue +112 -0
  59. package/lib/composables/Dropdown.ts +40 -99
  60. package/lib/composables/Form.ts +21 -18
  61. package/lib/composables/Grid.ts +117 -0
  62. package/lib/composables/Markdown.ts +138 -0
  63. package/lib/composables/Step.ts +7 -0
  64. package/lib/composables/Table.ts +103 -0
  65. package/lib/composables/Tooltip.ts +91 -0
  66. package/lib/composables/Validation.ts +6 -7
  67. package/lib/composables/markdown/LinkPlugin.ts +45 -0
  68. package/lib/mixins/Sheet.ts +5 -3
  69. package/lib/stores/Snackbars.ts +48 -0
  70. package/lib/{assets/styles → styles}/base.css +0 -0
  71. package/lib/{assets/styles → styles}/bootstrap.css +1 -0
  72. package/lib/{assets/styles → styles}/variables.css +55 -48
  73. package/lib/support/Day.ts +8 -0
  74. package/lib/support/Num.ts +3 -0
  75. package/lib/support/Time.ts +5 -2
  76. package/lib/support/Utils.ts +4 -3
  77. package/lib/types/shims.d.ts +3 -0
  78. package/lib/validation/validators/requiredYmd.ts +1 -1
  79. package/lib/validation/validators/ymd.ts +4 -4
  80. package/package.json +62 -43
  81. package/CHANGELOG.md +0 -41
  82. package/lib/components/SDialog.vue +0 -140
  83. package/lib/components/SDropdownItem.vue +0 -78
  84. package/lib/components/SDropdownItemText.vue +0 -22
  85. package/lib/components/SDropdownItemUser.vue +0 -40
  86. package/lib/components/SInputDropdownItemTextTag.vue +0 -94
  87. package/lib/components/SInputDropdownItemUser.vue +0 -41
  88. package/lib/components/SPortalModals.vue +0 -74
  89. package/lib/composables/Dialog.ts +0 -38
  90. package/lib/composables/Modal.ts +0 -34
  91. package/lib/composables/Snackbar.ts +0 -18
  92. package/lib/store/Sefirot.ts +0 -17
  93. package/lib/store/dialog/index.ts +0 -42
  94. package/lib/store/modal/index.ts +0 -61
  95. package/lib/store/snackbars/index.ts +0 -70
@@ -1,119 +1,60 @@
1
- import Fuse from 'fuse.js'
2
- import { Ref, ComputedRef, ref, computed, isRef } from 'vue'
1
+ import { MaybeRef } from '@vueuse/core'
3
2
 
4
- export interface Dropdown {
5
- title?: string
6
- search?: Search
7
- closeOnClick: boolean
8
- selected?: ComputedRef<any>
9
- items: ComputedRef<Item[]>
10
- callback? (item: Item): void
11
- }
12
-
13
- export interface Search {
14
- placeholder: string
15
- missing: string
16
- value: Ref<string>
17
- onInput (text: string | null): void
18
- }
19
-
20
- export type Item = TextItem | UserItem
21
-
22
- export interface ItemBase {
23
- type: string
24
- value: any
25
- disabled?: boolean
26
- callback?: Function
27
- }
28
-
29
- export type ItemType = 'text' | 'user'
3
+ export type DropdownSection =
4
+ | DropdownSectionMenu
5
+ | DropdownSectionFilter
30
6
 
31
- export interface TextItem extends ItemBase {
32
- type: 'text'
33
- text: string
34
- }
7
+ export type DropdownSectionType = 'menu' | 'filter'
35
8
 
36
- export interface UserItem extends ItemBase {
37
- type: 'user'
38
- avatar: string
39
- name: string
9
+ export interface DropdownSectionBase {
10
+ type: DropdownSectionType
40
11
  }
41
12
 
42
- export interface UseDropdownOptions {
43
- title?: string
44
- search?: UseDropdownSearchOptions
45
- closeOnClick?: boolean
46
- selected?: ComputedRef<any>
47
- items: Item[] | ComputedRef<Item[]>
48
- callback? (item: Item): void
13
+ export interface DropdownSectionMenu extends DropdownSectionBase {
14
+ type: 'menu'
15
+ options: DropdownSectionMenuOption[]
49
16
  }
50
17
 
51
- export interface UseDropdownSearchOptions {
52
- placeholder: string
53
- missing: string
18
+ export interface DropdownSectionMenuOption {
19
+ label: string
20
+ onClick(): void
54
21
  }
55
22
 
56
- export function useDropdown(dropdown: UseDropdownOptions): Dropdown {
57
- const title = dropdown.title
58
- const search = dropdown.search && useSearch(dropdown.search)
59
- const closeOnClick = dropdown.closeOnClick ?? false
60
- const selected = dropdown.selected
61
- const items = useItems(dropdown.items, search)
62
- const callback = dropdown.callback
63
-
64
- return {
65
- title,
66
- search,
67
- closeOnClick,
68
- selected,
69
- items,
70
- callback
71
- }
23
+ export interface DropdownSectionFilter extends DropdownSectionBase {
24
+ type: 'filter'
25
+ search?: boolean
26
+ selected: MaybeRef<DropdownSectionFilterSelectedValue>
27
+ options: DropdownSectionFilterOption[]
28
+ onClick?(value: string | number | boolean): void
72
29
  }
73
30
 
74
- export function useSearch(search: UseDropdownSearchOptions): Search {
75
- const placeholder = search.placeholder
76
- const missing = search.missing
77
- const value = ref('')
78
-
79
- function onInput(text: string): void {
80
- value.value = text
81
- }
82
-
83
- return {
84
- placeholder,
85
- missing,
86
- value,
87
- onInput
88
- }
89
- }
90
-
91
- function useItems(items: Item[] | ComputedRef<Item[]>, search?: Search): ComputedRef<Item[]> {
92
- const fuse = computed(() => {
93
- return createFuse(isRef(items) ? items.value : items, search)
94
- })
95
-
96
- return computed(() => {
97
- const value = search?.value.value
31
+ export type DropdownSectionFilterSelectedValue =
32
+ | string
33
+ | number
34
+ | boolean
35
+ | null
36
+ | (string | number | boolean)[]
98
37
 
99
- if (!value) {
100
- return isRef(items) ? items.value : items
101
- }
38
+ export type DropdownSectionFilterOption =
39
+ | DropdownSectionFilterOptionText
40
+ | DropdownSectionFilterOptionAvatar
102
41
 
103
- return fuse.value!.search(value).map(result => result.item)
104
- })
42
+ export interface DropdownSectionFilterOptionBase {
43
+ type?: 'text' | 'avatar'
44
+ label: string
45
+ value: string | number | boolean
46
+ onClick?(value: string | number | boolean): void
105
47
  }
106
48
 
107
- function createFuse <T extends Item>(items: T[], search?: Search): Fuse<T> | null {
108
- return search
109
- ? new Fuse(items, { keys: ['text', 'name'], threshold: 0.3 })
110
- : null
49
+ export interface DropdownSectionFilterOptionText extends DropdownSectionFilterOptionBase {
50
+ type?: 'text'
111
51
  }
112
52
 
113
- export function useTextItem(item: Omit<TextItem, 'type'>): TextItem {
114
- return { type: 'text', ...item }
53
+ export interface DropdownSectionFilterOptionAvatar extends DropdownSectionFilterOptionBase {
54
+ type: 'avatar'
55
+ image?: string | null
115
56
  }
116
57
 
117
- export function useUserItem(item: Omit<UserItem, 'type'>): UserItem {
118
- return { type: 'user', ...item }
58
+ export function createDropdown(section: DropdownSection[]): DropdownSection[] {
59
+ return section
119
60
  }
@@ -1,32 +1,34 @@
1
- import { Ref, ToRefs, reactive, computed } from 'vue'
2
- import { cloneDeep } from '../support/Utils'
3
- import { useSnackbar } from './Snackbar'
4
- import { Validation, ExtractState, ValidationArgs, useValidation } from './Validation'
1
+ import cloneDeep from 'lodash-es/cloneDeep'
2
+ import { Ref, reactive } from 'vue'
3
+ import { useSnackbars } from '../stores/Snackbars'
4
+ import { Validation, useValidation } from './Validation'
5
5
 
6
- export interface Form<D> {
7
- data: D
8
- validation: Ref<Validation>
6
+ export interface Form<T extends Record<string, any>> {
7
+ data: T
8
+ validation: Ref<Validation<any, T>>
9
9
  init(): void
10
10
  reset(): void
11
11
  validate(): Promise<boolean>
12
12
  validateAndNotify(): Promise<boolean>
13
13
  }
14
14
 
15
- export interface UseFormOptions<D> {
16
- data: D
17
- rules?: Ref<any> | any
15
+ export interface UseFormOptions<T extends Record<string, any>> {
16
+ data: T,
17
+ rules?: Record<string, any>
18
18
  }
19
19
 
20
- export function useForm<D>(options: UseFormOptions<D>): Form<D> {
21
- const snackbar = useSnackbar()
20
+ export function useForm<
21
+ T extends Record<string, any>
22
+ >(options: UseFormOptions<T>): Form<T> {
23
+ const snackbars = useSnackbars()
22
24
 
23
25
  const initialData = cloneDeep(options.data)
24
26
 
25
- const data = reactive(options.data as any)
27
+ const data = reactive(options.data)
26
28
 
27
29
  const rules = options.rules ?? {}
28
30
 
29
- const validation = useValidation(data, rules as any)
31
+ const validation = useValidation(data, rules)
30
32
 
31
33
  function init(): void {
32
34
  Object.assign(data, initialData)
@@ -42,15 +44,16 @@ export function useForm<D>(options: UseFormOptions<D>): Form<D> {
42
44
  }
43
45
 
44
46
  async function validateAndNotify(): Promise<boolean> {
45
- const result = await validate()
47
+ const valid = await validate()
46
48
 
47
- if (!result) {
48
- snackbar.push({
49
+ if (!valid) {
50
+ snackbars.push({
51
+ mode: 'danger',
49
52
  text: 'Form contains errors. Please correct them and try again.'
50
53
  })
51
54
  }
52
55
 
53
- return result
56
+ return valid
54
57
  }
55
58
 
56
59
  return {
@@ -0,0 +1,117 @@
1
+ import { Ref, ref, watchEffect, onMounted, onUnmounted } from 'vue'
2
+
3
+ export interface Grid {
4
+ container: Ref<HTMLElement | null>
5
+ }
6
+
7
+ export interface UseGridOptions {
8
+ tag?: string
9
+ class?: string
10
+ type?: 'fill' | 'fit'
11
+ }
12
+
13
+ type CssStyles = Partial<Record<keyof CSSStyleDeclaration, string>>
14
+
15
+ export function useGrid(options: UseGridOptions): Grid {
16
+ const container: Ref<HTMLElement | null> = ref(null)
17
+
18
+ const spacerClass = options.class ? toClassName(options.class) : 'spacer'
19
+ const spacerTag = options.tag ?? 'div'
20
+ const type = options.type ?? 'fit'
21
+
22
+ const observer = new MutationObserver((_, observer) => {
23
+ observer.disconnect()
24
+
25
+ adjustSpacer()
26
+
27
+ observer.observe(container.value!, { childList: true })
28
+ })
29
+
30
+ watchEffect(() => {
31
+ observer.disconnect()
32
+
33
+ if (container.value) {
34
+ adjustSpacer()
35
+ observer.observe(container.value, { childList: true })
36
+ }
37
+ })
38
+
39
+ onMounted(() => {
40
+ window.addEventListener('resize', adjustSpacer)
41
+ })
42
+
43
+ onUnmounted(() => {
44
+ window.removeEventListener('resize', adjustSpacer)
45
+ })
46
+
47
+ function adjustSpacer() {
48
+ container.value?.querySelectorAll(`${toClassSelector(spacerClass)}`)
49
+ .forEach(n => n.remove())
50
+
51
+ const track = container.value?.firstElementChild
52
+
53
+ const containerWidth = container.value?.clientWidth ?? 0
54
+ const trackWidth = track?.clientWidth ?? 0
55
+ const trackCount = container.value?.childElementCount ?? 0
56
+
57
+ const perRow = trackWidth !== 0 ? Math.floor(containerWidth / trackWidth) : 0
58
+ const mod = perRow !== 0 ? trackCount % perRow : 0
59
+ const lack = mod !== 0 ? perRow - mod : 0
60
+
61
+ const fragment = createSpacers(lack, spacerTag, spacerClass, type)
62
+
63
+ container.value?.appendChild(fragment!)
64
+ }
65
+
66
+ return {
67
+ container
68
+ }
69
+ }
70
+
71
+ function toClassSelector(name: string) {
72
+ return name.startsWith('.') ? name : '.' + name
73
+ }
74
+
75
+ function toClassName(name: string) {
76
+ return name.startsWith('.') ? name.slice(1) : name
77
+ }
78
+
79
+ function createSpacers(size: number, tag: string, classes: string, type: 'fill' | 'fit') {
80
+ const fragment = document.createDocumentFragment()
81
+
82
+ if (size === 0) {
83
+ return fragment
84
+ }
85
+
86
+ if (type === 'fill') {
87
+ const spacer = createSpacer(tag, classes, { gridColumn: `span ${size}`})
88
+
89
+ fragment.appendChild(spacer)
90
+
91
+ return fragment
92
+ }
93
+
94
+ if (type === 'fit') {
95
+ for (let i = 0; i < size; i++) {
96
+ fragment.appendChild(createSpacer(tag, classes))
97
+ }
98
+ }
99
+
100
+ return fragment
101
+ }
102
+
103
+ function createSpacer(tag: string, classes?: string, styles?: CssStyles) {
104
+ const spacer = document.createElement(tag)
105
+
106
+ if (classes) {
107
+ spacer.className = classes
108
+ }
109
+
110
+ if (styles) {
111
+ for (const s in styles) {
112
+ spacer.style[s] = styles[s]!
113
+ }
114
+ }
115
+
116
+ return spacer
117
+ }
@@ -0,0 +1,138 @@
1
+ import MarkdownIt from 'markdown-it'
2
+ import { onUnmounted, Ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { isCallbackUrl, isExternalUrl, LinkAttrs, linkPlugin } from './markdown/LinkPlugin'
5
+
6
+ export type UseMarkdown = (source: string, inline: boolean) => string
7
+
8
+ export interface UseMarkdownOptions extends MarkdownIt.Options {
9
+ linkAttrs?: LinkAttrs
10
+ config?: (md: MarkdownIt) => void
11
+ }
12
+
13
+ export function useMarkdown(options: UseMarkdownOptions = {}): UseMarkdown {
14
+ const md = new MarkdownIt({
15
+ linkify: true,
16
+ ...options
17
+ })
18
+
19
+ md.use(linkPlugin, {
20
+ target: '_blank',
21
+ rel: 'noopener noreferrer',
22
+ ...options.linkAttrs
23
+ })
24
+
25
+ if (options.config) {
26
+ options.config(md)
27
+ }
28
+
29
+ return (source, inline) => {
30
+ return inline ? md.renderInline(source) : md.render(source)
31
+ }
32
+ }
33
+
34
+ export interface UseLink {
35
+ addListeners(): void
36
+ removeListeners(): void
37
+ subscribe(cb: LinkSubscriber): () => void
38
+ }
39
+
40
+ export interface UseLinkOptions {
41
+ container: Ref<Element | null>
42
+ callbacks?: LinkCallback[]
43
+ }
44
+
45
+ export interface LinkSubscriberPayload {
46
+ event: Event
47
+ target: HTMLAnchorElement
48
+ isExternal: boolean
49
+ isCallback: boolean
50
+ }
51
+
52
+ export type LinkSubscriber = (payload: LinkSubscriberPayload) => void
53
+
54
+ export type LinkCallback = () => void
55
+
56
+ export function useLink({ container, callbacks }: UseLinkOptions): UseLink {
57
+ const router = useRouter()
58
+ const subscribers: LinkSubscriber[] = []
59
+
60
+ onUnmounted(() => removeListeners())
61
+
62
+ function handler(event: Event): void {
63
+ const target = event.target as HTMLAnchorElement
64
+ const href = target.getAttribute('href')!
65
+
66
+ if (!href) {
67
+ return
68
+ }
69
+
70
+ const isExternal = isExternalUrl(href)
71
+ const isCallback = isCallbackUrl(href)
72
+
73
+ subscribers.forEach(sub => sub({
74
+ event,
75
+ target,
76
+ isExternal,
77
+ isCallback
78
+ }))
79
+
80
+ if (isExternal) {
81
+ return
82
+ }
83
+
84
+ if (!event.defaultPrevented) {
85
+ event.preventDefault()
86
+ }
87
+
88
+ if (isCallback) {
89
+ const idx = parseInt(target.dataset.callbackId || '')
90
+ const callback = (callbacks ?? [])[idx]
91
+
92
+ if (!callback) {
93
+ throw new Error(`Callback not found at index: ${idx}`)
94
+ }
95
+
96
+ return callback()
97
+ }
98
+
99
+ router.push(href)
100
+ }
101
+
102
+ function addListeners(): void {
103
+ removeListeners()
104
+
105
+ if (container.value) {
106
+ findLinks(container.value).forEach((element) => {
107
+ element.addEventListener('click', handler)
108
+ })
109
+ }
110
+ }
111
+
112
+ function removeListeners(): void {
113
+ if (container.value) {
114
+ findLinks(container.value).forEach((element) => {
115
+ element.removeEventListener('click', handler)
116
+ })
117
+ }
118
+ }
119
+
120
+ function subscribe(fn: LinkSubscriber): () => void {
121
+ subscribers.push(fn)
122
+
123
+ return () => {
124
+ const idx = subscribers.indexOf(fn)
125
+ idx > -1 && subscribers.splice(idx, 1)
126
+ }
127
+ }
128
+
129
+ return {
130
+ addListeners,
131
+ removeListeners,
132
+ subscribe
133
+ }
134
+ }
135
+
136
+ function findLinks(target: Element) {
137
+ return target.querySelectorAll('a.SMarkdown-link')
138
+ }
@@ -0,0 +1,7 @@
1
+ export interface Step {
2
+ status: StepStatus
3
+ text?: string
4
+ }
5
+
6
+ export type StepStatus = 'upcoming' | 'active' | 'done' | 'failed'
7
+ export type BarMode = 'mute' | 'active'
@@ -0,0 +1,103 @@
1
+ import { MaybeRef } from '@vueuse/core'
2
+ import { reactive } from 'vue'
3
+ import { DropdownSection } from './Dropdown'
4
+
5
+ export interface Table {
6
+ orders: string[]
7
+ columns: TableColumns
8
+ records?: Record<string, any>[]
9
+ total?: number
10
+ page?: number
11
+ perPage?: number
12
+ reset?: boolean
13
+ borderless?: boolean
14
+ loading?: boolean
15
+ onPrev?(): void
16
+ onNext?(): void
17
+ onReset?(): void
18
+ }
19
+
20
+ export interface TableColumns {
21
+ [name: string]: TableColumn
22
+ }
23
+
24
+ export interface TableColumn {
25
+ label: string
26
+ className?: string
27
+ dropdown?: DropdownSection[]
28
+ cell?: TableCell
29
+ }
30
+
31
+ export type TableCell =
32
+ | TableCellText
33
+ | TableCellDay
34
+ | TableCellPill
35
+ | TableCellAvatar
36
+ | TableCellAvatars
37
+
38
+ export type TableCellType = 'text' | 'day' | 'pill' | 'avatar' | 'avatars'
39
+
40
+ export interface TableCellBase {
41
+ type: TableCellType
42
+ }
43
+
44
+ export interface TableCellText extends TableCellBase {
45
+ type: 'text'
46
+ icon?: any
47
+ value?: string | ((value: any) => string)
48
+ link?(value: any, record: any): string
49
+ color?: 'neutral' | 'soft' | 'mute'
50
+ iconColor?: 'neutral' | 'soft' | 'mute'
51
+ }
52
+
53
+ export interface TableCellDay extends TableCellBase {
54
+ type: 'day'
55
+ format?: string
56
+ color?: 'neutral' | 'soft' | 'mute'
57
+ }
58
+
59
+ export interface TableCellPill extends TableCellBase {
60
+ type: 'pill'
61
+ value?: string | ((value: any) => string)
62
+ color?: TableCellPillColor | ((value: any) => TableCellPillColor)
63
+ }
64
+
65
+ export type TableCellPillColor = 'info' | 'success' | 'warning' | 'danger' | 'mute'
66
+
67
+ export interface TableCellAvatar extends TableCellBase {
68
+ type: 'avatar'
69
+ image?(value: any, record: any): string | undefined
70
+ name?(value: any, record: any): string
71
+ link?(value: any, record: any): string
72
+ color?: 'neutral' | 'soft' | 'mute'
73
+ }
74
+
75
+ export interface TableCellAvatars extends TableCellBase {
76
+ type: 'avatars'
77
+ avatars(value: any, record: any): TableCellAvatarsOption[]
78
+ color?: 'neutral' | 'soft' | 'mute'
79
+ }
80
+
81
+ export interface TableCellAvatarsOption {
82
+ image?: string
83
+ name?: string
84
+ }
85
+
86
+ export interface UseTableOptions {
87
+ orders: string[]
88
+ columns: TableColumns
89
+ records?: MaybeRef<Record<string, any>[] | undefined>
90
+ total?: MaybeRef<number | undefined>
91
+ page?: MaybeRef<number | undefined>
92
+ perPage?: MaybeRef<number | undefined>
93
+ reset?: MaybeRef<boolean | undefined>
94
+ borderless?: boolean
95
+ loading?: MaybeRef<boolean | undefined>
96
+ onPrev?(): void
97
+ onNext?(): void
98
+ onReset?(): void
99
+ }
100
+
101
+ export function useTable(options: UseTableOptions): Table {
102
+ return reactive(options)
103
+ }
@@ -0,0 +1,91 @@
1
+ import { Ref, ref } from 'vue'
2
+
3
+ export type Position = 'top' | 'right' | 'bottom' | 'left'
4
+
5
+ const SCREEN_PADDING = 16
6
+
7
+ /**
8
+ * Prevent tooltip going off-screen by adjusting the position depending on
9
+ * the current window size. This only applies to position `top` and
10
+ * `bottom` since we only care about left and right of the screen.
11
+ */
12
+ export function useTooltip(
13
+ content: Ref<HTMLElement | null>,
14
+ tip: Ref<HTMLElement | null>,
15
+ position: Position
16
+ ) {
17
+ const on = ref(false)
18
+
19
+ function show(): void {
20
+ setPosition()
21
+ setTimeout(() => { on.value = true })
22
+ }
23
+
24
+ function hide(): void {
25
+ setTimeout(() => { on.value = false })
26
+ }
27
+
28
+ function setPosition(): void {
29
+ if (shouldPosition()) {
30
+ doSetPosition()
31
+ }
32
+ }
33
+
34
+ function doSetPosition(): void {
35
+ // Reset position first so that we can get the original position.
36
+ resetPosition()
37
+
38
+ // Temporally show tip to get its size.
39
+ tip.value!.style.display = 'block'
40
+
41
+ const contentRect = content.value!.getBoundingClientRect()
42
+ const tipRect = tip.value!.getBoundingClientRect()
43
+
44
+ const contentRightX = contentRect.x + contentRect.width
45
+ const tipRightX = tipRect.x + tipRect.width
46
+
47
+ if (tipRect.x < 0) {
48
+ adjustLeftPosition(contentRect.x)
49
+ } else if (tipRightX > window.outerWidth) {
50
+ adjustRightPosition(contentRightX)
51
+ }
52
+
53
+ tip.value!.style.display = 'none'
54
+ }
55
+
56
+ function adjustLeftPosition(contentRectX: number): void {
57
+ tip.value!.style.left = '0'
58
+ tip.value!.style.right = 'auto'
59
+ setTransform(-contentRectX + SCREEN_PADDING)
60
+ }
61
+
62
+ function adjustRightPosition(contentRightX: number): void {
63
+ tip.value!.style.left = 'auto'
64
+ tip.value!.style.right = '0'
65
+ setTransform((window.outerWidth - contentRightX) - SCREEN_PADDING)
66
+ }
67
+
68
+ function resetPosition(): void {
69
+ tip.value!.style.left = ''
70
+ tip.value!.style.right = ''
71
+ tip.value!.style.transform = ''
72
+ }
73
+
74
+ function setTransform(x: number): void {
75
+ tip.value!.style.transform = `translate(${x}px, ${position === 'top' ? -100 : 100}%)`
76
+ }
77
+
78
+ function shouldPosition(): boolean {
79
+ if (!tip.value || !content.value) {
80
+ return false
81
+ }
82
+
83
+ return position === 'top' || position === 'bottom'
84
+ }
85
+
86
+ return {
87
+ on,
88
+ show,
89
+ hide
90
+ }
91
+ }