quasar-ui-danx 0.4.93 → 0.4.95
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/dist/danx.es.js +5316 -5269
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +77 -77
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActionTable/Form/Fields/NumberField.vue +36 -8
- package/src/components/Utility/Files/FilePreview.vue +13 -3
- package/src/helpers/filePreviewHelpers.ts +31 -0
- package/src/helpers/formats/parsers.ts +5 -2
- package/src/helpers/objectStore.ts +10 -2
- package/src/svg/GoogleDocsIcon.vue +88 -0
- package/src/svg/index.ts +1 -0
package/package.json
CHANGED
|
@@ -50,7 +50,37 @@ function format(number) {
|
|
|
50
50
|
return fNumber(number, options);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const onUpdateDebounced = useDebounceFn((val: number | string | undefined) =>
|
|
53
|
+
const onUpdateDebounced = useDebounceFn((val: number | string | undefined) => {
|
|
54
|
+
// Apply min/max clamping to the final value
|
|
55
|
+
let clampedVal = val;
|
|
56
|
+
if (typeof val === "number") {
|
|
57
|
+
if (props.min !== undefined) {
|
|
58
|
+
clampedVal = Math.max(val, props.min);
|
|
59
|
+
}
|
|
60
|
+
if (props.max !== undefined) {
|
|
61
|
+
clampedVal = Math.min(clampedVal as number, props.max);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
emit("update", clampedVal);
|
|
65
|
+
}, props.delay);
|
|
66
|
+
|
|
67
|
+
const applyMinMaxClamping = useDebounceFn((value: number | undefined) => {
|
|
68
|
+
if (value === undefined) return;
|
|
69
|
+
|
|
70
|
+
let clampedValue = value;
|
|
71
|
+
if (props.min !== undefined) {
|
|
72
|
+
clampedValue = Math.max(clampedValue, props.min);
|
|
73
|
+
}
|
|
74
|
+
if (props.max !== undefined) {
|
|
75
|
+
clampedValue = Math.min(clampedValue, props.max);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Only update the display if the value changed after clamping
|
|
79
|
+
if (clampedValue !== value) {
|
|
80
|
+
numberVal.value = format(clampedValue);
|
|
81
|
+
emit("update:model-value", clampedValue);
|
|
82
|
+
}
|
|
83
|
+
}, 500);
|
|
54
84
|
|
|
55
85
|
function onInput(value) {
|
|
56
86
|
let number: number | undefined = undefined;
|
|
@@ -68,19 +98,17 @@ function onInput(value) {
|
|
|
68
98
|
value = value.replace(/[^\d.]/g, "");
|
|
69
99
|
number = +value;
|
|
70
100
|
|
|
71
|
-
|
|
72
|
-
number = Math.max(number, props.min);
|
|
73
|
-
}
|
|
74
|
-
if (props.max) {
|
|
75
|
-
number = Math.min(number, props.max);
|
|
76
|
-
}
|
|
77
|
-
|
|
101
|
+
// Don't clamp immediately - let user type freely
|
|
78
102
|
numberVal.value = format(number);
|
|
79
103
|
}
|
|
80
104
|
|
|
105
|
+
// Emit the raw typed value immediately
|
|
81
106
|
emit("update:model-value", number);
|
|
82
107
|
|
|
83
108
|
// Delay the change event, so we only see the value after the user has finished
|
|
84
109
|
onUpdateDebounced(number);
|
|
110
|
+
|
|
111
|
+
// Apply min/max clamping after user stops typing
|
|
112
|
+
applyMinMaxClamping(number);
|
|
85
113
|
}
|
|
86
114
|
</script>
|
|
@@ -37,8 +37,12 @@
|
|
|
37
37
|
v-else
|
|
38
38
|
class="flex items-center justify-center h-full"
|
|
39
39
|
>
|
|
40
|
+
<GoogleDocsIcon
|
|
41
|
+
v-if="isExternalLink"
|
|
42
|
+
class="h-3/4"
|
|
43
|
+
/>
|
|
40
44
|
<PdfIcon
|
|
41
|
-
v-if="isPdf"
|
|
45
|
+
v-else-if="isPdf"
|
|
42
46
|
class="w-3/4"
|
|
43
47
|
/>
|
|
44
48
|
<TextFileIcon
|
|
@@ -163,8 +167,8 @@ import { computed, ComputedRef, onMounted, ref, watch } from "vue";
|
|
|
163
167
|
import { danxOptions } from "../../../config";
|
|
164
168
|
import { download, uniqueBy } from "../../../helpers";
|
|
165
169
|
import * as fileHelpers from "../../../helpers/filePreviewHelpers";
|
|
166
|
-
import { getMimeType, getOptimizedUrl } from "../../../helpers/filePreviewHelpers";
|
|
167
|
-
import { ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
|
|
170
|
+
import { getMimeType, getOptimizedUrl, isExternalLinkFile } from "../../../helpers/filePreviewHelpers";
|
|
171
|
+
import { GoogleDocsIcon, ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
|
|
168
172
|
import { UploadedFile } from "../../../types";
|
|
169
173
|
import { FullScreenCarouselDialog } from "../Dialogs";
|
|
170
174
|
|
|
@@ -245,6 +249,7 @@ const mimeType = computed(() => computedImage.value ? getMimeType(computedImage.
|
|
|
245
249
|
const isImage = computed(() => computedImage.value ? fileHelpers.isImage(computedImage.value) : false);
|
|
246
250
|
const isVideo = computed(() => computedImage.value ? fileHelpers.isVideo(computedImage.value) : false);
|
|
247
251
|
const isPdf = computed(() => computedImage.value ? fileHelpers.isPdf(computedImage.value) : false);
|
|
252
|
+
const isExternalLink = computed(() => computedImage.value ? isExternalLinkFile(computedImage.value) : false);
|
|
248
253
|
const previewUrl = computed(() => computedImage.value ? getOptimizedUrl(computedImage.value) : "");
|
|
249
254
|
const thumbUrl = computed(() => computedImage.value?.thumb?.url || "");
|
|
250
255
|
const isPreviewable = computed(() => {
|
|
@@ -281,6 +286,11 @@ function onRemove() {
|
|
|
281
286
|
}
|
|
282
287
|
|
|
283
288
|
function onShowPreview() {
|
|
289
|
+
// For external links (Google Docs, etc.), open directly in new tab
|
|
290
|
+
if (computedImage.value && isExternalLinkFile(computedImage.value)) {
|
|
291
|
+
window.open(computedImage.value.url, "_blank");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
284
294
|
showPreview.value = true;
|
|
285
295
|
}
|
|
286
296
|
|
|
@@ -105,3 +105,34 @@ export function getThumbUrl(file: UploadedFile): string {
|
|
|
105
105
|
export function getOptimizedUrl(file: UploadedFile): string {
|
|
106
106
|
return file.optimized?.url || file.blobUrl || file.url || "";
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if file is an external link that should open in a new tab
|
|
111
|
+
* (e.g., Google Docs, external URLs that can't be previewed inline)
|
|
112
|
+
*/
|
|
113
|
+
export function isExternalLinkFile(file: UploadedFile): boolean {
|
|
114
|
+
const url = file.url || "";
|
|
115
|
+
const mime = getMimeType(file);
|
|
116
|
+
|
|
117
|
+
// Google Docs/Sheets/Slides URLs
|
|
118
|
+
if (url.includes("docs.google.com") || url.includes("drive.google.com")) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Google Doc MIME types
|
|
123
|
+
if (mime.startsWith("application/vnd.google-apps.")) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if file can be previewed inline (image, video, text, pdf)
|
|
132
|
+
*/
|
|
133
|
+
export function canPreviewInline(file: UploadedFile): boolean {
|
|
134
|
+
if (isExternalLinkFile(file)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return isImage(file) || isVideo(file) || isText(file) || isPdf(file) || !!file.thumb?.url;
|
|
138
|
+
}
|
|
@@ -63,9 +63,12 @@ export function fMarkdownCode(type: string, string: string | object): string {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const regex = new RegExp(`\`\`\`${type}`, "g");
|
|
67
66
|
string = (string || "") as string;
|
|
68
|
-
if
|
|
67
|
+
// Check if string STARTS with the code fence (not just contains it anywhere)
|
|
68
|
+
// This fixes a bug where JSON containing embedded code fences in the data
|
|
69
|
+
// would incorrectly be detected as already wrapped
|
|
70
|
+
const startsWithCodeFence = new RegExp(`^\\s*\`\`\`${type}\\s`).test(string);
|
|
71
|
+
if (!startsWithCodeFence) {
|
|
69
72
|
string = parseMarkdownCode(string as string);
|
|
70
73
|
return `\`\`\`${type}\n${string}\n\`\`\``;
|
|
71
74
|
}
|
|
@@ -25,7 +25,12 @@ export function storeObject<T extends TypedObject>(newObject: T, recentlyStoredO
|
|
|
25
25
|
|
|
26
26
|
const id = newObject?.id || newObject?.name;
|
|
27
27
|
const type = newObject?.__type;
|
|
28
|
-
if (!id || !type)
|
|
28
|
+
if (!id || !type) {
|
|
29
|
+
// Still process children to store any nested TypedObjects
|
|
30
|
+
const reactiveObject = shallowReactive(newObject);
|
|
31
|
+
storeObjectChildren(newObject, recentlyStoredObjects, reactiveObject);
|
|
32
|
+
return reactiveObject;
|
|
33
|
+
}
|
|
29
34
|
|
|
30
35
|
if (!newObject.__id) {
|
|
31
36
|
newObject.__id = uid();
|
|
@@ -88,7 +93,7 @@ function storeObjectChildren<T extends TypedObject>(object: T, recentlyStoredObj
|
|
|
88
93
|
applyToObject = applyToObject || object;
|
|
89
94
|
for (const key of Object.keys(object)) {
|
|
90
95
|
const value = object[key];
|
|
91
|
-
if (Array.isArray(value)
|
|
96
|
+
if (Array.isArray(value)) {
|
|
92
97
|
for (const index in value) {
|
|
93
98
|
if (value[index] && typeof value[index] === "object") {
|
|
94
99
|
if (!applyToObject[key]) {
|
|
@@ -101,6 +106,9 @@ function storeObjectChildren<T extends TypedObject>(object: T, recentlyStoredObj
|
|
|
101
106
|
} else if (value?.__type) {
|
|
102
107
|
// @ts-expect-error __type is guaranteed to be set in this case
|
|
103
108
|
applyToObject[key] = storeObject(value as TypedObject, recentlyStoredObjects);
|
|
109
|
+
} else if (value && typeof value === "object") {
|
|
110
|
+
// Handle plain objects/dictionaries - recurse to find nested TypedObjects at any depth
|
|
111
|
+
storeObjectChildren(value, recentlyStoredObjects, applyToObject[key]);
|
|
104
112
|
}
|
|
105
113
|
}
|
|
106
114
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
viewBox="0 0 47 65"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
preserveAspectRatio="xMidYMid meet"
|
|
6
|
+
>
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient
|
|
9
|
+
id="gdoc-gradient"
|
|
10
|
+
x1="50%"
|
|
11
|
+
y1="8.59%"
|
|
12
|
+
x2="50%"
|
|
13
|
+
y2="100%"
|
|
14
|
+
>
|
|
15
|
+
<stop
|
|
16
|
+
stop-color="#1A237E"
|
|
17
|
+
stop-opacity="0.2"
|
|
18
|
+
offset="0%"
|
|
19
|
+
/>
|
|
20
|
+
<stop
|
|
21
|
+
stop-color="#1A237E"
|
|
22
|
+
stop-opacity="0.02"
|
|
23
|
+
offset="100%"
|
|
24
|
+
/>
|
|
25
|
+
</linearGradient>
|
|
26
|
+
<radialGradient
|
|
27
|
+
id="gdoc-radial"
|
|
28
|
+
cx="3.17%"
|
|
29
|
+
cy="2.72%"
|
|
30
|
+
r="161.25%"
|
|
31
|
+
>
|
|
32
|
+
<stop
|
|
33
|
+
stop-color="#FFFFFF"
|
|
34
|
+
stop-opacity="0.1"
|
|
35
|
+
offset="0%"
|
|
36
|
+
/>
|
|
37
|
+
<stop
|
|
38
|
+
stop-color="#FFFFFF"
|
|
39
|
+
stop-opacity="0"
|
|
40
|
+
offset="100%"
|
|
41
|
+
/>
|
|
42
|
+
</radialGradient>
|
|
43
|
+
</defs>
|
|
44
|
+
<!-- Main document shape -->
|
|
45
|
+
<path
|
|
46
|
+
d="M29.375,0 L4.406,0 C1.983,0 0,1.994 0,4.432 L0,60.568 C0,63.006 1.983,65 4.406,65 L42.594,65 C45.017,65 47,63.006 47,60.568 L47,17.727 L36.719,10.341 L29.375,0 Z"
|
|
47
|
+
fill="#4285F4"
|
|
48
|
+
/>
|
|
49
|
+
<!-- Corner fold shadow -->
|
|
50
|
+
<polygon
|
|
51
|
+
fill="url(#gdoc-gradient)"
|
|
52
|
+
points="30.664 16.431 47 32.858 47 17.727"
|
|
53
|
+
/>
|
|
54
|
+
<!-- Text lines -->
|
|
55
|
+
<path
|
|
56
|
+
d="M11.75,47.273 L35.25,47.273 L35.25,44.318 L11.75,44.318 L11.75,47.273 Z M11.75,53.182 L29.375,53.182 L29.375,50.227 L11.75,50.227 L11.75,53.182 Z M11.75,32.5 L11.75,35.455 L35.25,35.455 L35.25,32.5 L11.75,32.5 Z M11.75,41.364 L35.25,41.364 L35.25,38.409 L11.75,38.409 L11.75,41.364 Z"
|
|
57
|
+
fill="#F1F1F1"
|
|
58
|
+
/>
|
|
59
|
+
<!-- Corner fold -->
|
|
60
|
+
<path
|
|
61
|
+
d="M29.375,0 L29.375,13.295 C29.375,15.744 31.347,17.727 33.781,17.727 L47,17.727 L29.375,0 Z"
|
|
62
|
+
fill="#A1C2FA"
|
|
63
|
+
/>
|
|
64
|
+
<!-- Top highlight -->
|
|
65
|
+
<path
|
|
66
|
+
d="M4.406,0 C1.983,0 0,1.994 0,4.432 L0,4.801 C0,2.364 1.983,0.369 4.406,0.369 L29.375,0.369 L29.375,0 L4.406,0 Z"
|
|
67
|
+
fill="#FFFFFF"
|
|
68
|
+
fill-opacity="0.2"
|
|
69
|
+
/>
|
|
70
|
+
<!-- Bottom shadow -->
|
|
71
|
+
<path
|
|
72
|
+
d="M42.594,64.631 L4.406,64.631 C1.983,64.631 0,62.636 0,60.199 L0,60.568 C0,63.006 1.983,65 4.406,65 L42.594,65 C45.017,65 47,63.006 47,60.568 L47,60.199 C47,62.636 45.017,64.631 42.594,64.631 Z"
|
|
73
|
+
fill="#1A237E"
|
|
74
|
+
fill-opacity="0.2"
|
|
75
|
+
/>
|
|
76
|
+
<!-- Fold edge shadow -->
|
|
77
|
+
<path
|
|
78
|
+
d="M33.781,17.727 C31.347,17.727 29.375,15.744 29.375,13.295 L29.375,13.665 C29.375,16.113 31.347,18.097 33.781,18.097 L47,18.097 L47,17.727 L33.781,17.727 Z"
|
|
79
|
+
fill="#1A237E"
|
|
80
|
+
fill-opacity="0.1"
|
|
81
|
+
/>
|
|
82
|
+
<!-- Overlay gradient -->
|
|
83
|
+
<path
|
|
84
|
+
d="M29.375,0 L4.406,0 C1.983,0 0,1.994 0,4.432 L0,60.568 C0,63.006 1.983,65 4.406,65 L42.594,65 C45.017,65 47,63.006 47,60.568 L47,17.727 L29.375,0 Z"
|
|
85
|
+
fill="url(#gdoc-radial)"
|
|
86
|
+
/>
|
|
87
|
+
</svg>
|
|
88
|
+
</template>
|
package/src/svg/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { default as CaretDownIcon } from "./CaretDownIcon.svg";
|
|
|
2
2
|
export { default as DragHandleDotsIcon } from "./DragHandleDotsIcon.svg";
|
|
3
3
|
export { default as DragHandleIcon } from "./DragHandleIcon.svg";
|
|
4
4
|
export { default as FilterIcon } from "./FilterIcon.svg";
|
|
5
|
+
export { default as GoogleDocsIcon } from "./GoogleDocsIcon.vue";
|
|
5
6
|
export { default as ImageIcon } from "./ImageIcon.svg";
|
|
6
7
|
export { default as PdfIcon } from "./PdfIcon.svg";
|
|
7
8
|
export { default as PercentIcon } from "./PercentIcon.svg";
|