quasar-ui-danx 0.4.99 → 0.5.1
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 +17884 -12732
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +192 -118
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +11 -2
- package/scripts/publish.sh +76 -0
- package/src/components/Utility/Code/CodeViewer.vue +31 -14
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/MarkdownContent.vue +160 -6
- 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 +228 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -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/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 +779 -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 +369 -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 +1068 -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/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/index.ts +1 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -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 +412 -0
- package/src/helpers/formats/markdown/index.ts +92 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/helpers/formats/markdown/parseInline.ts +124 -0
- package/src/helpers/formats/markdown/render/index.ts +92 -0
- package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
- package/src/helpers/formats/markdown/render/renderList.ts +69 -0
- package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
- package/src/helpers/formats/markdown/state.ts +58 -0
- package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
- package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
- package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
- package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
- package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
- package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
- package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
- package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
- package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
- package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
- package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
- package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
- package/src/helpers/formats/markdown/types.ts +63 -0
- package/src/styles/danx.scss +1 -0
- package/src/styles/themes/danx/markdown.scss +96 -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/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/vitest.config.ts +19 -0
- package/src/helpers/formats/renderMarkdown.ts +0 -338
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quasar-ui-danx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"author": "Dan <dan@flytedesk.com>",
|
|
5
5
|
"description": "DanX Vue / Quasar component library",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
"build:bundle": "vite build",
|
|
16
16
|
"build": "yarn clean && yarn build:types && yarn build:bundle",
|
|
17
17
|
"preview": "vite preview",
|
|
18
|
-
"
|
|
18
|
+
"test": "vitest",
|
|
19
|
+
"test:run": "vitest run",
|
|
20
|
+
"test:coverage": "vitest run --coverage",
|
|
21
|
+
"publish:patch": "./scripts/publish.sh patch",
|
|
22
|
+
"publish:minor": "./scripts/publish.sh minor",
|
|
23
|
+
"publish:major": "./scripts/publish.sh major"
|
|
19
24
|
},
|
|
20
25
|
"repository": {
|
|
21
26
|
"type": "git",
|
|
@@ -32,6 +37,8 @@
|
|
|
32
37
|
"@typescript-eslint/eslint-plugin": "^7.6.0",
|
|
33
38
|
"@typescript-eslint/parser": "^7.6.0",
|
|
34
39
|
"@vitejs/plugin-vue": "^5.0.4",
|
|
40
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
41
|
+
"@vue/test-utils": "^2.4.6",
|
|
35
42
|
"autoprefixer": "^10.4.19",
|
|
36
43
|
"chalk": "^4.1.0",
|
|
37
44
|
"core-js": "^3.0.0",
|
|
@@ -40,6 +47,7 @@
|
|
|
40
47
|
"eslint-plugin-import": "^2.29.1",
|
|
41
48
|
"eslint-plugin-vue": "^9.24.1",
|
|
42
49
|
"fs-extra": "^8.1.0",
|
|
50
|
+
"jsdom": "^27.4.0",
|
|
43
51
|
"postcss": "^8.4.38",
|
|
44
52
|
"quasar": "^2.0.0",
|
|
45
53
|
"sass": "^1.33.0",
|
|
@@ -49,6 +57,7 @@
|
|
|
49
57
|
"vite": "^5.2.8",
|
|
50
58
|
"vite-plugin-dts": "^3.8.1",
|
|
51
59
|
"vite-svg-loader": "^5.1.0",
|
|
60
|
+
"vitest": "^4.0.16",
|
|
52
61
|
"vue": "^3.4.21",
|
|
53
62
|
"vue-eslint-parser": "^9.4.2",
|
|
54
63
|
"vue-router": "^4.3.2"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# NOTE: npm now requires granular tokens with 2FA, max 90-day lifetime
|
|
5
|
+
# Classic tokens have been revoked. If auth fails, you'll need to:
|
|
6
|
+
# 1. Run `npm login` to create a new granular token
|
|
7
|
+
# 2. Complete 2FA verification
|
|
8
|
+
# See: https://gh.io/all-npm-classic-tokens-revoked
|
|
9
|
+
|
|
10
|
+
# Colors for output
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
NC='\033[0m' # No Color
|
|
15
|
+
|
|
16
|
+
echo -e "${YELLOW}Checking npm authentication...${NC}"
|
|
17
|
+
|
|
18
|
+
# Check if logged in by trying to get current user
|
|
19
|
+
if ! npm whoami &>/dev/null; then
|
|
20
|
+
echo -e "${RED}Not logged in to npm or token expired.${NC}"
|
|
21
|
+
echo -e "${YELLOW}Please log in to npm:${NC}"
|
|
22
|
+
npm login
|
|
23
|
+
|
|
24
|
+
# Verify login succeeded
|
|
25
|
+
if ! npm whoami &>/dev/null; then
|
|
26
|
+
echo -e "${RED}Login failed. Aborting.${NC}"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
CURRENT_USER=$(npm whoami)
|
|
32
|
+
echo -e "${GREEN}Logged in as: ${CURRENT_USER}${NC}"
|
|
33
|
+
|
|
34
|
+
# Get the version bump type (default to patch)
|
|
35
|
+
BUMP_TYPE=${1:-patch}
|
|
36
|
+
|
|
37
|
+
if [[ ! "$BUMP_TYPE" =~ ^(patch|minor|major)$ ]]; then
|
|
38
|
+
echo -e "${RED}Invalid version bump type: ${BUMP_TYPE}${NC}"
|
|
39
|
+
echo "Usage: ./scripts/publish.sh [patch|minor|major]"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo -e "${YELLOW}Bumping version (${BUMP_TYPE})...${NC}"
|
|
44
|
+
|
|
45
|
+
# Build first to catch any errors before version bump
|
|
46
|
+
echo -e "${YELLOW}Building...${NC}"
|
|
47
|
+
yarn build
|
|
48
|
+
|
|
49
|
+
# Bump version (this updates package.json)
|
|
50
|
+
npm version $BUMP_TYPE --no-git-tag-version
|
|
51
|
+
|
|
52
|
+
# Get the new version
|
|
53
|
+
NEW_VERSION=$(node -p "require('./package.json').version")
|
|
54
|
+
echo -e "${GREEN}New version: ${NEW_VERSION}${NC}"
|
|
55
|
+
|
|
56
|
+
# Publish to npm
|
|
57
|
+
echo -e "${YELLOW}Publishing to npm...${NC}"
|
|
58
|
+
if npm publish; then
|
|
59
|
+
echo -e "${GREEN}Published successfully!${NC}"
|
|
60
|
+
|
|
61
|
+
# Git operations
|
|
62
|
+
echo -e "${YELLOW}Committing and tagging...${NC}"
|
|
63
|
+
cd ..
|
|
64
|
+
git add ui
|
|
65
|
+
git commit -m "v${NEW_VERSION}"
|
|
66
|
+
git tag "v${NEW_VERSION}"
|
|
67
|
+
git push
|
|
68
|
+
git push --tags
|
|
69
|
+
|
|
70
|
+
echo -e "${GREEN}Done! Published v${NEW_VERSION}${NC}"
|
|
71
|
+
else
|
|
72
|
+
echo -e "${RED}Publish failed!${NC}"
|
|
73
|
+
echo -e "${YELLOW}Rolling back version in package.json...${NC}"
|
|
74
|
+
git checkout package.json
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
@@ -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
|
/>
|
|
@@ -110,6 +117,7 @@ export interface CodeViewerProps {
|
|
|
110
117
|
collapsible?: boolean;
|
|
111
118
|
defaultCollapsed?: boolean;
|
|
112
119
|
defaultCodeFormat?: "json" | "yaml";
|
|
120
|
+
allowAnyLanguage?: boolean;
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
const props = withDefaults(defineProps<CodeViewerProps>(), {
|
|
@@ -127,6 +135,8 @@ const emit = defineEmits<{
|
|
|
127
135
|
"update:modelValue": [value: object | string | null];
|
|
128
136
|
"update:format": [format: CodeFormat];
|
|
129
137
|
"update:editable": [editable: boolean];
|
|
138
|
+
"exit": [];
|
|
139
|
+
"delete": [];
|
|
130
140
|
}>();
|
|
131
141
|
|
|
132
142
|
// Initialize composable with current props
|
|
@@ -138,6 +148,7 @@ const codeFormat = useCodeFormat({
|
|
|
138
148
|
// Local state
|
|
139
149
|
const currentFormat = ref<CodeFormat>(props.format);
|
|
140
150
|
const codeRef = ref<HTMLPreElement | null>(null);
|
|
151
|
+
const languageBadgeRef = ref<InstanceType<typeof LanguageBadge> | null>(null);
|
|
141
152
|
|
|
142
153
|
// Collapsed state (for collapsible mode)
|
|
143
154
|
const isCollapsed = ref(props.collapsible && props.defaultCollapsed);
|
|
@@ -154,9 +165,26 @@ function toggleCollapse() {
|
|
|
154
165
|
isCollapsed.value = !isCollapsed.value;
|
|
155
166
|
}
|
|
156
167
|
|
|
157
|
-
//
|
|
168
|
+
// Initialize editor composable
|
|
169
|
+
const editor = useCodeViewerEditor({
|
|
170
|
+
codeRef,
|
|
171
|
+
codeFormat,
|
|
172
|
+
currentFormat,
|
|
173
|
+
canEdit: toRef(props, "canEdit"),
|
|
174
|
+
editable: toRef(props, "editable"),
|
|
175
|
+
onEmitModelValue: (value) => emit("update:modelValue", value),
|
|
176
|
+
onEmitEditable: (editable) => emit("update:editable", editable),
|
|
177
|
+
onEmitFormat: (format) => onFormatChange(format),
|
|
178
|
+
onExit: () => emit("exit"),
|
|
179
|
+
onDelete: () => emit("delete"),
|
|
180
|
+
onOpenLanguageSearch: () => languageBadgeRef.value?.openSearchPanel()
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Sync composable format with current format and update editor content
|
|
158
184
|
watch(currentFormat, (newFormat) => {
|
|
159
185
|
codeFormat.setFormat(newFormat);
|
|
186
|
+
// Update editor content when format changes (needed for syntax re-highlighting)
|
|
187
|
+
editor.updateEditingContentOnFormatChange();
|
|
160
188
|
});
|
|
161
189
|
|
|
162
190
|
// Watch for external format changes
|
|
@@ -170,17 +198,6 @@ watch(() => props.modelValue, () => {
|
|
|
170
198
|
editor.syncEditingContentFromValue();
|
|
171
199
|
});
|
|
172
200
|
|
|
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
201
|
// Sync internal editable state with prop
|
|
185
202
|
watch(() => props.editable, (newValue) => {
|
|
186
203
|
editor.syncEditableFromProp(newValue);
|
|
@@ -198,7 +215,7 @@ const { collapsedPreview } = useCodeViewerCollapse({
|
|
|
198
215
|
function onFormatChange(newFormat: CodeFormat) {
|
|
199
216
|
currentFormat.value = newFormat;
|
|
200
217
|
emit("update:format", newFormat);
|
|
201
|
-
editor.updateEditingContentOnFormatChange()
|
|
218
|
+
// Note: editor.updateEditingContentOnFormatChange() is called by the currentFormat watcher
|
|
202
219
|
}
|
|
203
220
|
|
|
204
221
|
// 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<{
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
@click="$emit('toggle-edit')"
|
|
27
27
|
>
|
|
28
28
|
<EditIcon class="w-3.5 h-3.5" />
|
|
29
|
-
<QTooltip>{{ isEditing ? 'Exit edit mode' : 'Edit content' }}</QTooltip>
|
|
30
29
|
</QBtn>
|
|
31
30
|
</div>
|
|
32
31
|
</template>
|
|
33
32
|
|
|
34
33
|
<script setup lang="ts">
|
|
35
34
|
import { FaSolidPencil as EditIcon } from "danx-icon";
|
|
35
|
+
import { QBtn } from "quasar";
|
|
36
36
|
import { computed } from "vue";
|
|
37
37
|
import { ValidationError } from "../../../composables/useCodeFormat";
|
|
38
38
|
|
|
@@ -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>
|