Skip to content
Merged

fix #60

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions src/components/element/VmIde.vue
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ import DialogImportFile from './pages/ide/dialog/DialogImportFile';
import DialogBulkUpload from './pages/ide/dialog/DialogBulkUpload';
import DialogFileBrowser from './pages/ide/dialog/DialogFileBrowser';
import sessionManager from '../../utils/sessionManager';
import clipboardTracker from '../../utils/clipboardTracker';
import CsvViewer from './pages/ide/CsvViewer';
import MediaViewer from './pages/ide/editor/MediaViewer';
import SettingsModal from './pages/ide/SettingsModal';
Expand Down Expand Up @@ -2761,17 +2762,31 @@ export default {
document.execCommand('copy');
}
},
handlePaste() {
// Trigger paste in the active editor
async handlePaste() {
// Trigger paste in the active editor with clipboard validation
const activeEditor = this.getActiveCodeMirrorInstance();
if (activeEditor) {
navigator.clipboard.readText().then(text => {
activeEditor.replaceSelection(text);
}).catch(() => {
document.execCommand('paste');
});
try {
const text = await navigator.clipboard.readText();
// Validate paste for students - professors bypass all restrictions
const isAllowed = await clipboardTracker.validatePaste(text);
if (isAllowed) {
activeEditor.replaceSelection(text);
}
// If not allowed, toast notification is shown by clipboardTracker
} catch (err) {
// Clipboard API failed - only allow fallback for professors
if (clipboardTracker.isProfessor()) {
document.execCommand('paste');
} else {
console.log('[VmIde] Paste blocked for student (clipboard API failed)');
}
}
} else {
document.execCommand('paste');
// No active editor - only allow for professors
if (clipboardTracker.isProfessor()) {
document.execCommand('paste');
}
}
},
handleFind() {
Expand Down
123 changes: 121 additions & 2 deletions src/components/element/pages/ide/HybridConsole.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<textarea
v-model="userInput"
@keydown="handleKeyDown"
@paste="handleTextareaPaste"
ref="inputField"
class="user-input-field"
placeholder="Type your input and press Enter..."
Expand Down Expand Up @@ -148,6 +149,7 @@
<script>
import { ref, watch, nextTick, computed } from 'vue'
import { ElMessage } from 'element-plus'
import clipboardTracker from '../../../../utils/clipboardTracker'
// CodeMirror for REPL syntax highlighting
import Codemirror from 'codemirror-editor-vue3'
import CodeMirror from 'codemirror'
Expand Down Expand Up @@ -272,6 +274,41 @@ export default {
},
'Shift-Tab': (cm) => {
cm.indentSelection('subtract')
},
// Paste validation for students (Ctrl+V and Ctrl+Shift+V)
'Ctrl-V': async (cm) => {
await handleReplPaste(cm)
},
'Shift-Ctrl-V': async (cm) => {
await handleReplPaste(cm)
},
// Mac paste shortcuts
'Cmd-V': async (cm) => {
await handleReplPaste(cm)
},
'Shift-Cmd-V': async (cm) => {
await handleReplPaste(cm)
},
// Track copy operations from REPL (Ctrl+C and Cmd+C)
'Ctrl-C': (cm) => {
const selectedText = cm.getSelection()
if (selectedText) {
clipboardTracker.trackIDECopy(selectedText)
if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
navigator.clipboard.writeText(selectedText)
}
}
return false
},
'Cmd-C': (cm) => {
const selectedText = cm.getSelection()
if (selectedText) {
clipboardTracker.trackIDECopy(selectedText)
if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
navigator.clipboard.writeText(selectedText)
}
}
return false
}
}
})
Expand All @@ -289,6 +326,59 @@ export default {
})

// Methods

// Handle paste validation for REPL (students can only paste content copied from IDE)
const handleReplPaste = async (cm) => {
try {
if (navigator.clipboard && navigator.clipboard.readText && window.isSecureContext) {
const text = await navigator.clipboard.readText()
const isAllowed = await clipboardTracker.validatePaste(text)
if (isAllowed) {
cm.replaceSelection(text)
}
// If not allowed, toast notification is shown by clipboardTracker
}
} catch (err) {
// Clipboard API failed - only allow for professors
if (clipboardTracker.isProfessor()) {
// Can't read clipboard, let native behavior work
console.log('[HybridConsole] Clipboard read failed for professor, native paste allowed')
} else {
console.log('[HybridConsole] Paste blocked for student (clipboard API failed)')
}
}
}

// Handle paste for regular textarea input (script input mode)
const handleTextareaPaste = async (e) => {
// Check if student - professors can paste freely
if (!clipboardTracker.isProfessor()) {
e.preventDefault()
e.stopPropagation()

const clipboardData = e.clipboardData || window.clipboardData
const pastedText = clipboardData?.getData('text')

if (pastedText) {
const isAllowed = await clipboardTracker.validatePaste(pastedText)
if (isAllowed) {
// Insert at cursor position
const textarea = e.target
const start = textarea.selectionStart
const end = textarea.selectionEnd
const value = textarea.value
userInput.value = value.substring(0, start) + pastedText + value.substring(end)
// Set cursor position after pasted text
nextTick(() => {
textarea.selectionStart = textarea.selectionEnd = start + pastedText.length
})
}
// If not allowed, toast notification is shown by clipboardTracker
}
}
// Professors: allow default paste behavior
}

const addOutput = (content, type = 'text', className = '') => {
outputLines.value.push({
type,
Expand Down Expand Up @@ -355,19 +445,47 @@ export default {
})
}

// Handle DOM paste events on REPL CodeMirror (right-click paste)
const handleReplDOMPaste = async (e) => {
// Check if student - professors can paste freely
if (!clipboardTracker.isProfessor()) {
e.preventDefault()
e.stopPropagation()

const clipboardData = e.clipboardData || window.clipboardData
const pastedText = clipboardData?.getData('text')

if (pastedText) {
const isAllowed = await clipboardTracker.validatePaste(pastedText)
if (isAllowed && replEditor.value?.cminstance) {
replEditor.value.cminstance.replaceSelection(pastedText)
}
// If not allowed, toast notification is shown by clipboardTracker
}
}
// Professors: allow default paste behavior
}

const enterReplMode = () => {
isReplMode.value = true
waitingForInput.value = false
replPrompt.value = '>>> '
userInput.value = ''
historyIndex.value = -1

// Focus REPL editor with delay for proper initialization
nextTick(() => {
setTimeout(() => {
if (replEditor.value?.cminstance) {
replEditor.value.cminstance.refresh()
replEditor.value.cminstance.focus()

// Add DOM paste listener for right-click paste protection
const cmWrapper = replEditor.value.$el
if (cmWrapper && !cmWrapper._replPasteListenerAdded) {
cmWrapper.addEventListener('paste', handleReplDOMPaste, true)
cmWrapper._replPasteListenerAdded = true
}
} else if (inputField.value) {
inputField.value.focus()
}
Expand Down Expand Up @@ -684,7 +802,8 @@ export default {
stopProgram,
exportOutput,
downloadFigure,
openFigureInNewTab
openFigureInNewTab,
handleTextareaPaste
}
}
}
Expand Down
15 changes: 9 additions & 6 deletions src/components/element/pages/ide/TwoHeaderMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -485,8 +485,11 @@ export default {
);
};

// Ctrl+X - Cut (allow native cut in all text inputs)
if (e.ctrlKey && !e.shiftKey && e.key === 'x') {
// Helper: Check for Ctrl (Windows/Linux) or Cmd (Mac)
const isCopyPasteModifier = e.ctrlKey || e.metaKey;

// Ctrl+X or Cmd+X - Cut (allow native cut in all text inputs)
if (isCopyPasteModifier && !e.shiftKey && e.key === 'x') {
if (shouldAllowNativeCopyPaste()) {
// Let the input handle cut naturally
return;
Expand All @@ -497,8 +500,8 @@ export default {
this.cut();
return;
}
// Ctrl+C - Copy (allow native copy in all text inputs)
if (e.ctrlKey && !e.shiftKey && e.key === 'c') {
// Ctrl+C or Cmd+C - Copy (allow native copy in all text inputs)
if (isCopyPasteModifier && !e.shiftKey && e.key === 'c') {
if (shouldAllowNativeCopyPaste()) {
// Let the input handle copy naturally
return;
Expand All @@ -509,8 +512,8 @@ export default {
this.copy();
return;
}
// Ctrl+V or Ctrl+Shift+V - Paste (allow native paste in all text inputs)
if (e.ctrlKey && e.key === 'v') {
// Ctrl+V, Ctrl+Shift+V, Cmd+V, or Cmd+Shift+V - Paste (allow native paste in all text inputs)
if (isCopyPasteModifier && e.key === 'v') {
if (shouldAllowNativeCopyPaste()) {
// Let the input handle paste naturally
return;
Expand Down
80 changes: 80 additions & 0 deletions src/components/element/pages/ide/editor/CodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,54 @@ export default {
},
'Shift-Ctrl-V': async (cm) => {
await this.handlePasteOperation(cm);
},

// Mac shortcuts - Copy (Cmd+C)
'Cmd-C': (cm) => {
const selectedText = cm.getSelection();
if (selectedText) {
clipboardTracker.trackIDECopy(selectedText);
console.log('[CodeMirror] Mac Copy tracked:', selectedText.substring(0, 50));

if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
navigator.clipboard.writeText(selectedText).catch((error) => {
console.error('[CodeMirror] Mac clipboard API failed:', error);
this.copyTextFallback(selectedText, cm);
});
} else {
this.copyTextFallback(selectedText, cm);
}
}
return false;
},

// Mac shortcuts - Cut (Cmd+X)
'Cmd-X': (cm) => {
const selectedText = cm.getSelection();
if (selectedText) {
clipboardTracker.trackIDECopy(selectedText);
console.log('[CodeMirror] Mac Cut tracked:', selectedText.substring(0, 50));

if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
navigator.clipboard.writeText(selectedText).then(() => {
cm.replaceSelection('');
}).catch((error) => {
console.error('[CodeMirror] Mac clipboard cut failed:', error);
this.cutTextFallback(selectedText, cm);
});
} else {
this.cutTextFallback(selectedText, cm);
}
}
return false;
},

// Mac shortcuts - Paste (Cmd+V and Cmd+Shift+V)
'Cmd-V': async (cm) => {
await this.handlePasteOperation(cm);
},
'Shift-Cmd-V': async (cm) => {
await this.handlePasteOperation(cm);
}
},
}
Expand All @@ -287,6 +335,12 @@ export default {
setTimeout(() => {
this.$refs.codeEditor.cminstance.refresh();
}, 100);

// Add DOM paste event listener for right-click paste protection
const cmWrapper = this.$refs.codeEditor.$el;
if (cmWrapper) {
cmWrapper.addEventListener('paste', this.handleDOMPaste, true);
}
}
});

Expand Down Expand Up @@ -336,6 +390,11 @@ export default {
if (this.writeTimeout) {
clearTimeout(this.writeTimeout);
}

// Remove DOM paste event listener
if (this.$refs.codeEditor && this.$refs.codeEditor.$el) {
this.$refs.codeEditor.$el.removeEventListener('paste', this.handleDOMPaste, true);
}
},
computed: {
currentTheme() {
Expand Down Expand Up @@ -388,6 +447,27 @@ export default {
}
},
methods: {
// Handle DOM paste events (right-click paste, etc.)
async handleDOMPaste(e) {
// Always intercept paste events to validate
e.preventDefault();
e.stopPropagation();

const clipboardData = e.clipboardData || window.clipboardData;
const pastedText = clipboardData?.getData('text');

if (pastedText) {
const isAllowed = await clipboardTracker.validatePaste(pastedText);
if (isAllowed) {
const cm = this.$refs.codeEditor?.cminstance;
if (cm) {
cm.replaceSelection(pastedText);
}
}
// If not allowed, toast notification shown by clipboardTracker
}
},

applySavedSettings() {
// Apply saved font size
const savedFontSize = localStorage.getItem('fontSize') || localStorage.getItem('editorFontSize');
Expand Down