bt-core-app 0.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.
- package/.vscode/extensions.json +3 -0
- package/README.md +45 -0
- package/index.html +13 -0
- package/package.json +39 -0
- package/public/vite.svg +1 -0
- package/src/assets/vue.svg +1 -0
- package/src/components/BT-Btn.vue +40 -0
- package/src/components/BT-Col.vue +36 -0
- package/src/components/BT-Span.vue +21 -0
- package/src/components/Dialog-Confirm.vue +47 -0
- package/src/components/Dialog-Select-Date.vue +89 -0
- package/src/components/Dialog-Select.vue +140 -0
- package/src/components/Dialog-Text.vue +59 -0
- package/src/composables/actions-tracker.ts +99 -0
- package/src/composables/actions.ts +354 -0
- package/src/composables/api.ts +471 -0
- package/src/composables/auth.ts +382 -0
- package/src/composables/cosmetics.ts +178 -0
- package/src/composables/csv.ts +198 -0
- package/src/composables/dates.ts +79 -0
- package/src/composables/demo.ts +25 -0
- package/src/composables/dialogs.ts +115 -0
- package/src/composables/document-meta.ts +40 -0
- package/src/composables/draggable.ts +189 -0
- package/src/composables/filters.ts +256 -0
- package/src/composables/forage.ts +49 -0
- package/src/composables/helpers.ts +694 -0
- package/src/composables/id.ts +20 -0
- package/src/composables/list.ts +601 -0
- package/src/composables/navigation.ts +214 -0
- package/src/composables/presets.ts +20 -0
- package/src/composables/pwa.ts +89 -0
- package/src/composables/resizable.ts +382 -0
- package/src/composables/rules.ts +40 -0
- package/src/composables/stores.ts +797 -0
- package/src/composables/track.ts +55 -0
- package/src/composables/urls.ts +11 -0
- package/src/core.ts +92 -0
- package/src/index.ts +16 -0
- package/src/types.ts +13 -0
- package/src/useApi.ts +68 -0
- package/src/vite-env.d.ts +1 -0
- package/test/api.test.ts +84 -0
- package/test/forage.test.ts +31 -0
- package/test/helpers.test.ts +231 -0
- package/test/navigation.test.ts +99 -0
- package/test/stores-last-update.test.ts +138 -0
- package/test/stores-session.test.ts +118 -0
- package/test/track.test.ts +29 -0
- package/test/utils.ts +15 -0
- package/tsconfig.json +33 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { TableColumn } from "./list"
|
|
2
|
+
import { isLengthyArray, fromCamelCase, nestedValue } from "../index"
|
|
3
|
+
|
|
4
|
+
export interface CSVProps {
|
|
5
|
+
canExportCSV?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const csvDefaults = {
|
|
9
|
+
canExportCSV: false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseCSVPropsReturn {
|
|
13
|
+
exportToCSV: Function
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CSVItem {
|
|
17
|
+
header: string
|
|
18
|
+
itemText?: string
|
|
19
|
+
value: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare global {
|
|
23
|
+
interface Navigator {
|
|
24
|
+
msSaveOrOpenBlob: (blob: Blob, fileName: string) => boolean
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useCSV(): UseCSVPropsReturn {
|
|
29
|
+
|
|
30
|
+
function exportToCSV(
|
|
31
|
+
items: any[],
|
|
32
|
+
headers?: TableColumn[],
|
|
33
|
+
fileName: string = 'csvData.csv') {
|
|
34
|
+
|
|
35
|
+
if (!isLengthyArray(items)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let dnaArray: CSVItem[] = []
|
|
40
|
+
|
|
41
|
+
if (headers != null) {
|
|
42
|
+
dnaArray = headers?.filter(y => (y.csv ?? y.csvText ?? y.csvFilter ?? y.csvArray) != null)
|
|
43
|
+
.map(z => {
|
|
44
|
+
return {
|
|
45
|
+
header: z.title ?? '',
|
|
46
|
+
itemText: z.itemText,
|
|
47
|
+
value: z.value
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
dnaArray = Object.keys(items[0]).map(x => { return { header: fromCamelCase(x) ?? '', value: x }; });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
dnaArray = dnaArray.filter(z => z.header.length > 0)
|
|
56
|
+
|
|
57
|
+
var lineArray: any[] = [];
|
|
58
|
+
|
|
59
|
+
// var increments = [];
|
|
60
|
+
// if (dnaArray.some(y => y.breakdown === true)) {
|
|
61
|
+
// try {
|
|
62
|
+
// increments = await BlitzIt.store.getAll('stock-increments');
|
|
63
|
+
// }
|
|
64
|
+
// catch (err) {
|
|
65
|
+
// console.log('generating csv file could not pull increments for breakdown');
|
|
66
|
+
// console.log(this.extractErrorDescription(err));
|
|
67
|
+
// }
|
|
68
|
+
// }
|
|
69
|
+
|
|
70
|
+
// if (docTitle != null) {
|
|
71
|
+
// lineArray.push(docTitle);
|
|
72
|
+
// }
|
|
73
|
+
|
|
74
|
+
// dnaArray = dnaArray.filter(x => x.header != null);
|
|
75
|
+
|
|
76
|
+
//print header row
|
|
77
|
+
// lineArray.push(dnaArray.map(x => x.header))
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < items.length; i++) {
|
|
80
|
+
const d = items[i];
|
|
81
|
+
|
|
82
|
+
var newItem: any = {};
|
|
83
|
+
var extraLines: any[] = [];
|
|
84
|
+
|
|
85
|
+
for (let ii = 0; ii < dnaArray.length; ii++) {
|
|
86
|
+
const dna = dnaArray[ii];
|
|
87
|
+
var v = null;
|
|
88
|
+
if (typeof(dna.value) == 'function') {
|
|
89
|
+
v = dna.value(d);
|
|
90
|
+
}
|
|
91
|
+
else if (typeof(dna.value) == 'string') {
|
|
92
|
+
v = nestedValue(d, dna.value);
|
|
93
|
+
|
|
94
|
+
// if (dna.navigation != null && v != null) {
|
|
95
|
+
// //search from local storage
|
|
96
|
+
// try {
|
|
97
|
+
// var res = await BlitzIt.store.get(dna.navigation, v, null, false, null, null, true);
|
|
98
|
+
// if (res != null) {
|
|
99
|
+
// v = res;
|
|
100
|
+
// }
|
|
101
|
+
// }
|
|
102
|
+
// catch (err) {
|
|
103
|
+
// console.log(err);
|
|
104
|
+
// }
|
|
105
|
+
// }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// if (v != null && dna.valueFilter != null) {
|
|
109
|
+
// console.log('aa');
|
|
110
|
+
// v = this.$options.filters[dna.valueFilter](v);
|
|
111
|
+
// console.log(v);
|
|
112
|
+
// }
|
|
113
|
+
|
|
114
|
+
// if (dna.csvArray) {
|
|
115
|
+
// if (this.isLengthyArray(v)) {
|
|
116
|
+
// v.forEach(w => {
|
|
117
|
+
// var otherNewItem = {};
|
|
118
|
+
// // otherNewItem[dna.header] = w.toString();
|
|
119
|
+
// // extraLines.push(otherNewItem);
|
|
120
|
+
// if (dna.breakdown && w.productID != null) {
|
|
121
|
+
// otherNewItem[dna.header] = `${getBreakdown(w.quantity, measurements, increments, w.productID)}, ${w.product?.productName}`;
|
|
122
|
+
// extraLines.push(otherNewItem);
|
|
123
|
+
// }
|
|
124
|
+
// else {
|
|
125
|
+
// otherNewItem[dna.header] = w.toString();
|
|
126
|
+
// extraLines.push(otherNewItem);
|
|
127
|
+
// }
|
|
128
|
+
// })
|
|
129
|
+
// }
|
|
130
|
+
// }
|
|
131
|
+
// else {
|
|
132
|
+
// if (dna.breakdown) {
|
|
133
|
+
// var prodProp = dna.csvProductIDProp || 'productID';
|
|
134
|
+
// newItem[dna.header] = getBreakdown(v, measurements, increments, d[prodProp]);
|
|
135
|
+
// }
|
|
136
|
+
if (dna.itemText != null) {
|
|
137
|
+
newItem[dna.header] = nestedValue(v, dna.itemText);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
newItem[dna.header] = v;
|
|
141
|
+
}
|
|
142
|
+
// }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
lineArray.push(newItem);
|
|
146
|
+
|
|
147
|
+
if (isLengthyArray(extraLines)) {
|
|
148
|
+
extraLines.forEach(e => {
|
|
149
|
+
lineArray.push(e);
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
extraLines = [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
var resArray = [];
|
|
157
|
+
|
|
158
|
+
// if (docTitle != null) {
|
|
159
|
+
// resArray.push(docTitle);
|
|
160
|
+
// }
|
|
161
|
+
|
|
162
|
+
//print header row
|
|
163
|
+
resArray.push(dnaArray.map(x => x.header))
|
|
164
|
+
|
|
165
|
+
lineArray.forEach(obj => {
|
|
166
|
+
let propArray: any[] = [];
|
|
167
|
+
dnaArray.forEach(function(k) {
|
|
168
|
+
var v = obj[k.header];
|
|
169
|
+
propArray.push(v != null ? v : '');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
resArray.push(propArray.join(","));
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
var csvContent = resArray.join("\n");
|
|
176
|
+
var file = new Blob([csvContent], { type: "text/plain;charset=utf-8" });
|
|
177
|
+
|
|
178
|
+
if (window.navigator.msSaveOrOpenBlob) {
|
|
179
|
+
window.navigator.msSaveOrOpenBlob(file, fileName);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
var a = document.createElement("a"),
|
|
183
|
+
url = URL.createObjectURL(file);
|
|
184
|
+
a.href = url;
|
|
185
|
+
a.download = fileName;
|
|
186
|
+
document.body.appendChild(a);
|
|
187
|
+
a.click();
|
|
188
|
+
setTimeout(function () {
|
|
189
|
+
document.body.removeChild(a);
|
|
190
|
+
window.URL.revokeObjectURL(url);
|
|
191
|
+
}, 0);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
exportToCSV
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DateTime } from 'luxon'
|
|
2
|
+
// import { useTimeZone } from '@/composables/auth'
|
|
3
|
+
|
|
4
|
+
export interface BTDates {
|
|
5
|
+
getToday: () => string
|
|
6
|
+
getTomorrow: () => string
|
|
7
|
+
tzDate: (val?: string, fromFormat?: string) => DateTime
|
|
8
|
+
tzString: (val?: string, format?: string, fromFormat?: string) => string
|
|
9
|
+
utcDate: (val?: string, fromFormat?: string) => DateTime
|
|
10
|
+
utcString: (val?: string, format?: string, fromFormat?: string) => string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CreateDatesOptions {
|
|
14
|
+
getTimeZone: () => string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createDates(options: CreateDatesOptions): BTDates {
|
|
18
|
+
|
|
19
|
+
function getToday(): string {
|
|
20
|
+
return tzDate()?.startOf('day').toUTC().toString() ?? '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getTomorrow(): string {
|
|
24
|
+
return tzDate()?.endOf('day').toUTC().toString() ?? '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tzDate(val?: string, fromFormat?: string): DateTime {
|
|
28
|
+
if (val == null) {
|
|
29
|
+
//create now
|
|
30
|
+
return DateTime.utc().setZone(options.getTimeZone())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return fromFormat ? DateTime.fromFormat(val, fromFormat, { zone: options.getTimeZone() }) : DateTime.fromISO(val, { zone: options.getTimeZone() })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tzString(val?: string, format?: string, fromFormat?: string): string {
|
|
37
|
+
if (val == null) {
|
|
38
|
+
//create now
|
|
39
|
+
const d = DateTime.utc().setZone(options.getTimeZone())
|
|
40
|
+
return format ? d.toFormat(format) : d.toString()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (val == 'Invalid DateTime') {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const d = fromFormat ? DateTime.fromFormat(val, fromFormat, { zone: options.getTimeZone() }) : DateTime.fromISO(val, { zone: options.getTimeZone() })
|
|
48
|
+
|
|
49
|
+
return format ? d.toFormat(format) : d?.toString()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function utcDate(val?: string, fromFormat?: string): DateTime {
|
|
53
|
+
if (val == null) {
|
|
54
|
+
return DateTime.utc()
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
return fromFormat ? DateTime.fromFormat(val, fromFormat) : DateTime.fromISO(val)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function utcString(val?: string, format?: string, fromFormat?: string): string {
|
|
62
|
+
if (val == null) {
|
|
63
|
+
return format ? DateTime.utc().toFormat(format) : DateTime.utc().toString()
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const d = fromFormat ? DateTime.fromFormat(val, fromFormat) : DateTime.fromISO(val)
|
|
67
|
+
return format ? d.toFormat(format) : d.toString()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
getToday,
|
|
73
|
+
getTomorrow,
|
|
74
|
+
tzDate,
|
|
75
|
+
tzString,
|
|
76
|
+
utcDate,
|
|
77
|
+
utcString
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface BTDemo {
|
|
4
|
+
endDemo: () => void
|
|
5
|
+
isDemoing: Ref<boolean>
|
|
6
|
+
startDemo: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createDemo(): BTDemo {
|
|
10
|
+
const isDemoing = ref(false)
|
|
11
|
+
|
|
12
|
+
function startDemo() {
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function endDemo() {
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
endDemo,
|
|
22
|
+
isDemoing,
|
|
23
|
+
startDemo
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// import { openDialog } from 'vue3-promise-dialog'
|
|
2
|
+
import { createConfirmDialog } from 'vuejs-confirm-dialog'
|
|
3
|
+
import BTConfirmDialog from '@/components/Dialog-Confirm.vue'
|
|
4
|
+
import BTSelectDateDialog from '@/components/Dialog-Select-Date.vue'
|
|
5
|
+
import BTSelectDialog from '@/components/Dialog-Select.vue'
|
|
6
|
+
import BTTextDialog from '@/components/Dialog-Text.vue'
|
|
7
|
+
import { type ListProps } from '@/composables/list'
|
|
8
|
+
|
|
9
|
+
export interface ConfirmDialogProps {
|
|
10
|
+
cancelText?: string
|
|
11
|
+
cancelValue?: any
|
|
12
|
+
confirmText?: string
|
|
13
|
+
confirmValue?: any
|
|
14
|
+
msg?: string
|
|
15
|
+
maxWidth?: number
|
|
16
|
+
minWidth?: number
|
|
17
|
+
title?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SelectDateProps {
|
|
21
|
+
cancelText?: string
|
|
22
|
+
cancelValue?: any
|
|
23
|
+
confirmText?: string
|
|
24
|
+
dateFrom?: string
|
|
25
|
+
dateRules?: Function | unknown[]
|
|
26
|
+
format?: string
|
|
27
|
+
fromNow?: boolean
|
|
28
|
+
height?: string
|
|
29
|
+
msg?: string
|
|
30
|
+
maxWidth?: number
|
|
31
|
+
minWidth?: number
|
|
32
|
+
range?: boolean
|
|
33
|
+
required?: boolean
|
|
34
|
+
requireTime?: boolean
|
|
35
|
+
title?: string
|
|
36
|
+
useTime?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SelectDialogProps extends ListProps {
|
|
40
|
+
cancelText?: string
|
|
41
|
+
cancelValue?: any
|
|
42
|
+
canUnselect?: boolean
|
|
43
|
+
confirmText?: string
|
|
44
|
+
height?: string
|
|
45
|
+
itemSubtext?: string
|
|
46
|
+
itemText?: string
|
|
47
|
+
itemValue?: string
|
|
48
|
+
msg?: string
|
|
49
|
+
maxWidth?: number
|
|
50
|
+
minWidth?: number
|
|
51
|
+
multiple?: boolean
|
|
52
|
+
nav?: string
|
|
53
|
+
onFilter?: Function
|
|
54
|
+
required?: boolean
|
|
55
|
+
subtextFilter?: string
|
|
56
|
+
subtextFunction?: Function
|
|
57
|
+
textFilter?: string
|
|
58
|
+
textFunction?: Function
|
|
59
|
+
title?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TextDialogProps extends ListProps {
|
|
63
|
+
cancelText?: string
|
|
64
|
+
confirmText?: string
|
|
65
|
+
height?: string
|
|
66
|
+
label?: string
|
|
67
|
+
msg?: string
|
|
68
|
+
maxWidth?: number
|
|
69
|
+
minWidth?: number
|
|
70
|
+
required?: boolean
|
|
71
|
+
title?: string
|
|
72
|
+
value?: any
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const confirmDialog = createConfirmDialog(BTConfirmDialog as any)
|
|
76
|
+
const selectDateDialog = createConfirmDialog(BTSelectDateDialog as any)
|
|
77
|
+
const selectDialog = createConfirmDialog(BTSelectDialog as any)
|
|
78
|
+
const textDialog = createConfirmDialog(BTTextDialog as any)
|
|
79
|
+
|
|
80
|
+
export function useRequireConfirmation(action: any, props: ConfirmDialogProps, requireConfirm: boolean) {
|
|
81
|
+
if (requireConfirm) {
|
|
82
|
+
const { reveal, onConfirm } = createConfirmDialog(BTConfirmDialog as any, props)
|
|
83
|
+
onConfirm(action)
|
|
84
|
+
reveal()
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
action()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function useConfirmAsync(text: string) {
|
|
92
|
+
const { isCanceled } = await confirmDialog.reveal({ msg: text })
|
|
93
|
+
return !isCanceled
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns undefined if cancelled
|
|
98
|
+
* [] if multiple
|
|
99
|
+
* Null | Obj if single
|
|
100
|
+
* @param opts
|
|
101
|
+
*/
|
|
102
|
+
export async function useSelectDialog(opts?: SelectDialogProps) {
|
|
103
|
+
const { data } = await selectDialog.reveal(opts)
|
|
104
|
+
return data
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function useSelectDate(opts?: SelectDateProps) {
|
|
108
|
+
const { data } = await selectDateDialog.reveal(opts)
|
|
109
|
+
return data
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function useTextDialog(opts?: TextDialogProps) {
|
|
113
|
+
const { data } = await textDialog.reveal(opts)
|
|
114
|
+
return data
|
|
115
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type RouteLocationNormalized } from 'vue-router'
|
|
2
|
+
import { BTDemo } from '@/composables/demo'
|
|
3
|
+
import { type Environment } from '@/composables/urls'
|
|
4
|
+
|
|
5
|
+
export interface UseDocumentMetaOptions {
|
|
6
|
+
demo?: BTDemo
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**routes with meta object */
|
|
10
|
+
export function useDocumentMeta(options?: UseDocumentMetaOptions) {
|
|
11
|
+
|
|
12
|
+
function updateMeta(to: RouteLocationNormalized) {
|
|
13
|
+
const nearestWithTitle = to.matched.slice().reverse().find(r => r.meta && r.meta.title);
|
|
14
|
+
|
|
15
|
+
if(nearestWithTitle) {
|
|
16
|
+
document.title = nearestWithTitle.meta.title as string
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const env = import.meta.env.MODE as Environment
|
|
20
|
+
let title = ''
|
|
21
|
+
|
|
22
|
+
if (env == 'development')
|
|
23
|
+
title = 'BWeb Dev'
|
|
24
|
+
else if (env == 'staging')
|
|
25
|
+
title = 'BlitzIt Sandpit'
|
|
26
|
+
else {
|
|
27
|
+
title = 'BlitzIt Web | Cloud-Based Wholesale Logistics Platform'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (options?.demo?.isDemoing.value == true)
|
|
31
|
+
title = `Training: ${title}`
|
|
32
|
+
|
|
33
|
+
document.title = title
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
updateMeta
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { type ComponentPublicInstance, type MaybeRefOrGetter, type Ref, ref, toValue } from 'vue'
|
|
2
|
+
import { type Position, useEventListener } from '@vueuse/core'
|
|
3
|
+
import { type PointerOrTouchEvent } from './resizable'
|
|
4
|
+
|
|
5
|
+
export interface UseDraggableOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Only start the dragging when click on the element directly
|
|
8
|
+
*
|
|
9
|
+
* @default false
|
|
10
|
+
*/
|
|
11
|
+
// exact?: MaybeRefOrGetter<boolean>
|
|
12
|
+
|
|
13
|
+
preventDefault?: MaybeRefOrGetter<boolean>
|
|
14
|
+
stopPropagation?: MaybeRefOrGetter<boolean>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether dispatch events in capturing phase
|
|
18
|
+
*
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
capture?: boolean
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Element to attach `pointermove` and `pointerup` events to.
|
|
25
|
+
*
|
|
26
|
+
* @default window
|
|
27
|
+
*/
|
|
28
|
+
draggingElement?: MaybeRefOrGetter<HTMLElement | SVGElement | Window | Document | null | undefined>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Element for calculating bounds (If not set, it will use the event's target).
|
|
32
|
+
*
|
|
33
|
+
* @default undefined
|
|
34
|
+
*/
|
|
35
|
+
// containerElement?: MaybeRefOrGetter<HTMLElement | SVGElement | null | undefined>
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handle that triggers the drag event
|
|
39
|
+
*
|
|
40
|
+
* @default target
|
|
41
|
+
*/
|
|
42
|
+
handle?: MaybeRefOrGetter<HTMLElement | SVGElement | null | undefined>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Initial position of the element.
|
|
46
|
+
*
|
|
47
|
+
* @default { x: 0, y: 0 }
|
|
48
|
+
*/
|
|
49
|
+
initialValue?: MaybeRefOrGetter<Position>
|
|
50
|
+
|
|
51
|
+
onStart?: (position: Position, event: PointerEvent) => void | false
|
|
52
|
+
onMove?: (position: Position, event: PointerEvent) => void
|
|
53
|
+
onEnd?: (position: Position, event: PointerEvent) => void
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Axis to drag on.
|
|
57
|
+
*
|
|
58
|
+
* @default 'both'
|
|
59
|
+
*/
|
|
60
|
+
axis?: 'x' | 'y' | 'both'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useDraggable(
|
|
64
|
+
target: MaybeRefOrGetter<ComponentPublicInstance | null>, //HTMLElement | SVGElement | null | undefined>,
|
|
65
|
+
handle: MaybeRefOrGetter<ComponentPublicInstance | null>,
|
|
66
|
+
options: UseDraggableOptions = {},
|
|
67
|
+
) {
|
|
68
|
+
const {
|
|
69
|
+
preventDefault = false,
|
|
70
|
+
stopPropagation = false,
|
|
71
|
+
axis = 'both',
|
|
72
|
+
} = options
|
|
73
|
+
|
|
74
|
+
const config = { capture: options.capture ?? true }
|
|
75
|
+
let currentElementPosition = { x: 0, y: 0 }
|
|
76
|
+
let startingElementPosition = { x: 0, y: 0 }
|
|
77
|
+
let startingPointerPosition = { x: 0, y: 0 }
|
|
78
|
+
|
|
79
|
+
let listeners: Function[] = []
|
|
80
|
+
let moveListeners: Function[] = []
|
|
81
|
+
let draggingIsOn: Ref<boolean> = ref(false)
|
|
82
|
+
|
|
83
|
+
function handleEvent(e: PointerEvent) {
|
|
84
|
+
if (toValue(preventDefault))
|
|
85
|
+
e.preventDefault()
|
|
86
|
+
if (toValue(stopPropagation))
|
|
87
|
+
e.stopPropagation()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function start(e: PointerOrTouchEvent) {
|
|
91
|
+
const t = toValue(target)
|
|
92
|
+
if (!t) return
|
|
93
|
+
const el = t.$el
|
|
94
|
+
const isTouch = e.type === 'touchstart' && e.touches.length > 0
|
|
95
|
+
const evtData = isTouch ? e.touches[0] : e
|
|
96
|
+
|
|
97
|
+
startingPointerPosition = { x: evtData.clientX, y: evtData.clientY }
|
|
98
|
+
|
|
99
|
+
startingElementPosition = {
|
|
100
|
+
x: el.offsetLeft,
|
|
101
|
+
y: el.offsetTop
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
currentElementPosition = {
|
|
105
|
+
x: el.offsetLeft,
|
|
106
|
+
y: el.offsetTop
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.onStart?.(startingPointerPosition, e) === false) return
|
|
110
|
+
|
|
111
|
+
moveListeners.push(useEventListener('mousemove', move))
|
|
112
|
+
moveListeners.push(useEventListener('touchmove', move))
|
|
113
|
+
moveListeners.push(useEventListener('mouseup', end))
|
|
114
|
+
moveListeners.push(useEventListener('touchend', end))
|
|
115
|
+
|
|
116
|
+
handleEvent(e)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function move(e: any) {
|
|
120
|
+
if (!startingPointerPosition) return
|
|
121
|
+
|
|
122
|
+
const t = toValue(target)
|
|
123
|
+
if (!t) return
|
|
124
|
+
// const el = t.$el
|
|
125
|
+
const isTouch = e.type === 'touchmove' && e.touches.length > 0
|
|
126
|
+
const evtData = isTouch ? e.touches[0] : e
|
|
127
|
+
let dx = evtData.clientX - startingPointerPosition.x
|
|
128
|
+
let dy = evtData.clientY - startingPointerPosition.y
|
|
129
|
+
|
|
130
|
+
if (axis === 'x' || axis === 'both')
|
|
131
|
+
currentElementPosition.x = startingElementPosition.x + dx
|
|
132
|
+
if (axis === 'y' || axis === 'both')
|
|
133
|
+
currentElementPosition.y = startingElementPosition.y + dy
|
|
134
|
+
|
|
135
|
+
options.onMove?.(currentElementPosition, e)
|
|
136
|
+
|
|
137
|
+
handleEvent(e)
|
|
138
|
+
|
|
139
|
+
applyToElementStyle(currentElementPosition)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function end(e: any) {
|
|
143
|
+
document.documentElement.style.cursor = ''
|
|
144
|
+
moveListeners.forEach(m => { m() })
|
|
145
|
+
moveListeners.length = 0
|
|
146
|
+
options.onEnd?.(currentElementPosition, e)
|
|
147
|
+
handleEvent(e)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function applyToElementStyle(s: Position) {
|
|
151
|
+
const t = toValue(target)
|
|
152
|
+
if (!t) return
|
|
153
|
+
const el = t.$el
|
|
154
|
+
|
|
155
|
+
el.style.left = `${s.x}px`
|
|
156
|
+
el.style.top = `${s.y}px`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function turnDraggableOn() {
|
|
160
|
+
if (toValue(draggingIsOn)) return
|
|
161
|
+
const handleT = toValue(handle)
|
|
162
|
+
if (!handleT) return
|
|
163
|
+
const handleEl = handleT.$el
|
|
164
|
+
handleEl.style.cursor = 'move'
|
|
165
|
+
|
|
166
|
+
listeners.push(useEventListener(handleEl, 'mousedown', start, config))
|
|
167
|
+
listeners.push(useEventListener(handleEl, 'touchstart', start, config))
|
|
168
|
+
draggingIsOn.value = true
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function turnDraggableOff() {
|
|
172
|
+
if (!toValue(draggingIsOn)) return
|
|
173
|
+
const handleT = toValue(handle)
|
|
174
|
+
if (!handleT) return
|
|
175
|
+
const handleEl = handleT.$el
|
|
176
|
+
handleEl.style.cursor = ''
|
|
177
|
+
|
|
178
|
+
//provide new position and size?
|
|
179
|
+
listeners.forEach(l => { l() })
|
|
180
|
+
listeners.length = 0
|
|
181
|
+
draggingIsOn.value = false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
draggingIsOn,
|
|
186
|
+
turnDraggableOff,
|
|
187
|
+
turnDraggableOn
|
|
188
|
+
}
|
|
189
|
+
}
|