Refactor RsCodeMirror Component for Enhanced Language and Theme Handling
- Introduced helper functions to manage language and theme extensions, improving code organization and readability. - Implemented dynamic initialization of extensions based on selected language and theme, ensuring accurate syntax highlighting. - Added fullscreen functionality with keyboard shortcuts for a better user experience, including visual feedback for fullscreen mode. - Enhanced template structure and styles for improved responsiveness and usability, particularly in fullscreen mode. - Updated event handling for keyboard shortcuts to streamline code formatting and fullscreen toggling. - Ensured proper cleanup of body overflow styles on component unmount to maintain layout integrity.
This commit is contained in:
parent
07539e2344
commit
4b23da5239
@ -89,29 +89,62 @@ const dropdownThemes = ref([
|
||||
|
||||
const value = ref(props.modelValue);
|
||||
const extensions = ref([]);
|
||||
if (props.language == "vue") {
|
||||
extensions.value = [
|
||||
vue(),
|
||||
oneDark,
|
||||
autocompletion(),
|
||||
indentUnit.of(" "),
|
||||
indentOnInput(),
|
||||
];
|
||||
} else if (props.language == "javascript") {
|
||||
extensions.value = [
|
||||
javascript(),
|
||||
oneDark,
|
||||
autocompletion(),
|
||||
indentUnit.of(" "),
|
||||
indentOnInput(),
|
||||
];
|
||||
} else if (props.language == "css") {
|
||||
extensions.value = [
|
||||
css(),
|
||||
oneDark,
|
||||
autocompletion(),
|
||||
];
|
||||
|
||||
// Helper function to get theme extension
|
||||
const getThemeExtension = (themeVal) => {
|
||||
switch (themeVal) {
|
||||
case "oneDark":
|
||||
return oneDark;
|
||||
case "amy":
|
||||
return amy;
|
||||
case "ayuLight":
|
||||
return ayuLight;
|
||||
case "barf":
|
||||
return barf;
|
||||
case "cobalt":
|
||||
return cobalt;
|
||||
case "dracula":
|
||||
return dracula;
|
||||
case "clouds":
|
||||
default:
|
||||
return clouds;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get language extension
|
||||
const getLanguageExtension = () => {
|
||||
switch (props.language) {
|
||||
case "vue":
|
||||
return vue();
|
||||
case "css":
|
||||
case "scss":
|
||||
return css();
|
||||
case "html":
|
||||
return css(); // Use CSS for HTML highlighting
|
||||
case "javascript":
|
||||
case "js":
|
||||
default:
|
||||
return javascript();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize extensions with proper theme
|
||||
const initializeExtensions = () => {
|
||||
const currentTheme = props.theme || editorTheme.value || "oneDark";
|
||||
const themeExtension = getThemeExtension(currentTheme);
|
||||
const languageExtension = getLanguageExtension();
|
||||
|
||||
extensions.value = [
|
||||
languageExtension,
|
||||
themeExtension,
|
||||
autocompletion(),
|
||||
indentUnit.of(" "),
|
||||
indentOnInput(),
|
||||
];
|
||||
};
|
||||
|
||||
// Initialize extensions on component creation
|
||||
initializeExtensions();
|
||||
|
||||
const totalLines = ref(0);
|
||||
const totalLength = ref(0);
|
||||
@ -124,35 +157,45 @@ const handleReady = (payload) => {
|
||||
totalLength.value = view.value.state.doc.length;
|
||||
};
|
||||
|
||||
// Watch for theme changes
|
||||
watch(
|
||||
() => editorTheme.value,
|
||||
(themeVal) => {
|
||||
const themeExtension =
|
||||
themeVal === "oneDark"
|
||||
? oneDark
|
||||
: themeVal === "amy"
|
||||
? amy
|
||||
: themeVal === "ayuLight"
|
||||
? ayuLight
|
||||
: themeVal === "barf"
|
||||
? barf
|
||||
: themeVal === "cobalt"
|
||||
? cobalt
|
||||
: themeVal === "dracula"
|
||||
? dracula
|
||||
: clouds;
|
||||
() => props.theme || editorTheme.value,
|
||||
(newTheme) => {
|
||||
if (newTheme) {
|
||||
const themeExtension = getThemeExtension(newTheme);
|
||||
const languageExtension = getLanguageExtension();
|
||||
|
||||
if (props.language == "vue") {
|
||||
extensions.value = [
|
||||
vue(),
|
||||
languageExtension,
|
||||
themeExtension,
|
||||
autocompletion(),
|
||||
indentUnit.of(" "),
|
||||
indentOnInput(),
|
||||
];
|
||||
} else {
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Watch for language changes
|
||||
watch(
|
||||
() => props.language,
|
||||
() => {
|
||||
initializeExtensions();
|
||||
}
|
||||
);
|
||||
|
||||
// Watch for editorTheme store changes
|
||||
watch(
|
||||
() => editorTheme.value,
|
||||
(newTheme) => {
|
||||
if (!props.theme && newTheme) {
|
||||
// Only update if no explicit theme prop is provided
|
||||
const themeExtension = getThemeExtension(newTheme);
|
||||
const languageExtension = getLanguageExtension();
|
||||
|
||||
extensions.value = [
|
||||
javascript(),
|
||||
languageExtension,
|
||||
themeExtension,
|
||||
autocompletion(),
|
||||
indentUnit.of(" "),
|
||||
@ -244,12 +287,49 @@ const formatCurrentCode = async () => {
|
||||
|
||||
const debouncedFormatCode = useDebounceFn(formatCurrentCode, 300);
|
||||
|
||||
// Fullscreen functionality
|
||||
const isFullscreen = ref(false);
|
||||
const fullscreenContainer = ref(null);
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
|
||||
if (isFullscreen.value) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
// Focus the editor after entering fullscreen
|
||||
nextTick(() => {
|
||||
if (view.value) {
|
||||
view.value.focus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
};
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (isFullscreen.value) {
|
||||
isFullscreen.value = false;
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
// Press Shift + Alt + F to format code
|
||||
if (e.shiftKey && e.altKey && e.key === "F") {
|
||||
e.preventDefault();
|
||||
debouncedFormatCode();
|
||||
}
|
||||
// Press F11 or Ctrl+Shift+F to toggle fullscreen
|
||||
if (e.key === "F11" || (e.ctrlKey && e.shiftKey && e.key === "F")) {
|
||||
e.preventDefault();
|
||||
toggleFullscreen();
|
||||
}
|
||||
// Press Escape to exit fullscreen
|
||||
if (e.key === "Escape" && isFullscreen.value) {
|
||||
e.preventDefault();
|
||||
exitFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@ -259,6 +339,10 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
// Ensure body overflow is reset
|
||||
if (isFullscreen.value) {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Add this watch effect after the value ref declaration
|
||||
@ -274,10 +358,20 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="props.class">
|
||||
<div
|
||||
class="flex justify-between items-center gap-2 p-2 bg-[#282C34] text-[#abb2bf]"
|
||||
ref="fullscreenContainer"
|
||||
:class="[
|
||||
props.class,
|
||||
{
|
||||
'fullscreen-editor': isFullscreen,
|
||||
'fixed inset-0 z-[9999] bg-[#282C34]': isFullscreen
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center gap-2 p-2 bg-[#282C34] text-[#abb2bf] border-b border-gray-600"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
Theme:
|
||||
<FormKit
|
||||
@ -293,19 +387,41 @@ watch(
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isFullscreen" class="text-sm text-gray-300 ml-4">
|
||||
<Icon name="material-symbols:info-outline" class="w-4 h-4 inline mr-1" />
|
||||
Press <kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded">Esc</kbd> or
|
||||
<kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded">F11</kbd> to exit fullscreen
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="toggleFullscreen"
|
||||
class="px-3 py-1 bg-gray-600 hover:bg-gray-500 text-sm rounded transition-colors flex items-center"
|
||||
:title="isFullscreen ? 'Exit Fullscreen (F11)' : 'Enter Fullscreen (F11)'"
|
||||
>
|
||||
<Icon
|
||||
:name="isFullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'"
|
||||
class="!w-4 !h-4 mr-2"
|
||||
/>
|
||||
{{ isFullscreen ? 'Exit Fullscreen' : 'Fullscreen' }}
|
||||
</button>
|
||||
<rs-button
|
||||
@click="formatCurrentCode"
|
||||
class="px-3 py-1 bg-blue-600 text-sm rounded"
|
||||
class="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-sm rounded transition-colors"
|
||||
>
|
||||
<Icon name="vscode-icons:file-type-prettier" class="!w-5 !h-5 mr-2" />
|
||||
Format Code (Shift + Alt + F)
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
<client-only>
|
||||
<CodeMirror
|
||||
v-model="value"
|
||||
placeholder="Code goes here..."
|
||||
:style="{ height: height }"
|
||||
:style="{
|
||||
height: isFullscreen ? 'calc(100vh - 120px)' : height,
|
||||
minHeight: isFullscreen ? 'calc(100vh - 120px)' : 'auto'
|
||||
}"
|
||||
:autofocus="true"
|
||||
:indent-with-tab="true"
|
||||
:tab-size="2"
|
||||
@ -319,12 +435,107 @@ watch(
|
||||
/>
|
||||
</client-only>
|
||||
<div
|
||||
class="footer flex justify-end items-center gap-2 p-2 bg-[#282C34] text-[#abb2bf]"
|
||||
class="footer flex justify-between items-center gap-2 p-2 bg-[#282C34] text-[#abb2bf] border-t border-gray-600"
|
||||
>
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<span v-if="isFullscreen" class="text-gray-300">
|
||||
<Icon name="material-symbols:keyboard" class="w-4 h-4 inline mr-1" />
|
||||
Shortcuts:
|
||||
<kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded mx-1">Ctrl+Shift+F</kbd> Format,
|
||||
<kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded mx-1">F11</kbd> Toggle Fullscreen
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="">Lines: {{ numberComma(totalLines) }}</span>
|
||||
<span class="">Length: {{ numberComma(totalLength) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Fullscreen editor styles */
|
||||
.fullscreen-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: #282C34;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.fullscreen-editor .footer,
|
||||
.fullscreen-editor .header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fullscreen-editor :deep(.cm-editor) {
|
||||
flex: 1;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.fullscreen-editor :deep(.cm-scroller) {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
.fullscreen-editor {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Keyboard shortcut styling */
|
||||
kbd {
|
||||
@apply inline-flex items-center px-2 py-1 bg-gray-700 text-gray-200 text-xs font-mono rounded shadow;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
/* Better button styling */
|
||||
button:focus {
|
||||
@apply outline-none ring-2 ring-blue-500 ring-offset-2 ring-offset-gray-800;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for fullscreen */
|
||||
@media (max-width: 768px) {
|
||||
.fullscreen-editor .footer,
|
||||
.fullscreen-editor .header {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.fullscreen-editor .footer span,
|
||||
.fullscreen-editor .header span {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.fullscreen-editor button {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for entering fullscreen */
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen-editor {
|
||||
animation: fadeInScale 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.fullscreen-editor :deep(.cm-focused) {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user