quasar-ui-danx 0.4.95 → 0.4.99
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 +24452 -22880
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +133 -122
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Utility/Buttons/ActionButton.vue +11 -3
- package/src/components/Utility/Code/CodeViewer.vue +219 -0
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
- package/src/components/Utility/Code/LanguageBadge.vue +122 -0
- package/src/components/Utility/Code/MarkdownContent.vue +251 -0
- package/src/components/Utility/Code/index.ts +5 -0
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
- package/src/components/Utility/Files/CarouselHeader.vue +24 -0
- package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
- package/src/components/Utility/Files/FilePreview.vue +118 -166
- package/src/components/Utility/Files/index.ts +1 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useCodeFormat.ts +199 -0
- package/src/composables/useCodeViewerCollapse.ts +125 -0
- package/src/composables/useCodeViewerEditor.ts +420 -0
- package/src/composables/useFilePreview.ts +119 -0
- package/src/composables/useTranscodeLoader.ts +68 -0
- package/src/helpers/formats/highlightSyntax.ts +327 -0
- package/src/helpers/formats/index.ts +3 -1
- package/src/helpers/formats/renderMarkdown.ts +338 -0
- package/src/styles/danx.scss +3 -0
- package/src/styles/themes/danx/code.scss +158 -0
- package/src/styles/themes/danx/index.scss +2 -0
- package/src/styles/themes/danx/markdown.scss +145 -0
- package/src/styles/themes/danx/scrollbar.scss +125 -0
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
>
|
|
75
75
|
<QLinearProgress
|
|
76
76
|
:key="'progress-' + isUploading ? 'uploading' : 'transcoding'"
|
|
77
|
-
:value="isUploading ? file
|
|
77
|
+
:value="isUploading ? (file?.progress || 0) : ((transcodingStatus?.progress || 0) / 100)"
|
|
78
78
|
size="36px"
|
|
79
79
|
:color="isUploading ? 'green-800' : 'blue-800'"
|
|
80
80
|
:animation-speed="transcodingStatus?.estimate_ms || 3000"
|
|
@@ -107,6 +107,22 @@
|
|
|
107
107
|
</template>
|
|
108
108
|
|
|
109
109
|
<div class="absolute top-1 right-1 flex items-center flex-nowrap justify-between space-x-1 transition-all opacity-0 group-hover:opacity-100">
|
|
110
|
+
<QBtn
|
|
111
|
+
v-if="hasMetadata"
|
|
112
|
+
:size="btnSize"
|
|
113
|
+
class="dx-file-preview-metadata bg-purple-700 text-white opacity-70 hover:opacity-100 py-1 px-2 relative"
|
|
114
|
+
@click.stop="showMetadataDialog = true"
|
|
115
|
+
>
|
|
116
|
+
<div class="flex items-center flex-nowrap gap-1">
|
|
117
|
+
<MetaIcon class="w-4 h-4" />
|
|
118
|
+
<QBadge
|
|
119
|
+
class="bg-purple-900 text-purple-200"
|
|
120
|
+
:label="metadataKeyCount"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
<QTooltip>View Metadata</QTooltip>
|
|
124
|
+
</QBtn>
|
|
125
|
+
|
|
110
126
|
<QBtn
|
|
111
127
|
v-if="hasTranscodes"
|
|
112
128
|
:size="btnSize"
|
|
@@ -152,6 +168,14 @@
|
|
|
152
168
|
</QBtn>
|
|
153
169
|
</div>
|
|
154
170
|
|
|
171
|
+
<FileMetadataDialog
|
|
172
|
+
v-if="showMetadataDialog"
|
|
173
|
+
:filename="filename"
|
|
174
|
+
:mime-type="mimeType"
|
|
175
|
+
:metadata="filteredMetadata"
|
|
176
|
+
@close="showMetadataDialog = false"
|
|
177
|
+
/>
|
|
178
|
+
|
|
155
179
|
<FullScreenCarouselDialog
|
|
156
180
|
v-if="showPreview && !disabled && previewableFiles"
|
|
157
181
|
:files="previewableFiles"
|
|
@@ -163,204 +187,132 @@
|
|
|
163
187
|
|
|
164
188
|
<script setup lang="ts">
|
|
165
189
|
import { DocumentTextIcon as TextFileIcon, DownloadIcon, FilmIcon, PlayIcon } from "@heroicons/vue/outline";
|
|
166
|
-
import {
|
|
167
|
-
import {
|
|
190
|
+
import { FaSolidBarcode as MetaIcon } from "danx-icon";
|
|
191
|
+
import { computed, ref, toRef } from "vue";
|
|
192
|
+
import { useFilePreview } from "../../../composables/useFilePreview";
|
|
193
|
+
import { useTranscodeLoader } from "../../../composables/useTranscodeLoader";
|
|
168
194
|
import { download, uniqueBy } from "../../../helpers";
|
|
169
|
-
import
|
|
170
|
-
import { getMimeType, getOptimizedUrl, isExternalLinkFile } from "../../../helpers/filePreviewHelpers";
|
|
195
|
+
import { isExternalLinkFile } from "../../../helpers/filePreviewHelpers";
|
|
171
196
|
import { GoogleDocsIcon, ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
|
|
172
197
|
import { UploadedFile } from "../../../types";
|
|
173
198
|
import { FullScreenCarouselDialog } from "../Dialogs";
|
|
174
|
-
|
|
175
|
-
export interface FileTranscode {
|
|
176
|
-
status: "Complete" | "Pending" | "In Progress";
|
|
177
|
-
progress: number;
|
|
178
|
-
estimate_ms: number;
|
|
179
|
-
started_at: string;
|
|
180
|
-
completed_at: string;
|
|
181
|
-
message?: string;
|
|
182
|
-
}
|
|
199
|
+
import FileMetadataDialog from "./FileMetadataDialog.vue";
|
|
183
200
|
|
|
184
201
|
export interface FilePreviewProps {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
src?: string;
|
|
203
|
+
file?: UploadedFile;
|
|
204
|
+
relatedFiles?: UploadedFile[];
|
|
205
|
+
missingIcon?: any;
|
|
206
|
+
showFilename?: boolean;
|
|
207
|
+
downloadButtonClass?: string;
|
|
208
|
+
imageFit?: "cover" | "contain" | "fill" | "none" | "scale-down";
|
|
209
|
+
downloadable?: boolean;
|
|
210
|
+
removable?: boolean;
|
|
211
|
+
disabled?: boolean;
|
|
212
|
+
square?: boolean;
|
|
213
|
+
btnSize?: "xs" | "sm" | "md" | "lg";
|
|
197
214
|
}
|
|
198
215
|
|
|
199
216
|
const emit = defineEmits(["remove"]);
|
|
200
217
|
|
|
201
218
|
const props = withDefaults(defineProps<FilePreviewProps>(), {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
src: "",
|
|
220
|
+
file: null,
|
|
221
|
+
relatedFiles: null,
|
|
222
|
+
missingIcon: ImageIcon,
|
|
223
|
+
downloadButtonClass: "bg-blue-600 text-white",
|
|
224
|
+
imageFit: "cover",
|
|
225
|
+
downloadable: false,
|
|
226
|
+
removable: false,
|
|
227
|
+
disabled: false,
|
|
228
|
+
square: false,
|
|
229
|
+
btnSize: "sm"
|
|
213
230
|
});
|
|
214
231
|
|
|
232
|
+
// Use composables for file preview logic
|
|
233
|
+
const fileRef = toRef(props, "file");
|
|
234
|
+
const srcRef = toRef(props, "src");
|
|
235
|
+
|
|
236
|
+
const {
|
|
237
|
+
computedImage,
|
|
238
|
+
filename,
|
|
239
|
+
mimeType,
|
|
240
|
+
isVideo,
|
|
241
|
+
isPdf,
|
|
242
|
+
isExternalLink,
|
|
243
|
+
previewUrl,
|
|
244
|
+
thumbUrl,
|
|
245
|
+
isPreviewable,
|
|
246
|
+
hasMetadata,
|
|
247
|
+
metadataKeyCount,
|
|
248
|
+
filteredMetadata,
|
|
249
|
+
hasTranscodes,
|
|
250
|
+
transcodingStatus
|
|
251
|
+
} = useFilePreview({ file: fileRef, src: srcRef });
|
|
215
252
|
|
|
253
|
+
// Load transcodes automatically
|
|
254
|
+
useTranscodeLoader({ file: fileRef });
|
|
255
|
+
|
|
256
|
+
// Local state
|
|
216
257
|
const showPreview = ref(false);
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
if (props.file) {
|
|
220
|
-
return props.file;
|
|
221
|
-
} else if (props.src) {
|
|
222
|
-
return {
|
|
223
|
-
id: props.src,
|
|
224
|
-
url: props.src,
|
|
225
|
-
type: "image/" + props.src.split(".").pop()?.toLowerCase(),
|
|
226
|
-
name: "",
|
|
227
|
-
size: 0,
|
|
228
|
-
__type: "BrowserFile"
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
});
|
|
258
|
+
const showMetadataDialog = ref(false);
|
|
259
|
+
const isConfirmingRemove = ref(false);
|
|
233
260
|
|
|
234
|
-
|
|
261
|
+
// Computed
|
|
262
|
+
const isUploading = computed(() => props.file && props.file?.progress !== undefined);
|
|
235
263
|
const statusMessage = computed(() => isUploading.value ? "Uploading..." : transcodingStatus.value?.message);
|
|
236
|
-
|
|
237
|
-
const previewableFiles
|
|
238
|
-
|
|
264
|
+
|
|
265
|
+
const previewableFiles = computed(() => {
|
|
266
|
+
return props.relatedFiles?.length > 0
|
|
267
|
+
? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl)
|
|
268
|
+
: [computedImage.value];
|
|
239
269
|
});
|
|
240
270
|
|
|
271
|
+
// Helpers
|
|
241
272
|
function filesHaveSameUrl(a: UploadedFile, b: UploadedFile) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
273
|
+
return a.id === b.id ||
|
|
274
|
+
[b.url, b.optimized?.url, b.thumb?.url].includes(a.url) ||
|
|
275
|
+
[a.url, a.optimized?.url, a.thumb?.url].includes(b.url);
|
|
245
276
|
}
|
|
246
277
|
|
|
247
|
-
const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
|
|
248
|
-
const mimeType = computed(() => computedImage.value ? getMimeType(computedImage.value) : "");
|
|
249
|
-
const isImage = computed(() => computedImage.value ? fileHelpers.isImage(computedImage.value) : false);
|
|
250
|
-
const isVideo = computed(() => computedImage.value ? fileHelpers.isVideo(computedImage.value) : false);
|
|
251
|
-
const isPdf = computed(() => computedImage.value ? fileHelpers.isPdf(computedImage.value) : false);
|
|
252
|
-
const isExternalLink = computed(() => computedImage.value ? isExternalLinkFile(computedImage.value) : false);
|
|
253
|
-
const previewUrl = computed(() => computedImage.value ? getOptimizedUrl(computedImage.value) : "");
|
|
254
|
-
const thumbUrl = computed(() => computedImage.value?.thumb?.url || "");
|
|
255
|
-
const isPreviewable = computed(() => {
|
|
256
|
-
return !!thumbUrl.value || isVideo.value || isImage.value;
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Resolve the active transcoding operation if there is one, otherwise return null
|
|
261
|
-
*/
|
|
262
|
-
const transcodingStatus = computed(() => {
|
|
263
|
-
let status = null;
|
|
264
|
-
const metaTranscodes: FileTranscode[] = props.file?.meta?.transcodes || [];
|
|
265
|
-
|
|
266
|
-
for (let transcodeName of Object.keys(metaTranscodes)) {
|
|
267
|
-
const transcode = metaTranscodes[transcodeName];
|
|
268
|
-
if (!["Complete", "Timeout"].includes(transcode?.status)) {
|
|
269
|
-
return { ...transcode, message: `${transcodeName} ${transcode.status}` };
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return status;
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const isConfirmingRemove = ref(false);
|
|
277
278
|
function onRemove() {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
279
|
+
if (!isConfirmingRemove.value) {
|
|
280
|
+
isConfirmingRemove.value = true;
|
|
281
|
+
setTimeout(() => {
|
|
282
|
+
isConfirmingRemove.value = false;
|
|
283
|
+
}, 2000);
|
|
284
|
+
} else {
|
|
285
|
+
emit("remove");
|
|
286
|
+
}
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
function onShowPreview() {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
showPreview.value = true;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Check if transcodes need to be loaded for the current file
|
|
299
|
-
*/
|
|
300
|
-
function shouldLoadTranscodes(): boolean {
|
|
301
|
-
if (!props.file?.id) return false;
|
|
302
|
-
if (isLoadingTranscodes.value) return false;
|
|
303
|
-
if (!danxOptions.value.fileUpload?.refreshFile) return false;
|
|
304
|
-
|
|
305
|
-
// Only load if transcodes is explicitly null, undefined, or an empty array
|
|
306
|
-
const transcodes = props.file.transcodes;
|
|
307
|
-
return transcodes === null || transcodes === undefined || (Array.isArray(transcodes) && transcodes.length === 0);
|
|
290
|
+
if (computedImage.value && isExternalLinkFile(computedImage.value)) {
|
|
291
|
+
window.open(computedImage.value.url, "_blank");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
showPreview.value = true;
|
|
308
295
|
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Load transcodes for the current file
|
|
312
|
-
*/
|
|
313
|
-
async function loadTranscodes() {
|
|
314
|
-
if (!shouldLoadTranscodes()) return;
|
|
315
|
-
|
|
316
|
-
isLoadingTranscodes.value = true;
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
const refreshFile = danxOptions.value.fileUpload.refreshFile;
|
|
320
|
-
if (refreshFile && props.file?.id) {
|
|
321
|
-
const refreshedFile = await refreshFile(props.file.id);
|
|
322
|
-
|
|
323
|
-
// Update the file object with the loaded transcodes
|
|
324
|
-
if (refreshedFile.transcodes && props.file) {
|
|
325
|
-
props.file.transcodes = refreshedFile.transcodes;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
} catch (error) {
|
|
329
|
-
console.error("Failed to load transcodes:", error);
|
|
330
|
-
} finally {
|
|
331
|
-
isLoadingTranscodes.value = false;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Load transcodes when component mounts
|
|
336
|
-
onMounted(() => {
|
|
337
|
-
loadTranscodes();
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Watch for file changes and reload transcodes if needed
|
|
341
|
-
watch(() => props.file?.id, () => {
|
|
342
|
-
loadTranscodes();
|
|
343
|
-
});
|
|
344
296
|
</script>
|
|
345
297
|
|
|
346
298
|
<style module="cls" lang="scss">
|
|
347
299
|
.action-button {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
300
|
+
position: absolute;
|
|
301
|
+
bottom: 1.5em;
|
|
302
|
+
right: 1em;
|
|
303
|
+
z-index: 1;
|
|
352
304
|
}
|
|
353
305
|
|
|
354
306
|
.play-button {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
307
|
+
position: absolute;
|
|
308
|
+
top: 0;
|
|
309
|
+
left: 0;
|
|
310
|
+
display: flex;
|
|
311
|
+
justify-content: center;
|
|
312
|
+
align-items: center;
|
|
313
|
+
width: 100%;
|
|
314
|
+
height: 100%;
|
|
315
|
+
pointer-events: none;
|
|
316
|
+
@apply text-blue-200;
|
|
365
317
|
}
|
|
366
318
|
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as CarouselHeader } from "./CarouselHeader.vue";
|
|
2
|
+
export { default as FileMetadataDialog } from "./FileMetadataDialog.vue";
|
|
2
3
|
export { default as FilePreview } from "./FilePreview.vue";
|
|
3
4
|
export { default as FileRenderer } from "./FileRenderer.vue";
|
|
4
5
|
export { default as SvgImg } from "./SvgImg.vue";
|
package/src/composables/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
export * from "./useCodeFormat";
|
|
2
|
+
export * from "./useCodeViewerCollapse";
|
|
3
|
+
export * from "./useCodeViewerEditor";
|
|
1
4
|
export * from "./useFileNavigation";
|
|
5
|
+
export * from "./useFilePreview";
|
|
2
6
|
export * from "./useKeyboardNavigation";
|
|
3
7
|
export * from "./useThumbnailScroll";
|
|
8
|
+
export * from "./useTranscodeLoader";
|
|
4
9
|
export * from "./useVirtualCarousel";
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { computed, ref, Ref } from "vue";
|
|
2
|
+
import { parse as parseYAML, stringify as yamlStringify } from "yaml";
|
|
3
|
+
import { fJSON, parseMarkdownJSON, parseMarkdownYAML } from "../helpers/formats/parsers";
|
|
4
|
+
|
|
5
|
+
export type CodeFormat = "json" | "yaml" | "text" | "markdown";
|
|
6
|
+
|
|
7
|
+
export interface UseCodeFormatOptions {
|
|
8
|
+
initialFormat?: CodeFormat;
|
|
9
|
+
initialValue?: object | string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ValidationError {
|
|
13
|
+
message: string;
|
|
14
|
+
line?: number;
|
|
15
|
+
column?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseCodeFormatReturn {
|
|
19
|
+
// State
|
|
20
|
+
format: Ref<CodeFormat>;
|
|
21
|
+
rawContent: Ref<string>;
|
|
22
|
+
|
|
23
|
+
// Computed
|
|
24
|
+
parsedValue: Ref<object | null>;
|
|
25
|
+
formattedContent: Ref<string>;
|
|
26
|
+
isValid: Ref<boolean>;
|
|
27
|
+
|
|
28
|
+
// Methods
|
|
29
|
+
setFormat: (format: CodeFormat) => void;
|
|
30
|
+
setContent: (content: string) => void;
|
|
31
|
+
setValue: (value: object | string | null) => void;
|
|
32
|
+
parse: (content: string) => object | null;
|
|
33
|
+
formatValue: (value: object | null, targetFormat?: CodeFormat) => string;
|
|
34
|
+
validate: (content: string, targetFormat?: CodeFormat) => boolean;
|
|
35
|
+
validateWithError: (content: string, targetFormat?: CodeFormat) => ValidationError | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useCodeFormat(options: UseCodeFormatOptions = {}): UseCodeFormatReturn {
|
|
39
|
+
const format = ref<CodeFormat>(options.initialFormat ?? "yaml");
|
|
40
|
+
const rawContent = ref("");
|
|
41
|
+
|
|
42
|
+
// Parse any string (JSON or YAML) to object
|
|
43
|
+
function parseContent(content: string): object | null {
|
|
44
|
+
if (!content) return null;
|
|
45
|
+
|
|
46
|
+
// Try JSON first
|
|
47
|
+
const jsonResult = parseMarkdownJSON(content);
|
|
48
|
+
if (jsonResult !== false && jsonResult !== null) return jsonResult;
|
|
49
|
+
|
|
50
|
+
// Try YAML
|
|
51
|
+
const yamlResult = parseMarkdownYAML(content);
|
|
52
|
+
if (yamlResult !== false && yamlResult !== null) return yamlResult;
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Format object to string in specified format
|
|
58
|
+
function formatValueToString(value: object | string | null, targetFormat: CodeFormat = format.value): string {
|
|
59
|
+
if (!value) return "";
|
|
60
|
+
|
|
61
|
+
// Text and markdown formats - just return as-is
|
|
62
|
+
if (targetFormat === "text" || targetFormat === "markdown") {
|
|
63
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const obj = typeof value === "string" ? parseContent(value) : value;
|
|
68
|
+
if (!obj) return typeof value === "string" ? value : "";
|
|
69
|
+
|
|
70
|
+
if (targetFormat === "json") {
|
|
71
|
+
const formatted = fJSON(obj);
|
|
72
|
+
return typeof formatted === "string" ? formatted : JSON.stringify(obj, null, 2);
|
|
73
|
+
} else {
|
|
74
|
+
return yamlStringify(obj as object);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate string content for a format
|
|
82
|
+
function validateContent(content: string, targetFormat: CodeFormat = format.value): boolean {
|
|
83
|
+
if (!content) return true;
|
|
84
|
+
|
|
85
|
+
// Text and markdown formats are always valid
|
|
86
|
+
if (targetFormat === "text" || targetFormat === "markdown") return true;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (targetFormat === "json") {
|
|
90
|
+
JSON.parse(content);
|
|
91
|
+
} else {
|
|
92
|
+
parseYAML(content);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate and return error details if invalid
|
|
101
|
+
function validateContentWithError(content: string, targetFormat: CodeFormat = format.value): ValidationError | null {
|
|
102
|
+
if (!content) return null;
|
|
103
|
+
|
|
104
|
+
// Text and markdown formats are always valid
|
|
105
|
+
if (targetFormat === "text" || targetFormat === "markdown") return null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
if (targetFormat === "json") {
|
|
109
|
+
JSON.parse(content);
|
|
110
|
+
} else {
|
|
111
|
+
parseYAML(content);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
} catch (e: unknown) {
|
|
115
|
+
const error = e as Error & { linePos?: { line: number; col: number }[] };
|
|
116
|
+
let line: number | undefined;
|
|
117
|
+
let column: number | undefined;
|
|
118
|
+
|
|
119
|
+
// YAML errors from 'yaml' library have linePos
|
|
120
|
+
if (error.linePos && error.linePos[0]) {
|
|
121
|
+
line = error.linePos[0].line;
|
|
122
|
+
column = error.linePos[0].col;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// JSON parse errors - try to extract position from message
|
|
126
|
+
if (targetFormat === "json" && error.message) {
|
|
127
|
+
const posMatch = error.message.match(/position\s+(\d+)/i);
|
|
128
|
+
if (posMatch) {
|
|
129
|
+
const pos = parseInt(posMatch[1], 10);
|
|
130
|
+
// Convert position to line number
|
|
131
|
+
const lines = content.substring(0, pos).split("\n");
|
|
132
|
+
line = lines.length;
|
|
133
|
+
column = lines[lines.length - 1].length + 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
message: error.message || "Invalid syntax",
|
|
139
|
+
line,
|
|
140
|
+
column
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Initialize with value if provided
|
|
146
|
+
if (options.initialValue) {
|
|
147
|
+
rawContent.value = formatValueToString(options.initialValue, format.value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Computed: parsed object from raw content
|
|
151
|
+
const parsedValue = computed(() => parseContent(rawContent.value));
|
|
152
|
+
|
|
153
|
+
// Computed: formatted string
|
|
154
|
+
// For text and markdown formats, return rawContent directly without parsing
|
|
155
|
+
const formattedContent = computed(() => {
|
|
156
|
+
if (format.value === "text" || format.value === "markdown") {
|
|
157
|
+
return rawContent.value;
|
|
158
|
+
}
|
|
159
|
+
return formatValueToString(parsedValue.value, format.value);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Computed: is current content valid
|
|
163
|
+
const isValid = computed(() => validateContent(rawContent.value, format.value));
|
|
164
|
+
|
|
165
|
+
// Methods
|
|
166
|
+
function setFormat(newFormat: CodeFormat) {
|
|
167
|
+
if (format.value === newFormat) return;
|
|
168
|
+
|
|
169
|
+
// Convert content to new format
|
|
170
|
+
const obj = parsedValue.value;
|
|
171
|
+
format.value = newFormat;
|
|
172
|
+
if (obj) {
|
|
173
|
+
rawContent.value = formatValueToString(obj, newFormat);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setContent(content: string) {
|
|
178
|
+
rawContent.value = content;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function setValue(value: object | string | null) {
|
|
182
|
+
rawContent.value = formatValueToString(value, format.value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
format,
|
|
187
|
+
rawContent,
|
|
188
|
+
parsedValue,
|
|
189
|
+
formattedContent,
|
|
190
|
+
isValid,
|
|
191
|
+
setFormat,
|
|
192
|
+
setContent,
|
|
193
|
+
setValue,
|
|
194
|
+
parse: parseContent,
|
|
195
|
+
formatValue: formatValueToString,
|
|
196
|
+
validate: validateContent,
|
|
197
|
+
validateWithError: validateContentWithError
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { computed, Ref } from "vue";
|
|
2
|
+
import { CodeFormat, UseCodeFormatReturn } from "./useCodeFormat";
|
|
3
|
+
import { highlightSyntax } from "../helpers/formats/highlightSyntax";
|
|
4
|
+
|
|
5
|
+
export interface UseCodeViewerCollapseOptions {
|
|
6
|
+
modelValue: Ref<object | string | null | undefined>;
|
|
7
|
+
format: Ref<CodeFormat>;
|
|
8
|
+
displayContent: Ref<string>;
|
|
9
|
+
codeFormat: UseCodeFormatReturn;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseCodeViewerCollapseReturn {
|
|
13
|
+
collapsedPreview: Ref<string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format a value for collapsed preview display
|
|
18
|
+
*/
|
|
19
|
+
function formatValuePreview(val: unknown, includeQuotes = true): string {
|
|
20
|
+
if (val === null) {
|
|
21
|
+
return "null";
|
|
22
|
+
}
|
|
23
|
+
if (typeof val === "string") {
|
|
24
|
+
const truncated = val.length > 15 ? val.slice(0, 15) + "..." : val;
|
|
25
|
+
return includeQuotes ? `"${truncated}"` : truncated;
|
|
26
|
+
}
|
|
27
|
+
if (typeof val === "object") {
|
|
28
|
+
return Array.isArray(val) ? `[${val.length}]` : "{...}";
|
|
29
|
+
}
|
|
30
|
+
return String(val);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get syntax highlighting class for a value type
|
|
35
|
+
*/
|
|
36
|
+
function getSyntaxClass(val: unknown): string {
|
|
37
|
+
if (val === null) return "null";
|
|
38
|
+
if (typeof val === "string") return "string";
|
|
39
|
+
if (typeof val === "number") return "number";
|
|
40
|
+
if (typeof val === "boolean") return "boolean";
|
|
41
|
+
return "punctuation";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Composable for collapsed preview logic in CodeViewer
|
|
46
|
+
*/
|
|
47
|
+
export function useCodeViewerCollapse(options: UseCodeViewerCollapseOptions): UseCodeViewerCollapseReturn {
|
|
48
|
+
const { modelValue, format, displayContent, codeFormat } = options;
|
|
49
|
+
|
|
50
|
+
const collapsedPreview = computed(() => {
|
|
51
|
+
const content = displayContent.value;
|
|
52
|
+
if (!content) return "<span class=\"syntax-null\">null</span>";
|
|
53
|
+
|
|
54
|
+
const maxLength = 100;
|
|
55
|
+
let preview = "";
|
|
56
|
+
|
|
57
|
+
if (format.value === "json") {
|
|
58
|
+
// For JSON, show compact inline format
|
|
59
|
+
try {
|
|
60
|
+
const parsed = typeof modelValue.value === "string"
|
|
61
|
+
? JSON.parse(modelValue.value)
|
|
62
|
+
: modelValue.value;
|
|
63
|
+
|
|
64
|
+
// Handle null at top level
|
|
65
|
+
if (parsed === null) {
|
|
66
|
+
return "<span class=\"syntax-null\">null</span>";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(parsed)) {
|
|
70
|
+
preview = `[${parsed.length} items]`;
|
|
71
|
+
} else if (typeof parsed === "object") {
|
|
72
|
+
const keys = Object.keys(parsed);
|
|
73
|
+
const keyPreviews = keys.slice(0, 3).map(k => {
|
|
74
|
+
const val = parsed[k];
|
|
75
|
+
const valStr = formatValuePreview(val);
|
|
76
|
+
return `<span class="syntax-key">${k}</span>: <span class="syntax-${getSyntaxClass(val)}">${valStr}</span>`;
|
|
77
|
+
});
|
|
78
|
+
preview = `{${keyPreviews.join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
|
|
79
|
+
} else {
|
|
80
|
+
preview = highlightSyntax(String(parsed), { format: "json" });
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Fall back to truncated content
|
|
84
|
+
preview = content.replace(/\s+/g, " ").slice(0, maxLength);
|
|
85
|
+
if (content.length > maxLength) preview += "...";
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
// For YAML, show key: value pairs inline
|
|
89
|
+
try {
|
|
90
|
+
const parsed = typeof modelValue.value === "string"
|
|
91
|
+
? codeFormat.parse(modelValue.value)
|
|
92
|
+
: modelValue.value;
|
|
93
|
+
|
|
94
|
+
// Handle null at top level
|
|
95
|
+
if (parsed === null) {
|
|
96
|
+
return "<span class=\"syntax-null\">null</span>";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(parsed)) {
|
|
100
|
+
preview = `[${parsed.length} items]`;
|
|
101
|
+
} else if (typeof parsed === "object") {
|
|
102
|
+
const keys = Object.keys(parsed);
|
|
103
|
+
const keyPreviews = keys.slice(0, 3).map(k => {
|
|
104
|
+
const val = (parsed as Record<string, unknown>)[k];
|
|
105
|
+
const valStr = formatValuePreview(val, false);
|
|
106
|
+
return `<span class="syntax-key">${k}</span>: <span class="syntax-${getSyntaxClass(val)}">${valStr}</span>`;
|
|
107
|
+
});
|
|
108
|
+
preview = keyPreviews.join(", ") + (keys.length > 3 ? ", ..." : "");
|
|
109
|
+
} else {
|
|
110
|
+
preview = String(parsed);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Fall back to truncated first line
|
|
114
|
+
const firstLine = content.split("\n")[0];
|
|
115
|
+
preview = firstLine.length > maxLength ? firstLine.slice(0, maxLength) + "..." : firstLine;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return preview;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
collapsedPreview
|
|
124
|
+
};
|
|
125
|
+
}
|