quasar-ui-danx 0.5.0 → 0.5.2
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/.claude/settings.local.json +8 -0
- package/dist/danx.es.js +16119 -10641
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +202 -123
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +8 -1
- package/src/components/Utility/Buttons/ActionButton.vue +15 -5
- package/src/components/Utility/Code/CodeViewer.vue +41 -16
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/MarkdownContent.vue +31 -163
- package/src/components/Utility/Code/index.ts +3 -0
- package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
- package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
- package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
- package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
- package/src/components/Utility/Markdown/MarkdownEditor.vue +233 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -0
- package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
- package/src/components/Utility/Markdown/TablePopover.vue +420 -0
- package/src/components/Utility/Markdown/index.ts +11 -0
- package/src/components/Utility/Markdown/types.ts +27 -0
- package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
- package/src/composables/markdown/features/useBlockquotes.ts +248 -0
- package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +805 -0
- package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
- package/src/composables/markdown/features/useContextMenu.ts +444 -0
- package/src/composables/markdown/features/useFocusTracking.ts +116 -0
- package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
- package/src/composables/markdown/features/useHeadings.ts +290 -0
- package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
- package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
- package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
- package/src/composables/markdown/features/useLinks.spec.ts +388 -0
- package/src/composables/markdown/features/useLinks.ts +374 -0
- package/src/composables/markdown/features/useLists.spec.ts +834 -0
- package/src/composables/markdown/features/useLists.ts +747 -0
- package/src/composables/markdown/features/usePopoverManager.ts +181 -0
- package/src/composables/markdown/features/useTables.spec.ts +1601 -0
- package/src/composables/markdown/features/useTables.ts +1107 -0
- package/src/composables/markdown/index.ts +16 -0
- package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
- package/src/composables/markdown/useMarkdownEditor.ts +1077 -0
- package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
- package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
- package/src/composables/markdown/useMarkdownSelection.ts +219 -0
- package/src/composables/markdown/useMarkdownSync.ts +549 -0
- package/src/composables/useCodeFormat.ts +17 -10
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/highlightCSS.ts +236 -0
- package/src/helpers/formats/highlightHTML.ts +483 -0
- package/src/helpers/formats/highlightJavaScript.ts +346 -0
- package/src/helpers/formats/highlightSyntax.ts +15 -4
- package/src/helpers/formats/index.ts +3 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +425 -0
- package/src/helpers/formats/markdown/index.ts +7 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/styles/danx.scss +3 -3
- package/src/styles/index.scss +5 -5
- package/src/styles/themes/danx/code.scss +257 -1
- package/src/styles/themes/danx/index.scss +10 -10
- package/src/styles/themes/danx/markdown.scss +59 -0
- package/src/test/helpers/editorTestUtils.spec.ts +296 -0
- package/src/test/helpers/editorTestUtils.ts +253 -0
- package/src/test/helpers/index.ts +1 -0
- package/src/test/highlighters.test.ts +153 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/src/types/widgets.d.ts +2 -2
- package/vite.config.js +5 -1
- package/vitest.config.ts +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quasar-ui-danx",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"author": "Dan <dan@flytedesk.com>",
|
|
5
5
|
"description": "DanX Vue / Quasar component library",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"build:bundle": "vite build",
|
|
16
16
|
"build": "yarn clean && yarn build:types && yarn build:bundle",
|
|
17
17
|
"preview": "vite preview",
|
|
18
|
+
"test": "vitest",
|
|
19
|
+
"test:run": "vitest run",
|
|
20
|
+
"test:coverage": "vitest run --coverage",
|
|
18
21
|
"publish:patch": "./scripts/publish.sh patch",
|
|
19
22
|
"publish:minor": "./scripts/publish.sh minor",
|
|
20
23
|
"publish:major": "./scripts/publish.sh major"
|
|
@@ -34,6 +37,8 @@
|
|
|
34
37
|
"@typescript-eslint/eslint-plugin": "^7.6.0",
|
|
35
38
|
"@typescript-eslint/parser": "^7.6.0",
|
|
36
39
|
"@vitejs/plugin-vue": "^5.0.4",
|
|
40
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
41
|
+
"@vue/test-utils": "^2.4.6",
|
|
37
42
|
"autoprefixer": "^10.4.19",
|
|
38
43
|
"chalk": "^4.1.0",
|
|
39
44
|
"core-js": "^3.0.0",
|
|
@@ -42,6 +47,7 @@
|
|
|
42
47
|
"eslint-plugin-import": "^2.29.1",
|
|
43
48
|
"eslint-plugin-vue": "^9.24.1",
|
|
44
49
|
"fs-extra": "^8.1.0",
|
|
50
|
+
"jsdom": "^27.4.0",
|
|
45
51
|
"postcss": "^8.4.38",
|
|
46
52
|
"quasar": "^2.0.0",
|
|
47
53
|
"sass": "^1.33.0",
|
|
@@ -51,6 +57,7 @@
|
|
|
51
57
|
"vite": "^5.2.8",
|
|
52
58
|
"vite-plugin-dts": "^3.8.1",
|
|
53
59
|
"vite-svg-loader": "^5.1.0",
|
|
60
|
+
"vitest": "^4.0.16",
|
|
54
61
|
"vue": "^3.4.21",
|
|
55
62
|
"vue-eslint-parser": "^9.4.2",
|
|
56
63
|
"vue-router": "^4.3.2"
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
>
|
|
9
9
|
<div class="flex items-center flex-nowrap">
|
|
10
10
|
<component
|
|
11
|
+
v-if="icon || typeOptions.icon"
|
|
11
12
|
:is="icon || typeOptions.icon"
|
|
12
13
|
class="transition-all"
|
|
13
14
|
:class="resolvedIconClass"
|
|
14
15
|
/>
|
|
15
16
|
<div
|
|
16
17
|
v-if="label || label === 0"
|
|
17
|
-
class="ml-2"
|
|
18
|
-
:class="labelClass"
|
|
18
|
+
:class="[labelClass, { 'ml-2': icon || typeOptions.icon }]"
|
|
19
19
|
>
|
|
20
20
|
{{ label }}
|
|
21
21
|
</div>
|
|
@@ -87,7 +87,7 @@ import { ActionTarget, ResourceAction } from "../../../types";
|
|
|
87
87
|
|
|
88
88
|
export interface ActionButtonProps {
|
|
89
89
|
type?: "save" | "trash" | "back" | "create" | "edit" | "copy" | "folder" | "document" | "play" | "stop" | "pause" | "refresh" | "restart" | "confirm" | "cancel" | "export" | "import" | "minus" | "merge" | "check" | "clock" | "view" | "database" | "users" | "close";
|
|
90
|
-
color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "green" | "green-invert" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "yellow" | "orange" | "amber" | "purple" | "teal" | "teal-invert";
|
|
90
|
+
color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "sky-soft" | "green" | "green-invert" | "green-soft" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "slate-soft" | "yellow" | "orange" | "amber" | "purple" | "blue-soft" | "red-soft" | "teal" | "teal-invert";
|
|
91
91
|
size?: "xxs" | "xs" | "sm" | "md" | "lg";
|
|
92
92
|
icon?: object | string;
|
|
93
93
|
iconClass?: string;
|
|
@@ -126,7 +126,7 @@ const props = withDefaults(defineProps<ActionButtonProps>(), {
|
|
|
126
126
|
const mappedSizeClass = {
|
|
127
127
|
xxs: {
|
|
128
128
|
icon: "w-2",
|
|
129
|
-
button: "px
|
|
129
|
+
button: "px-1 h-4 text-[10px]"
|
|
130
130
|
},
|
|
131
131
|
xs: {
|
|
132
132
|
icon: "w-3",
|
|
@@ -157,16 +157,22 @@ const colorClass = computed(() => {
|
|
|
157
157
|
switch (props.color) {
|
|
158
158
|
case "red":
|
|
159
159
|
return "text-red-900 bg-red-300 hover:bg-red-400";
|
|
160
|
+
case "red-soft":
|
|
161
|
+
return "text-red-700 bg-red-100 hover:bg-red-200";
|
|
160
162
|
case "lime":
|
|
161
163
|
return "text-lime-900 bg-lime-300 hover:bg-lime-400";
|
|
162
164
|
case "green":
|
|
163
165
|
return "text-green-900 bg-green-300 hover:bg-green-400";
|
|
164
166
|
case "green-invert":
|
|
165
167
|
return "text-green-300 bg-green-900 hover:bg-green-800";
|
|
168
|
+
case "green-soft":
|
|
169
|
+
return "text-green-700 bg-green-100 hover:bg-green-200";
|
|
166
170
|
case "blue":
|
|
167
171
|
return "text-blue-900 bg-blue-300 hover:bg-blue-400";
|
|
168
172
|
case "blue-invert":
|
|
169
173
|
return "text-blue-300 bg-blue-900 hover:bg-blue-800";
|
|
174
|
+
case "blue-soft":
|
|
175
|
+
return "text-blue-700 bg-blue-100 hover:bg-blue-200";
|
|
170
176
|
case "teal":
|
|
171
177
|
return "text-teal-800 bg-teal-200 hover:bg-teal-400";
|
|
172
178
|
case "teal-invert":
|
|
@@ -175,6 +181,8 @@ const colorClass = computed(() => {
|
|
|
175
181
|
return "text-sky-900 bg-sky-300 hover:bg-sky-400";
|
|
176
182
|
case "sky-invert":
|
|
177
183
|
return "text-sky-400 bg-sky-800 hover:bg-sky-900";
|
|
184
|
+
case "sky-soft":
|
|
185
|
+
return "text-sky-700 bg-sky-100 hover:bg-sky-200";
|
|
178
186
|
case "white":
|
|
179
187
|
return "text-white bg-gray-800 hover:bg-gray-200";
|
|
180
188
|
case "yellow":
|
|
@@ -189,6 +197,8 @@ const colorClass = computed(() => {
|
|
|
189
197
|
return "text-slate-900 bg-slate-300 hover:bg-slate-400";
|
|
190
198
|
case "slate-invert":
|
|
191
199
|
return "text-slate-300 bg-slate-900 hover:bg-slate-800";
|
|
200
|
+
case "slate-soft":
|
|
201
|
+
return "text-slate-600 bg-slate-100 hover:bg-slate-200";
|
|
192
202
|
case "purple":
|
|
193
203
|
return "text-purple-300 bg-purple-900 hover:bg-purple-800";
|
|
194
204
|
default:
|
|
@@ -251,7 +261,7 @@ const typeOptions = computed(() => {
|
|
|
251
261
|
case "close":
|
|
252
262
|
return { icon: CloseIcon };
|
|
253
263
|
default:
|
|
254
|
-
return { icon:
|
|
264
|
+
return { icon: null };
|
|
255
265
|
}
|
|
256
266
|
});
|
|
257
267
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
class="dx-code-viewer group flex flex-col"
|
|
4
|
-
:class="{ 'is-collapsed': isCollapsed }"
|
|
4
|
+
:class="[{ 'is-collapsed': isCollapsed }, props.theme === 'light' ? 'theme-light' : '']"
|
|
5
5
|
>
|
|
6
6
|
<FieldLabel
|
|
7
7
|
v-if="label"
|
|
@@ -15,18 +15,25 @@
|
|
|
15
15
|
:preview="collapsedPreview"
|
|
16
16
|
:format="currentFormat"
|
|
17
17
|
:available-formats="currentFormat === 'json' || currentFormat === 'yaml' ? ['json', 'yaml'] : currentFormat === 'text' || currentFormat === 'markdown' ? ['text', 'markdown'] : []"
|
|
18
|
+
:allow-any-language="allowAnyLanguage"
|
|
18
19
|
@expand="toggleCollapse"
|
|
19
20
|
@format-change="onFormatChange"
|
|
20
21
|
/>
|
|
21
22
|
|
|
22
23
|
<!-- Expanded view - full code viewer -->
|
|
23
24
|
<template v-else>
|
|
24
|
-
<div
|
|
25
|
+
<div
|
|
26
|
+
class="code-wrapper relative flex flex-col flex-1 min-h-0"
|
|
27
|
+
tabindex="0"
|
|
28
|
+
@keydown="editor.onKeyDown"
|
|
29
|
+
>
|
|
25
30
|
<!-- Language badge - shows popout format options on hover -->
|
|
26
31
|
<LanguageBadge
|
|
32
|
+
ref="languageBadgeRef"
|
|
27
33
|
:format="currentFormat"
|
|
28
34
|
:available-formats="currentFormat === 'json' || currentFormat === 'yaml' ? ['json', 'yaml'] : currentFormat === 'text' || currentFormat === 'markdown' ? ['text', 'markdown'] : []"
|
|
29
35
|
:toggleable="true"
|
|
36
|
+
:allow-any-language="allowAnyLanguage"
|
|
30
37
|
@click.stop
|
|
31
38
|
@change="onFormatChange"
|
|
32
39
|
/>
|
|
@@ -77,10 +84,12 @@
|
|
|
77
84
|
|
|
78
85
|
<!-- Footer with char count and edit toggle -->
|
|
79
86
|
<CodeViewerFooter
|
|
87
|
+
v-if="!hideFooter"
|
|
80
88
|
:char-count="editor.charCount.value"
|
|
81
89
|
:validation-error="editor.validationError.value"
|
|
82
90
|
:can-edit="canEdit && currentFormat !== 'markdown'"
|
|
83
91
|
:is-editing="editor.isEditing.value"
|
|
92
|
+
:show-version="showVersion"
|
|
84
93
|
@toggle-edit="editor.toggleEdit"
|
|
85
94
|
/>
|
|
86
95
|
</div>
|
|
@@ -110,6 +119,10 @@ export interface CodeViewerProps {
|
|
|
110
119
|
collapsible?: boolean;
|
|
111
120
|
defaultCollapsed?: boolean;
|
|
112
121
|
defaultCodeFormat?: "json" | "yaml";
|
|
122
|
+
allowAnyLanguage?: boolean;
|
|
123
|
+
theme?: "dark" | "light";
|
|
124
|
+
showVersion?: boolean;
|
|
125
|
+
hideFooter?: boolean;
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
const props = withDefaults(defineProps<CodeViewerProps>(), {
|
|
@@ -120,13 +133,18 @@ const props = withDefaults(defineProps<CodeViewerProps>(), {
|
|
|
120
133
|
canEdit: false,
|
|
121
134
|
editable: false,
|
|
122
135
|
collapsible: false,
|
|
123
|
-
defaultCollapsed: true
|
|
136
|
+
defaultCollapsed: true,
|
|
137
|
+
theme: "dark",
|
|
138
|
+
showVersion: false,
|
|
139
|
+
hideFooter: false
|
|
124
140
|
});
|
|
125
141
|
|
|
126
142
|
const emit = defineEmits<{
|
|
127
143
|
"update:modelValue": [value: object | string | null];
|
|
128
144
|
"update:format": [format: CodeFormat];
|
|
129
145
|
"update:editable": [editable: boolean];
|
|
146
|
+
"exit": [];
|
|
147
|
+
"delete": [];
|
|
130
148
|
}>();
|
|
131
149
|
|
|
132
150
|
// Initialize composable with current props
|
|
@@ -138,6 +156,7 @@ const codeFormat = useCodeFormat({
|
|
|
138
156
|
// Local state
|
|
139
157
|
const currentFormat = ref<CodeFormat>(props.format);
|
|
140
158
|
const codeRef = ref<HTMLPreElement | null>(null);
|
|
159
|
+
const languageBadgeRef = ref<InstanceType<typeof LanguageBadge> | null>(null);
|
|
141
160
|
|
|
142
161
|
// Collapsed state (for collapsible mode)
|
|
143
162
|
const isCollapsed = ref(props.collapsible && props.defaultCollapsed);
|
|
@@ -154,9 +173,26 @@ function toggleCollapse() {
|
|
|
154
173
|
isCollapsed.value = !isCollapsed.value;
|
|
155
174
|
}
|
|
156
175
|
|
|
157
|
-
//
|
|
176
|
+
// Initialize editor composable
|
|
177
|
+
const editor = useCodeViewerEditor({
|
|
178
|
+
codeRef,
|
|
179
|
+
codeFormat,
|
|
180
|
+
currentFormat,
|
|
181
|
+
canEdit: toRef(props, "canEdit"),
|
|
182
|
+
editable: toRef(props, "editable"),
|
|
183
|
+
onEmitModelValue: (value) => emit("update:modelValue", value),
|
|
184
|
+
onEmitEditable: (editable) => emit("update:editable", editable),
|
|
185
|
+
onEmitFormat: (format) => onFormatChange(format),
|
|
186
|
+
onExit: () => emit("exit"),
|
|
187
|
+
onDelete: () => emit("delete"),
|
|
188
|
+
onOpenLanguageSearch: () => languageBadgeRef.value?.openSearchPanel()
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Sync composable format with current format and update editor content
|
|
158
192
|
watch(currentFormat, (newFormat) => {
|
|
159
193
|
codeFormat.setFormat(newFormat);
|
|
194
|
+
// Update editor content when format changes (needed for syntax re-highlighting)
|
|
195
|
+
editor.updateEditingContentOnFormatChange();
|
|
160
196
|
});
|
|
161
197
|
|
|
162
198
|
// Watch for external format changes
|
|
@@ -170,17 +206,6 @@ watch(() => props.modelValue, () => {
|
|
|
170
206
|
editor.syncEditingContentFromValue();
|
|
171
207
|
});
|
|
172
208
|
|
|
173
|
-
// Initialize editor composable
|
|
174
|
-
const editor = useCodeViewerEditor({
|
|
175
|
-
codeRef,
|
|
176
|
-
codeFormat,
|
|
177
|
-
currentFormat,
|
|
178
|
-
canEdit: toRef(props, "canEdit"),
|
|
179
|
-
editable: toRef(props, "editable"),
|
|
180
|
-
onEmitModelValue: (value) => emit("update:modelValue", value),
|
|
181
|
-
onEmitEditable: (editable) => emit("update:editable", editable)
|
|
182
|
-
});
|
|
183
|
-
|
|
184
209
|
// Sync internal editable state with prop
|
|
185
210
|
watch(() => props.editable, (newValue) => {
|
|
186
211
|
editor.syncEditableFromProp(newValue);
|
|
@@ -198,7 +223,7 @@ const { collapsedPreview } = useCodeViewerCollapse({
|
|
|
198
223
|
function onFormatChange(newFormat: CodeFormat) {
|
|
199
224
|
currentFormat.value = newFormat;
|
|
200
225
|
emit("update:format", newFormat);
|
|
201
|
-
editor.updateEditingContentOnFormatChange()
|
|
226
|
+
// Note: editor.updateEditingContentOnFormatChange() is called by the currentFormat watcher
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
// Get the raw markdown content for MarkdownContent component
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
:format="format"
|
|
12
12
|
:available-formats="availableFormats"
|
|
13
13
|
:toggleable="availableFormats.length > 1"
|
|
14
|
+
:allow-any-language="allowAnyLanguage"
|
|
14
15
|
@click.stop
|
|
15
16
|
@change="(fmt) => $emit('format-change', fmt)"
|
|
16
17
|
/>
|
|
@@ -25,6 +26,7 @@ defineProps<{
|
|
|
25
26
|
preview: string;
|
|
26
27
|
format: string;
|
|
27
28
|
availableFormats?: string[];
|
|
29
|
+
allowAnyLanguage?: boolean;
|
|
28
30
|
}>();
|
|
29
31
|
|
|
30
32
|
defineEmits<{
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
</template>
|
|
13
13
|
<template v-else>
|
|
14
14
|
{{ charCount.toLocaleString() }} chars
|
|
15
|
+
<span v-if="showVersion" class="ml-2 text-sky-400">[v1.0.5]</span>
|
|
15
16
|
</template>
|
|
16
17
|
</div>
|
|
17
18
|
<!-- Edit toggle button -->
|
|
@@ -26,13 +27,13 @@
|
|
|
26
27
|
@click="$emit('toggle-edit')"
|
|
27
28
|
>
|
|
28
29
|
<EditIcon class="w-3.5 h-3.5" />
|
|
29
|
-
<QTooltip>{{ isEditing ? 'Exit edit mode' : 'Edit content' }}</QTooltip>
|
|
30
30
|
</QBtn>
|
|
31
31
|
</div>
|
|
32
32
|
</template>
|
|
33
33
|
|
|
34
34
|
<script setup lang="ts">
|
|
35
35
|
import { FaSolidPencil as EditIcon } from "danx-icon";
|
|
36
|
+
import { QBtn } from "quasar";
|
|
36
37
|
import { computed } from "vue";
|
|
37
38
|
import { ValidationError } from "../../../composables/useCodeFormat";
|
|
38
39
|
|
|
@@ -41,6 +42,7 @@ export interface CodeViewerFooterProps {
|
|
|
41
42
|
validationError: ValidationError | null;
|
|
42
43
|
canEdit: boolean;
|
|
43
44
|
isEditing: boolean;
|
|
45
|
+
showVersion?: boolean;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const props = defineProps<CodeViewerFooterProps>();
|
|
@@ -3,8 +3,21 @@
|
|
|
3
3
|
class="dx-language-badge-container"
|
|
4
4
|
:class="{ 'is-toggleable': toggleable && availableFormats.length > 1 }"
|
|
5
5
|
@mouseenter="showOptions = true"
|
|
6
|
-
@mouseleave="
|
|
6
|
+
@mouseleave="onMouseLeave"
|
|
7
7
|
>
|
|
8
|
+
<!-- Search icon (when allowAnyLanguage is true) - placed first/leftmost -->
|
|
9
|
+
<transition name="slide-left">
|
|
10
|
+
<div
|
|
11
|
+
v-if="showOptions && allowAnyLanguage"
|
|
12
|
+
class="dx-language-option dx-language-search-trigger"
|
|
13
|
+
@click.stop="openSearchPanel"
|
|
14
|
+
>
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
16
|
+
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
|
17
|
+
</svg>
|
|
18
|
+
</div>
|
|
19
|
+
</transition>
|
|
20
|
+
|
|
8
21
|
<!-- Other format options (slide out to the left) -->
|
|
9
22
|
<transition name="slide-left">
|
|
10
23
|
<div
|
|
@@ -23,36 +36,197 @@
|
|
|
23
36
|
</transition>
|
|
24
37
|
|
|
25
38
|
<!-- Current format badge (stays in place) -->
|
|
26
|
-
<div class="dx-language-badge" :class="{ 'is-active': showOptions && otherFormats.length > 0 }">
|
|
39
|
+
<div class="dx-language-badge" :class="{ 'is-active': showOptions && (otherFormats.length > 0 || allowAnyLanguage) }">
|
|
27
40
|
{{ format.toUpperCase() }}
|
|
28
41
|
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Search dropdown panel -->
|
|
44
|
+
<transition name="fade">
|
|
45
|
+
<div
|
|
46
|
+
v-if="showSearchPanel"
|
|
47
|
+
class="dx-language-search-panel"
|
|
48
|
+
@click.stop
|
|
49
|
+
>
|
|
50
|
+
<input
|
|
51
|
+
ref="searchInputRef"
|
|
52
|
+
v-model="searchQuery"
|
|
53
|
+
type="text"
|
|
54
|
+
class="dx-language-search-input"
|
|
55
|
+
placeholder="Search languages..."
|
|
56
|
+
@input="onSearchQueryChange"
|
|
57
|
+
@keydown.down.prevent="navigateDown"
|
|
58
|
+
@keydown.up.prevent="navigateUp"
|
|
59
|
+
@keydown.enter.prevent="selectCurrentItem"
|
|
60
|
+
@keydown.escape="closeSearchPanel"
|
|
61
|
+
/>
|
|
62
|
+
<div class="dx-language-search-list">
|
|
63
|
+
<div
|
|
64
|
+
v-for="(lang, index) in filteredLanguages"
|
|
65
|
+
:key="lang"
|
|
66
|
+
class="dx-language-search-item"
|
|
67
|
+
:class="{ 'is-selected': index === selectedIndex }"
|
|
68
|
+
@click="selectLanguage(lang)"
|
|
69
|
+
@mouseenter="selectedIndex = index"
|
|
70
|
+
>
|
|
71
|
+
{{ lang.toUpperCase() }}
|
|
72
|
+
</div>
|
|
73
|
+
<div v-if="filteredLanguages.length === 0" class="dx-language-search-empty">
|
|
74
|
+
No languages found
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</transition>
|
|
29
79
|
</div>
|
|
30
80
|
</template>
|
|
31
81
|
|
|
32
82
|
<script setup lang="ts">
|
|
33
|
-
import { computed, ref } from "vue";
|
|
83
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
|
84
|
+
|
|
85
|
+
// All supported languages (sorted alphabetically)
|
|
86
|
+
const ALL_LANGUAGES = [
|
|
87
|
+
"bash",
|
|
88
|
+
"c",
|
|
89
|
+
"cpp",
|
|
90
|
+
"css",
|
|
91
|
+
"dockerfile",
|
|
92
|
+
"go",
|
|
93
|
+
"graphql",
|
|
94
|
+
"html",
|
|
95
|
+
"java",
|
|
96
|
+
"javascript",
|
|
97
|
+
"json",
|
|
98
|
+
"kotlin",
|
|
99
|
+
"markdown",
|
|
100
|
+
"php",
|
|
101
|
+
"python",
|
|
102
|
+
"ruby",
|
|
103
|
+
"rust",
|
|
104
|
+
"scss",
|
|
105
|
+
"sql",
|
|
106
|
+
"swift",
|
|
107
|
+
"text",
|
|
108
|
+
"typescript",
|
|
109
|
+
"xml",
|
|
110
|
+
"yaml"
|
|
111
|
+
];
|
|
34
112
|
|
|
35
113
|
export interface LanguageBadgeProps {
|
|
36
114
|
format: string;
|
|
37
115
|
availableFormats?: string[];
|
|
38
116
|
toggleable?: boolean;
|
|
117
|
+
allowAnyLanguage?: boolean;
|
|
39
118
|
}
|
|
40
119
|
|
|
41
120
|
const props = withDefaults(defineProps<LanguageBadgeProps>(), {
|
|
42
121
|
availableFormats: () => [],
|
|
43
|
-
toggleable: true
|
|
122
|
+
toggleable: true,
|
|
123
|
+
allowAnyLanguage: false
|
|
44
124
|
});
|
|
45
125
|
|
|
46
|
-
defineEmits<{
|
|
126
|
+
const emit = defineEmits<{
|
|
47
127
|
change: [format: string];
|
|
48
128
|
}>();
|
|
49
129
|
|
|
50
130
|
const showOptions = ref(false);
|
|
131
|
+
const showSearchPanel = ref(false);
|
|
132
|
+
const searchQuery = ref("");
|
|
133
|
+
const searchInputRef = ref<HTMLInputElement | null>(null);
|
|
134
|
+
const selectedIndex = ref(0);
|
|
51
135
|
|
|
52
136
|
// Get formats other than the current one
|
|
53
137
|
const otherFormats = computed(() => {
|
|
54
138
|
return props.availableFormats.filter(f => f !== props.format);
|
|
55
139
|
});
|
|
140
|
+
|
|
141
|
+
// Filter languages based on search query
|
|
142
|
+
const filteredLanguages = computed(() => {
|
|
143
|
+
if (!searchQuery.value) {
|
|
144
|
+
return ALL_LANGUAGES;
|
|
145
|
+
}
|
|
146
|
+
const query = searchQuery.value.toLowerCase();
|
|
147
|
+
return ALL_LANGUAGES.filter(lang => lang.toLowerCase().includes(query));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Reset selectedIndex when search query changes
|
|
151
|
+
function onSearchQueryChange() {
|
|
152
|
+
selectedIndex.value = 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Keyboard navigation functions
|
|
156
|
+
function navigateDown() {
|
|
157
|
+
if (filteredLanguages.value.length === 0) return;
|
|
158
|
+
selectedIndex.value = (selectedIndex.value + 1) % filteredLanguages.value.length;
|
|
159
|
+
scrollSelectedIntoView();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function navigateUp() {
|
|
163
|
+
if (filteredLanguages.value.length === 0) return;
|
|
164
|
+
selectedIndex.value = selectedIndex.value === 0
|
|
165
|
+
? filteredLanguages.value.length - 1
|
|
166
|
+
: selectedIndex.value - 1;
|
|
167
|
+
scrollSelectedIntoView();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function selectCurrentItem() {
|
|
171
|
+
if (filteredLanguages.value.length > 0) {
|
|
172
|
+
selectLanguage(filteredLanguages.value[selectedIndex.value]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function scrollSelectedIntoView() {
|
|
177
|
+
nextTick(() => {
|
|
178
|
+
const list = document.querySelector(".dx-language-search-list");
|
|
179
|
+
const selected = list?.querySelector(".is-selected");
|
|
180
|
+
selected?.scrollIntoView({ block: "nearest" });
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function openSearchPanel() {
|
|
185
|
+
showSearchPanel.value = true;
|
|
186
|
+
searchQuery.value = "";
|
|
187
|
+
selectedIndex.value = 0;
|
|
188
|
+
nextTick(() => {
|
|
189
|
+
searchInputRef.value?.focus();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function closeSearchPanel() {
|
|
194
|
+
showSearchPanel.value = false;
|
|
195
|
+
searchQuery.value = "";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function selectLanguage(lang: string) {
|
|
199
|
+
emit("change", lang);
|
|
200
|
+
closeSearchPanel();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function onMouseLeave() {
|
|
204
|
+
showOptions.value = false;
|
|
205
|
+
// Don't close search panel on mouse leave - let click outside handle it
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handleClickOutside(event: MouseEvent) {
|
|
209
|
+
if (showSearchPanel.value) {
|
|
210
|
+
const target = event.target as HTMLElement;
|
|
211
|
+
const panel = document.querySelector(".dx-language-search-panel");
|
|
212
|
+
if (panel && !panel.contains(target)) {
|
|
213
|
+
closeSearchPanel();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onMounted(() => {
|
|
219
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
onBeforeUnmount(() => {
|
|
223
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Expose openSearchPanel for external callers (e.g., keyboard shortcut from CodeViewer)
|
|
227
|
+
defineExpose({
|
|
228
|
+
openSearchPanel
|
|
229
|
+
});
|
|
56
230
|
</script>
|
|
57
231
|
|
|
58
232
|
<style lang="scss">
|
|
@@ -94,6 +268,19 @@ const otherFormats = computed(() => {
|
|
|
94
268
|
}
|
|
95
269
|
}
|
|
96
270
|
|
|
271
|
+
// Search trigger inherits from .dx-language-option, only adds flex centering for the icon
|
|
272
|
+
.dx-language-search-trigger {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
justify-content: center;
|
|
276
|
+
padding: 2px 6px;
|
|
277
|
+
|
|
278
|
+
svg {
|
|
279
|
+
width: 12px;
|
|
280
|
+
height: 12px;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
97
284
|
.dx-language-badge {
|
|
98
285
|
padding: 2px 8px;
|
|
99
286
|
font-size: 0.7em;
|
|
@@ -108,6 +295,81 @@ const otherFormats = computed(() => {
|
|
|
108
295
|
}
|
|
109
296
|
}
|
|
110
297
|
|
|
298
|
+
.dx-language-search-panel {
|
|
299
|
+
position: absolute;
|
|
300
|
+
top: 100%;
|
|
301
|
+
right: 0;
|
|
302
|
+
margin-top: 4px;
|
|
303
|
+
background: rgba(0, 0, 0, 0.9);
|
|
304
|
+
border-radius: 6px;
|
|
305
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
306
|
+
min-width: 160px;
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.dx-language-search-input {
|
|
312
|
+
width: 100%;
|
|
313
|
+
padding: 8px 12px;
|
|
314
|
+
font-size: 0.8em;
|
|
315
|
+
background: rgba(255, 255, 255, 0.05);
|
|
316
|
+
border: none;
|
|
317
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
318
|
+
color: rgba(255, 255, 255, 0.9);
|
|
319
|
+
outline: none;
|
|
320
|
+
|
|
321
|
+
&::placeholder {
|
|
322
|
+
color: rgba(255, 255, 255, 0.4);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
&:focus {
|
|
326
|
+
background: rgba(255, 255, 255, 0.08);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.dx-language-search-list {
|
|
331
|
+
max-height: 200px;
|
|
332
|
+
overflow-y: auto;
|
|
333
|
+
|
|
334
|
+
&::-webkit-scrollbar {
|
|
335
|
+
width: 6px;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
&::-webkit-scrollbar-track {
|
|
339
|
+
background: rgba(0, 0, 0, 0.2);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
&::-webkit-scrollbar-thumb {
|
|
343
|
+
background: rgba(255, 255, 255, 0.2);
|
|
344
|
+
border-radius: 3px;
|
|
345
|
+
|
|
346
|
+
&:hover {
|
|
347
|
+
background: rgba(255, 255, 255, 0.3);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.dx-language-search-item {
|
|
353
|
+
padding: 6px 12px;
|
|
354
|
+
font-size: 0.75em;
|
|
355
|
+
color: rgba(255, 255, 255, 0.7);
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
transition: all 0.15s;
|
|
358
|
+
|
|
359
|
+
&:hover,
|
|
360
|
+
&.is-selected {
|
|
361
|
+
background: rgba(255, 255, 255, 0.15);
|
|
362
|
+
color: rgba(255, 255, 255, 0.95);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.dx-language-search-empty {
|
|
367
|
+
padding: 12px;
|
|
368
|
+
font-size: 0.75em;
|
|
369
|
+
color: rgba(255, 255, 255, 0.4);
|
|
370
|
+
text-align: center;
|
|
371
|
+
}
|
|
372
|
+
|
|
111
373
|
// Slide animation for options
|
|
112
374
|
.slide-left-enter-active,
|
|
113
375
|
.slide-left-leave-active {
|
|
@@ -119,4 +381,15 @@ const otherFormats = computed(() => {
|
|
|
119
381
|
opacity: 0;
|
|
120
382
|
transform: translateX(10px);
|
|
121
383
|
}
|
|
384
|
+
|
|
385
|
+
// Fade animation for search panel
|
|
386
|
+
.fade-enter-active,
|
|
387
|
+
.fade-leave-active {
|
|
388
|
+
transition: opacity 0.15s ease;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.fade-enter-from,
|
|
392
|
+
.fade-leave-to {
|
|
393
|
+
opacity: 0;
|
|
394
|
+
}
|
|
122
395
|
</style>
|