quasar-ui-danx 0.4.94 → 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 +24432 -22819
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +130 -119
- 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 +124 -162
- 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/filePreviewHelpers.ts +31 -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/helpers/objectStore.ts +10 -2
- 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
- package/src/svg/GoogleDocsIcon.vue +88 -0
- package/src/svg/index.ts +1 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { computed, nextTick, onUnmounted, Ref, ref } from "vue";
|
|
2
|
+
import { CodeFormat, UseCodeFormatReturn, ValidationError } from "./useCodeFormat";
|
|
3
|
+
import { highlightSyntax } from "../helpers/formats/highlightSyntax";
|
|
4
|
+
|
|
5
|
+
export interface UseCodeViewerEditorOptions {
|
|
6
|
+
codeRef: Ref<HTMLPreElement | null>;
|
|
7
|
+
codeFormat: UseCodeFormatReturn;
|
|
8
|
+
currentFormat: Ref<CodeFormat>;
|
|
9
|
+
canEdit: Ref<boolean>;
|
|
10
|
+
editable: Ref<boolean>;
|
|
11
|
+
onEmitModelValue: (value: object | string | null) => void;
|
|
12
|
+
onEmitEditable: (editable: boolean) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseCodeViewerEditorReturn {
|
|
16
|
+
// State
|
|
17
|
+
internalEditable: Ref<boolean>;
|
|
18
|
+
editingContent: Ref<string>;
|
|
19
|
+
cachedHighlightedContent: Ref<string>;
|
|
20
|
+
isUserEditing: Ref<boolean>;
|
|
21
|
+
validationError: Ref<ValidationError | null>;
|
|
22
|
+
|
|
23
|
+
// Computed
|
|
24
|
+
isEditing: Ref<boolean>;
|
|
25
|
+
hasValidationError: Ref<boolean>;
|
|
26
|
+
highlightedContent: Ref<string>;
|
|
27
|
+
displayContent: Ref<string>;
|
|
28
|
+
charCount: Ref<number>;
|
|
29
|
+
isValid: Ref<boolean>;
|
|
30
|
+
|
|
31
|
+
// Methods
|
|
32
|
+
toggleEdit: () => void;
|
|
33
|
+
onContentEditableInput: (event: Event) => void;
|
|
34
|
+
onContentEditableBlur: () => void;
|
|
35
|
+
onKeyDown: (event: KeyboardEvent) => void;
|
|
36
|
+
syncEditableFromProp: (value: boolean) => void;
|
|
37
|
+
syncEditingContentFromValue: () => void;
|
|
38
|
+
updateEditingContentOnFormatChange: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get cursor offset in plain text within a contenteditable element
|
|
43
|
+
*/
|
|
44
|
+
function getCursorOffset(codeRef: HTMLPreElement | null): number {
|
|
45
|
+
const selection = window.getSelection();
|
|
46
|
+
if (!selection || !selection.rangeCount || !codeRef) return 0;
|
|
47
|
+
|
|
48
|
+
const range = selection.getRangeAt(0);
|
|
49
|
+
const preCaretRange = document.createRange();
|
|
50
|
+
preCaretRange.selectNodeContents(codeRef);
|
|
51
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
52
|
+
|
|
53
|
+
// Count characters by walking text nodes
|
|
54
|
+
let offset = 0;
|
|
55
|
+
const walker = document.createTreeWalker(preCaretRange.commonAncestorContainer, NodeFilter.SHOW_TEXT);
|
|
56
|
+
let node = walker.nextNode();
|
|
57
|
+
while (node) {
|
|
58
|
+
if (preCaretRange.intersectsNode(node)) {
|
|
59
|
+
const nodeRange = document.createRange();
|
|
60
|
+
nodeRange.selectNodeContents(node);
|
|
61
|
+
if (preCaretRange.compareBoundaryPoints(Range.END_TO_END, nodeRange) >= 0) {
|
|
62
|
+
offset += node.textContent?.length || 0;
|
|
63
|
+
} else {
|
|
64
|
+
// Partial node - cursor is in this node
|
|
65
|
+
offset += range.startOffset;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
node = walker.nextNode();
|
|
70
|
+
}
|
|
71
|
+
return offset;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set cursor to offset in plain text within a contenteditable element
|
|
76
|
+
*/
|
|
77
|
+
function setCursorOffset(codeRef: HTMLPreElement | null, targetOffset: number): void {
|
|
78
|
+
if (!codeRef) return;
|
|
79
|
+
|
|
80
|
+
const selection = window.getSelection();
|
|
81
|
+
if (!selection) return;
|
|
82
|
+
|
|
83
|
+
let currentOffset = 0;
|
|
84
|
+
const walker = document.createTreeWalker(codeRef, NodeFilter.SHOW_TEXT);
|
|
85
|
+
let node = walker.nextNode();
|
|
86
|
+
|
|
87
|
+
while (node) {
|
|
88
|
+
const nodeLength = node.textContent?.length || 0;
|
|
89
|
+
if (currentOffset + nodeLength >= targetOffset) {
|
|
90
|
+
const range = document.createRange();
|
|
91
|
+
range.setStart(node, targetOffset - currentOffset);
|
|
92
|
+
range.collapse(true);
|
|
93
|
+
selection.removeAllRanges();
|
|
94
|
+
selection.addRange(range);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
currentOffset += nodeLength;
|
|
98
|
+
node = walker.nextNode();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If offset not found, place at end
|
|
102
|
+
const range = document.createRange();
|
|
103
|
+
range.selectNodeContents(codeRef);
|
|
104
|
+
range.collapse(false);
|
|
105
|
+
selection.removeAllRanges();
|
|
106
|
+
selection.addRange(range);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get current line info from cursor position
|
|
111
|
+
*/
|
|
112
|
+
function getCurrentLineInfo(editingContent: string, codeRef: HTMLPreElement | null): { indent: string; lineContent: string } | null {
|
|
113
|
+
const text = editingContent;
|
|
114
|
+
if (!text) return { indent: "", lineContent: "" };
|
|
115
|
+
|
|
116
|
+
// Get cursor position in plain text
|
|
117
|
+
const cursorOffset = getCursorOffset(codeRef);
|
|
118
|
+
|
|
119
|
+
// Find the start of the current line (after the previous newline)
|
|
120
|
+
const textBeforeCursor = text.substring(0, cursorOffset);
|
|
121
|
+
const lastNewlineIndex = textBeforeCursor.lastIndexOf("\n");
|
|
122
|
+
const lineStart = lastNewlineIndex + 1;
|
|
123
|
+
|
|
124
|
+
// Get the content from line start to cursor
|
|
125
|
+
const lineContent = textBeforeCursor.substring(lineStart);
|
|
126
|
+
|
|
127
|
+
// Extract indentation (spaces/tabs at start of line)
|
|
128
|
+
const indentMatch = lineContent.match(/^[\t ]*/);
|
|
129
|
+
const indent = indentMatch ? indentMatch[0] : "";
|
|
130
|
+
|
|
131
|
+
return { indent, lineContent };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Calculate smart indentation based on context
|
|
136
|
+
*/
|
|
137
|
+
function getSmartIndent(lineInfo: { indent: string; lineContent: string }, format: CodeFormat): string {
|
|
138
|
+
const { indent, lineContent } = lineInfo;
|
|
139
|
+
const trimmedLine = lineContent.trim();
|
|
140
|
+
const indentUnit = " "; // 2 spaces
|
|
141
|
+
|
|
142
|
+
if (format === "yaml") {
|
|
143
|
+
// After a key with colon (e.g., "key:" or "key: |" or "key: >")
|
|
144
|
+
if (trimmedLine.endsWith(":") || trimmedLine.match(/:\s*[|>][-+]?\s*$/)) {
|
|
145
|
+
return indent + indentUnit;
|
|
146
|
+
}
|
|
147
|
+
// After array item start (e.g., "- item" or "- key: value")
|
|
148
|
+
if (trimmedLine.startsWith("- ")) {
|
|
149
|
+
return indent + indentUnit;
|
|
150
|
+
}
|
|
151
|
+
// Just "- " alone means we want to continue with same array indentation
|
|
152
|
+
if (trimmedLine === "-") {
|
|
153
|
+
return indent;
|
|
154
|
+
}
|
|
155
|
+
} else if (format === "json") {
|
|
156
|
+
// After opening brace/bracket
|
|
157
|
+
if (trimmedLine.endsWith("{") || trimmedLine.endsWith("[")) {
|
|
158
|
+
return indent + indentUnit;
|
|
159
|
+
}
|
|
160
|
+
// After comma, maintain indentation
|
|
161
|
+
if (trimmedLine.endsWith(",")) {
|
|
162
|
+
return indent;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Default: maintain current indentation
|
|
167
|
+
return indent;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Composable for CodeViewer editor functionality
|
|
172
|
+
*/
|
|
173
|
+
export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCodeViewerEditorReturn {
|
|
174
|
+
const { codeRef, codeFormat, currentFormat, canEdit, editable, onEmitModelValue, onEmitEditable } = options;
|
|
175
|
+
|
|
176
|
+
// Debounce timeout handles
|
|
177
|
+
let validationTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
178
|
+
let highlightTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
179
|
+
|
|
180
|
+
// Local state
|
|
181
|
+
const internalEditable = ref(editable.value);
|
|
182
|
+
const editingContent = ref("");
|
|
183
|
+
const cachedHighlightedContent = ref("");
|
|
184
|
+
const isUserEditing = ref(false);
|
|
185
|
+
const validationError = ref<ValidationError | null>(null);
|
|
186
|
+
|
|
187
|
+
// Computed: has validation error
|
|
188
|
+
const hasValidationError = computed(() => validationError.value !== null);
|
|
189
|
+
|
|
190
|
+
// Computed: is currently in edit mode
|
|
191
|
+
const isEditing = computed(() => canEdit.value && internalEditable.value);
|
|
192
|
+
|
|
193
|
+
// Computed: display content
|
|
194
|
+
const displayContent = computed(() => {
|
|
195
|
+
if (isUserEditing.value) {
|
|
196
|
+
return editingContent.value;
|
|
197
|
+
}
|
|
198
|
+
return codeFormat.formattedContent.value;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Computed: highlighted content with syntax highlighting
|
|
202
|
+
const highlightedContent = computed(() => {
|
|
203
|
+
if (isUserEditing.value) {
|
|
204
|
+
return cachedHighlightedContent.value;
|
|
205
|
+
}
|
|
206
|
+
const highlighted = highlightSyntax(displayContent.value, { format: currentFormat.value });
|
|
207
|
+
cachedHighlightedContent.value = highlighted;
|
|
208
|
+
return highlighted;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Computed: is current content valid
|
|
212
|
+
const isValid = computed(() => {
|
|
213
|
+
if (hasValidationError.value) return false;
|
|
214
|
+
return codeFormat.isValid.value;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Computed: character count
|
|
218
|
+
const charCount = computed(() => {
|
|
219
|
+
return displayContent.value?.length || 0;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Sync internal editable state with prop
|
|
223
|
+
function syncEditableFromProp(value: boolean): void {
|
|
224
|
+
internalEditable.value = value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Sync editing content when external value changes
|
|
228
|
+
function syncEditingContentFromValue(): void {
|
|
229
|
+
if (!isUserEditing.value) {
|
|
230
|
+
editingContent.value = codeFormat.formattedContent.value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Update editing content when format changes
|
|
235
|
+
function updateEditingContentOnFormatChange(): void {
|
|
236
|
+
if (isEditing.value) {
|
|
237
|
+
editingContent.value = codeFormat.formattedContent.value;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Debounced validation
|
|
242
|
+
function debouncedValidate(): void {
|
|
243
|
+
if (validationTimeout) {
|
|
244
|
+
clearTimeout(validationTimeout);
|
|
245
|
+
}
|
|
246
|
+
validationTimeout = setTimeout(() => {
|
|
247
|
+
validationError.value = codeFormat.validateWithError(editingContent.value, currentFormat.value);
|
|
248
|
+
}, 300);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Debounced highlighting
|
|
252
|
+
function debouncedHighlight(): void {
|
|
253
|
+
if (highlightTimeout) {
|
|
254
|
+
clearTimeout(highlightTimeout);
|
|
255
|
+
}
|
|
256
|
+
highlightTimeout = setTimeout(() => {
|
|
257
|
+
if (!codeRef.value || !isEditing.value) return;
|
|
258
|
+
|
|
259
|
+
// Save cursor position
|
|
260
|
+
const cursorOffset = getCursorOffset(codeRef.value);
|
|
261
|
+
|
|
262
|
+
// Re-apply highlighting
|
|
263
|
+
codeRef.value.innerHTML = highlightSyntax(editingContent.value, { format: currentFormat.value });
|
|
264
|
+
|
|
265
|
+
// Restore cursor position
|
|
266
|
+
setCursorOffset(codeRef.value, cursorOffset);
|
|
267
|
+
}, 300);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Toggle edit mode
|
|
271
|
+
function toggleEdit(): void {
|
|
272
|
+
internalEditable.value = !internalEditable.value;
|
|
273
|
+
onEmitEditable(internalEditable.value);
|
|
274
|
+
|
|
275
|
+
if (internalEditable.value) {
|
|
276
|
+
// Entering edit mode - initialize editing content and clear any previous error
|
|
277
|
+
editingContent.value = codeFormat.formattedContent.value;
|
|
278
|
+
validationError.value = null;
|
|
279
|
+
// Set content imperatively with syntax highlighting and focus
|
|
280
|
+
nextTick(() => {
|
|
281
|
+
if (codeRef.value) {
|
|
282
|
+
codeRef.value.innerHTML = highlightSyntax(editingContent.value, { format: currentFormat.value });
|
|
283
|
+
codeRef.value.focus();
|
|
284
|
+
// Move cursor to end
|
|
285
|
+
const selection = window.getSelection();
|
|
286
|
+
const range = document.createRange();
|
|
287
|
+
range.selectNodeContents(codeRef.value);
|
|
288
|
+
range.collapse(false);
|
|
289
|
+
selection?.removeAllRanges();
|
|
290
|
+
selection?.addRange(range);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
// Exiting edit mode - clear validation error
|
|
295
|
+
validationError.value = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Handle contenteditable input
|
|
300
|
+
function onContentEditableInput(event: Event): void {
|
|
301
|
+
if (!isEditing.value) return;
|
|
302
|
+
|
|
303
|
+
isUserEditing.value = true;
|
|
304
|
+
const target = event.target as HTMLElement;
|
|
305
|
+
editingContent.value = target.innerText || "";
|
|
306
|
+
|
|
307
|
+
debouncedValidate();
|
|
308
|
+
debouncedHighlight();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Handle blur - emit changes and re-apply highlighting
|
|
312
|
+
function onContentEditableBlur(): void {
|
|
313
|
+
if (!isEditing.value || !isUserEditing.value) return;
|
|
314
|
+
|
|
315
|
+
isUserEditing.value = false;
|
|
316
|
+
|
|
317
|
+
// Clear pending timeouts and process immediately
|
|
318
|
+
if (validationTimeout) {
|
|
319
|
+
clearTimeout(validationTimeout);
|
|
320
|
+
validationTimeout = null;
|
|
321
|
+
}
|
|
322
|
+
if (highlightTimeout) {
|
|
323
|
+
clearTimeout(highlightTimeout);
|
|
324
|
+
highlightTimeout = null;
|
|
325
|
+
}
|
|
326
|
+
validationError.value = codeFormat.validateWithError(editingContent.value, currentFormat.value);
|
|
327
|
+
|
|
328
|
+
// Parse and emit the value
|
|
329
|
+
const parsed = codeFormat.parse(editingContent.value);
|
|
330
|
+
if (parsed) {
|
|
331
|
+
onEmitModelValue(parsed);
|
|
332
|
+
} else {
|
|
333
|
+
onEmitModelValue(editingContent.value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Re-apply syntax highlighting after editing
|
|
337
|
+
if (codeRef.value) {
|
|
338
|
+
codeRef.value.innerHTML = highlightSyntax(editingContent.value, { format: currentFormat.value });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Handle keyboard shortcuts in edit mode
|
|
343
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
344
|
+
if (!isEditing.value) return;
|
|
345
|
+
|
|
346
|
+
// Enter key - smart indentation
|
|
347
|
+
if (event.key === "Enter") {
|
|
348
|
+
const lineInfo = getCurrentLineInfo(editingContent.value, codeRef.value);
|
|
349
|
+
if (lineInfo) {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
const smartIndent = getSmartIndent(lineInfo, currentFormat.value);
|
|
352
|
+
|
|
353
|
+
const selection = window.getSelection();
|
|
354
|
+
if (selection && selection.rangeCount > 0) {
|
|
355
|
+
const range = selection.getRangeAt(0);
|
|
356
|
+
range.deleteContents();
|
|
357
|
+
const textNode = document.createTextNode("\n" + smartIndent);
|
|
358
|
+
range.insertNode(textNode);
|
|
359
|
+
range.setStartAfter(textNode);
|
|
360
|
+
range.setEndAfter(textNode);
|
|
361
|
+
selection.removeAllRanges();
|
|
362
|
+
selection.addRange(range);
|
|
363
|
+
|
|
364
|
+
codeRef.value?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Tab key - insert spaces instead of moving focus
|
|
370
|
+
if (event.key === "Tab") {
|
|
371
|
+
event.preventDefault();
|
|
372
|
+
document.execCommand("insertText", false, " ");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Escape - exit edit mode
|
|
376
|
+
if (event.key === "Escape") {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
onContentEditableBlur();
|
|
379
|
+
toggleEdit();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Ctrl/Cmd + S - save without exiting
|
|
383
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
|
384
|
+
event.preventDefault();
|
|
385
|
+
onContentEditableBlur();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Cleanup timeouts on unmount
|
|
390
|
+
onUnmounted(() => {
|
|
391
|
+
if (validationTimeout) clearTimeout(validationTimeout);
|
|
392
|
+
if (highlightTimeout) clearTimeout(highlightTimeout);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
// State
|
|
397
|
+
internalEditable,
|
|
398
|
+
editingContent,
|
|
399
|
+
cachedHighlightedContent,
|
|
400
|
+
isUserEditing,
|
|
401
|
+
validationError,
|
|
402
|
+
|
|
403
|
+
// Computed
|
|
404
|
+
isEditing,
|
|
405
|
+
hasValidationError,
|
|
406
|
+
highlightedContent,
|
|
407
|
+
displayContent,
|
|
408
|
+
charCount,
|
|
409
|
+
isValid,
|
|
410
|
+
|
|
411
|
+
// Methods
|
|
412
|
+
toggleEdit,
|
|
413
|
+
onContentEditableInput,
|
|
414
|
+
onContentEditableBlur,
|
|
415
|
+
onKeyDown,
|
|
416
|
+
syncEditableFromProp,
|
|
417
|
+
syncEditingContentFromValue,
|
|
418
|
+
updateEditingContentOnFormatChange
|
|
419
|
+
};
|
|
420
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { computed, ComputedRef, Ref } from "vue";
|
|
2
|
+
import * as fileHelpers from "../helpers/filePreviewHelpers";
|
|
3
|
+
import { getMimeType, getOptimizedUrl, isExternalLinkFile } from "../helpers/filePreviewHelpers";
|
|
4
|
+
import { UploadedFile } from "../types";
|
|
5
|
+
|
|
6
|
+
export interface FileTranscode {
|
|
7
|
+
status: "Complete" | "Pending" | "In Progress" | "Timeout";
|
|
8
|
+
progress: number;
|
|
9
|
+
estimate_ms: number;
|
|
10
|
+
started_at: string;
|
|
11
|
+
completed_at: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseFilePreviewOptions {
|
|
16
|
+
file: Ref<UploadedFile | null | undefined>;
|
|
17
|
+
src?: Ref<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseFilePreviewReturn {
|
|
21
|
+
computedImage: ComputedRef<UploadedFile | null>;
|
|
22
|
+
filename: ComputedRef<string>;
|
|
23
|
+
mimeType: ComputedRef<string>;
|
|
24
|
+
isImage: ComputedRef<boolean>;
|
|
25
|
+
isVideo: ComputedRef<boolean>;
|
|
26
|
+
isPdf: ComputedRef<boolean>;
|
|
27
|
+
isExternalLink: ComputedRef<boolean>;
|
|
28
|
+
previewUrl: ComputedRef<string>;
|
|
29
|
+
thumbUrl: ComputedRef<string>;
|
|
30
|
+
isPreviewable: ComputedRef<boolean>;
|
|
31
|
+
hasMetadata: ComputedRef<boolean>;
|
|
32
|
+
metadataKeyCount: ComputedRef<number>;
|
|
33
|
+
filteredMetadata: ComputedRef<Record<string, unknown>>;
|
|
34
|
+
hasTranscodes: ComputedRef<boolean>;
|
|
35
|
+
transcodingStatus: ComputedRef<(FileTranscode & { message: string }) | null>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Composable for file preview computed properties
|
|
40
|
+
*/
|
|
41
|
+
export function useFilePreview(options: UseFilePreviewOptions): UseFilePreviewReturn {
|
|
42
|
+
const { file, src } = options;
|
|
43
|
+
|
|
44
|
+
const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
|
|
45
|
+
if (file.value) {
|
|
46
|
+
return file.value;
|
|
47
|
+
} else if (src?.value) {
|
|
48
|
+
return {
|
|
49
|
+
id: src.value,
|
|
50
|
+
url: src.value,
|
|
51
|
+
type: "image/" + src.value.split(".").pop()?.toLowerCase(),
|
|
52
|
+
name: "",
|
|
53
|
+
size: 0,
|
|
54
|
+
__type: "BrowserFile"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
|
|
61
|
+
const mimeType = computed(() => computedImage.value ? getMimeType(computedImage.value) : "");
|
|
62
|
+
const isImage = computed(() => computedImage.value ? fileHelpers.isImage(computedImage.value) : false);
|
|
63
|
+
const isVideo = computed(() => computedImage.value ? fileHelpers.isVideo(computedImage.value) : false);
|
|
64
|
+
const isPdf = computed(() => computedImage.value ? fileHelpers.isPdf(computedImage.value) : false);
|
|
65
|
+
const isExternalLink = computed(() => computedImage.value ? isExternalLinkFile(computedImage.value) : false);
|
|
66
|
+
const previewUrl = computed(() => computedImage.value ? getOptimizedUrl(computedImage.value) : "");
|
|
67
|
+
const thumbUrl = computed(() => computedImage.value?.thumb?.url || "");
|
|
68
|
+
const isPreviewable = computed(() => !!thumbUrl.value || isVideo.value || isImage.value);
|
|
69
|
+
|
|
70
|
+
const hasMetadata = computed(() => {
|
|
71
|
+
if (!file.value?.meta) return false;
|
|
72
|
+
const metaKeys = Object.keys(file.value.meta).filter(k => k !== "transcodes");
|
|
73
|
+
return metaKeys.length > 0;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const metadataKeyCount = computed(() => {
|
|
77
|
+
if (!file.value?.meta) return 0;
|
|
78
|
+
return Object.keys(file.value.meta).filter(k => k !== "transcodes").length;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const filteredMetadata = computed(() => {
|
|
82
|
+
if (!file.value?.meta) return {};
|
|
83
|
+
const { transcodes, ...rest } = file.value.meta;
|
|
84
|
+
return rest;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const hasTranscodes = computed(() => (file.value?.transcodes?.length || 0) > 0);
|
|
88
|
+
|
|
89
|
+
const transcodingStatus = computed(() => {
|
|
90
|
+
const metaTranscodes: Record<string, FileTranscode> = file.value?.meta?.transcodes || {};
|
|
91
|
+
|
|
92
|
+
for (const transcodeName of Object.keys(metaTranscodes)) {
|
|
93
|
+
const transcode = metaTranscodes[transcodeName];
|
|
94
|
+
if (!["Complete", "Timeout"].includes(transcode?.status)) {
|
|
95
|
+
return { ...transcode, message: `${transcodeName} ${transcode.status}` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
computedImage,
|
|
104
|
+
filename,
|
|
105
|
+
mimeType,
|
|
106
|
+
isImage,
|
|
107
|
+
isVideo,
|
|
108
|
+
isPdf,
|
|
109
|
+
isExternalLink,
|
|
110
|
+
previewUrl,
|
|
111
|
+
thumbUrl,
|
|
112
|
+
isPreviewable,
|
|
113
|
+
hasMetadata,
|
|
114
|
+
metadataKeyCount,
|
|
115
|
+
filteredMetadata,
|
|
116
|
+
hasTranscodes,
|
|
117
|
+
transcodingStatus
|
|
118
|
+
};
|
|
119
|
+
}
|