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:
Afiq 2025-08-07 19:38:28 +08:00
parent 07539e2344
commit 4b23da5239

View File

@ -89,29 +89,62 @@ const dropdownThemes = ref([
const value = ref(props.modelValue);
const extensions = ref([]);
if (props.language == "vue") {
// 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 = [
vue(),
oneDark,
languageExtension,
themeExtension,
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(),
];
}
};
// 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;
if (props.language == "vue") {
() => props.theme || editorTheme.value,
(newTheme) => {
if (newTheme) {
const themeExtension = getThemeExtension(newTheme);
const languageExtension = getLanguageExtension();
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,38 +358,70 @@ watch(
</script>
<template>
<div :class="props.class">
<div
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]"
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">
Theme:
<FormKit
v-model="editorTheme"
type="select"
placeholder="Select Themes"
:options="dropdownThemes"
:classes="{
input:
'!bg-[#282C34] !text-[#abb2bf] !border-[#abb2bf] hover:cursor-pointer h-6 w-[100px]',
inner: ' !rounded-none !mb-0',
outer: '!mb-0',
}"
/>
<div class="flex items-center gap-2">
Theme:
<FormKit
v-model="editorTheme"
type="select"
placeholder="Select Themes"
:options="dropdownThemes"
:classes="{
input:
'!bg-[#282C34] !text-[#abb2bf] !border-[#abb2bf] hover:cursor-pointer h-6 w-[100px]',
inner: ' !rounded-none !mb-0',
outer: '!mb-0',
}"
/>
</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 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>
<rs-button
@click="formatCurrentCode"
class="px-3 py-1 bg-blue-600 text-sm rounded"
>
<Icon name="vscode-icons:file-type-prettier" class="!w-5 !h-5 mr-2" />
Format Code (Shift + Alt + F)
</rs-button>
</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"
>
<span class="">Lines: {{ numberComma(totalLines) }}</span>
<span class="">Length: {{ numberComma(totalLength) }}</span>
<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>