📝 מדריך מימוש: עורך קוד CodeMirror ב-WebApp
📋 סקירה כללית
מדריך זה מתאר כיצד להוסיף עורך קוד מתקדם מבוסס CodeMirror 6 לצד העורך הקיים (textarea פשוט) ב-WebApp של CodeBot. המטרה היא לתת למשתמשים אפשרות בחירה בין עורך פשוט לעורך מתקדם עם יכולות עריכה משופרות.
🎯 יעדי הפיצ'ר
- שמירת העורך הקיים: העורך הנוכחי (textarea) יישאר כברירת מחדל
- הוספת CodeMirror 6: עורך מודרני עם תמיכה מלאה בשפות תכנות
- מעבר חלק: כפתור החלפה בין העורכים בזמן אמת
- שמירת העדפה: זכירת בחירת העורך למשתמש
- תכונות מתקדמות: הדגשת תחביר, השלמה אוטומטית, מיני-מפה, ועוד
🌟 תכונות CodeMirror שיוטמעו
תכונות בסיס
- ✅ הדגשת תחביר לכל השפות הנתמכות
- ✅ מספרי שורות דינאמיים
- ✅ קיפול קוד (Code Folding)
- ✅ סוגריים מתאימים (Bracket Matching)
- ✅ הזחה אוטומטית
- ✅ בחירת מספר עמודות (Multi-cursor)
תכונות מתקדמות
- ✅ השלמה אוטומטית (Autocomplete)
- ✅ חיפוש והחלפה מתקדמים (Ctrl+F / Ctrl+H)
- ✅ מיני-מפה (Minimap)
- ✅ תמיכה ב-RTL/LTR
- ✅ ערכות נושא (Themes) - כהה/בהיר
- ✅ קיצורי מקלדת מותאמים אישית
🏗️ ארכיטקטורה
מבנה הקומפוננטה
webapp/
├── static/
│ ├── js/
│ │ ├── editor-manager.js # מנהל העורכים
│ │ ├── codemirror-setup.js # הגדרות CodeMirror
│ │ └── editor-switcher.js # לוגיקת המעבר
│ └── css/
│ └── codemirror-custom.css # עיצוב מותאם
├── templates/
│ ├── edit_file.html # עדכון לתמיכה בשני עורכים
│ └── upload.html # עדכון לתמיכה בשני עורכים
└── app.py # תמיכה בשמירת העדפות
📦 התקנה ותלויות
1. הוספת CDN לבסיס התבנית
ב-templates/base.html, בתוך ה-<head>:
<!-- CodeMirror 6 - טעינה מותנית -->
{% if use_codemirror %}
<!-- CSS של CodeMirror -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@6/dist/codemirror.min.css">
<!-- טעינת CodeMirror באמצעות ESM modules מ-CDN -->
<script type="module">
// טעינת הרכיבים הבסיסיים של CodeMirror
import { EditorState } from 'https://cdn.jsdelivr.net/npm/@codemirror/state@6/dist/index.js';
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from 'https://cdn.jsdelivr.net/npm/@codemirror/view@6/dist/index.js';
import { defaultKeymap, history, historyKeymap } from 'https://cdn.jsdelivr.net/npm/@codemirror/commands@6/dist/index.js';
import { bracketMatching, foldGutter, foldKeymap } from 'https://cdn.jsdelivr.net/npm/@codemirror/language@6/dist/index.js';
import { searchKeymap, highlightSelectionMatches } from 'https://cdn.jsdelivr.net/npm/@codemirror/search@6/dist/index.js';
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from 'https://cdn.jsdelivr.net/npm/@codemirror/autocomplete@6/dist/index.js';
// הגדרת basicSetup מותאמת אישית
const basicSetup = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap
])
];
// יבוא Compartment לעדכון דינמי של הגדרות
import { Compartment } from 'https://cdn.jsdelivr.net/npm/@codemirror/state@6/dist/index.js';
// יצירת compartments לשפה ונושא
const languageCompartment = new Compartment();
const themeCompartment = new Compartment();
// חשיפה גלובלית לשימוש בקוד
window.CodeMirror6 = {
EditorState,
EditorView,
basicSetup,
keymap,
lineNumbers,
bracketMatching,
autocompletion,
Compartment,
languageCompartment,
themeCompartment
};
</script>
<!-- אופציונלי: Import Maps לתמיכה בדפדפנים מודרניים -->
<script type="importmap">
{
"imports": {
"@codemirror/": "https://cdn.jsdelivr.net/npm/@codemirror/",
"codemirror": "https://cdn.jsdelivr.net/npm/codemirror@6/dist/index.js"
}
}
</script>
{% endif %}
2. אופציה חלופית - שימוש עם Bundler
אם אתה מעדיף להשתמש ב-bundler (Webpack/Vite/Rollup) במקום CDN:
# התקנת החבילות הנדרשות
npm install --save \
codemirror \
@codemirror/state \
@codemirror/view \
@codemirror/commands \
@codemirror/language \
@codemirror/search \
@codemirror/autocomplete \
@codemirror/lint \
@codemirror/theme-one-dark \
@codemirror/lang-python \
@codemirror/lang-javascript \
@codemirror/lang-html \
@codemirror/lang-css \
@codemirror/lang-markdown \
@codemirror/lang-sql \
@codemirror/lang-json
// editor-bundle.js - קובץ לבנייה עם bundler
import { EditorState } from '@codemirror/state';
import { EditorView, basicSetup } from 'codemirror';
import { python } from '@codemirror/lang-python';
import { javascript } from '@codemirror/lang-javascript';
import { html } from '@codemirror/lang-html';
import { oneDark } from '@codemirror/theme-one-dark';
// ייצוא גלובלי
window.CodeMirrorBundle = {
EditorState,
EditorView,
basicSetup,
languages: { python, javascript, html },
themes: { oneDark }
};
3. הערות חשובות לגבי טעינת מודולים
⚠️ חשוב: הקוד במדריך זה מיועד לעבודה עם CDN ישירות בדפדפן.
- השימוש ב-URLs מלאים של CDN מבטיח שהמודולים ייטענו בצורה תקינה
- אם אתה משתמש ב-bundler, החלף את ה-imports ל-bare specifiers רגילים
- Import Maps נתמכים רק בדפדפנים מודרניים (Chrome 89+, Firefox 108+, Safari 16.4+)
💻 מימוש Frontend
1. מנהל העורכים - static/js/editor-manager.js
class EditorManager {
constructor() {
this.currentEditor = 'simple'; // 'simple' או 'codemirror'
this.cmInstance = null;
this.textarea = null;
this.lastKnownValue = ''; // ערך אחרון ידוע לשחזור
this.loadingElement = null;
this.beforeUnloadHandler = null;
this.syncTimer = null;
this.loadPreference();
}
// טעינת העדפת משתמש
loadPreference() {
const saved = localStorage.getItem('preferredEditor');
if (saved && ['simple', 'codemirror'].includes(saved)) {
this.currentEditor = saved;
}
}
// שמירת העדפה
savePreference(editorType) {
localStorage.setItem('preferredEditor', editorType);
// שליחה לשרת (אופציונלי)
fetch('/api/user/preferences', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({editor_type: editorType})
}).catch(err => console.warn('Failed to save preference:', err));
}
// אתחול העורך
async initEditor(container, options = {}) {
const {
language = 'python',
theme = 'dark',
value = '',
readOnly = false
} = options;
this.textarea = container.querySelector('textarea[name="code"]');
if (this.currentEditor === 'codemirror') {
await this.initCodeMirror(container, options);
} else {
this.initSimpleEditor(container, options);
}
this.addSwitcherButton(container);
}
// אתחול עורך פשוט
initSimpleEditor(container, options) {
if (this.cmInstance) {
// העתקת התוכן מ-CodeMirror
this.textarea.value = this.cmInstance.state.doc.toString();
this.cmInstance.destroy();
this.cmInstance = null;
}
this.textarea.style.display = 'block';
container.querySelector('.cm-editor')?.remove();
}
// אתחול CodeMirror
async initCodeMirror(container, options) {
try {
// הצגת מצב טעינה
this.showLoadingState(container);
// הסתרת textarea
this.textarea.style.display = 'none';
// יצירת container ל-CodeMirror
const cmContainer = document.createElement('div');
cmContainer.className = 'codemirror-container';
this.textarea.parentNode.insertBefore(cmContainer, this.textarea.nextSibling);
// טעינת CodeMirror
if (!window.CodeMirror6) {
await this.loadCodeMirror();
}
const { EditorState, EditorView, basicSetup, languageCompartment, themeCompartment } = window.CodeMirror6;
const langSupport = await this.getLanguageSupport(options.language);
const themeExtension = await this.getTheme(options.theme);
// יצירת debounced sync function
const debouncedSync = this.debounce((value) => {
this.textarea.value = value;
this.textarea.dispatchEvent(new Event('input', {bubbles: true}));
}, 100);
// הגדרת העורך עם compartments לעדכון דינמי
const state = EditorState.create({
doc: this.textarea.value || options.value,
extensions: [
basicSetup,
languageCompartment.of(langSupport), // שפה ב-compartment
themeCompartment.of(themeExtension), // נושא ב-compartment
EditorView.lineWrapping,
EditorView.updateListener.of(update => {
if (update.docChanged) {
// סנכרון מותאם ביצועים עם textarea
debouncedSync(update.state.doc.toString());
}
}),
// הגדרות נוספות
this.getCustomExtensions(options)
]
});
// יצירת העורך
this.cmInstance = new EditorView({
state,
parent: cmContainer
});
// הוספת event listener לניקוי
this.beforeUnloadHandler = () => this.destroy();
window.addEventListener('beforeunload', this.beforeUnloadHandler);
} catch (error) {
console.error('CodeMirror initialization failed:', error);
// fallback לעורך פשוט
this.currentEditor = 'simple';
this.initSimpleEditor(container, options);
this.showErrorNotification('העורך המתקדם לא זמין כרגע, עובר לעורך פשוט');
} finally {
this.hideLoadingState(container);
}
}
// הוספת כפתור מעבר
addSwitcherButton(container) {
if (container.querySelector('.editor-switcher')) return;
const switcher = document.createElement('div');
switcher.className = 'editor-switcher';
switcher.innerHTML = `
<button type="button" class="btn-switch-editor" title="החלף עורך">
<i class="fas fa-exchange-alt"></i>
<span>${this.currentEditor === 'simple' ? 'עורך מתקדם' : 'עורך פשוט'}</span>
</button>
<div class="editor-info">
${this.currentEditor === 'codemirror' ?
'<span><i class="fas fa-keyboard"></i> Ctrl+F לחיפוש | Ctrl+Space להשלמה</span>' :
'<span><i class="fas fa-info-circle"></i> עורך טקסט בסיסי</span>'
}
</div>
`;
const label = container.querySelector('label:has(+ textarea[name="code"])') ||
container.querySelector('label');
if (label) {
label.parentNode.insertBefore(switcher, label.nextSibling);
}
// אירוע לחיצה
switcher.querySelector('.btn-switch-editor').addEventListener('click', () => {
this.toggleEditor(container);
});
}
// החלפת עורך
async toggleEditor(container) {
// הצגת loading spinner
this.showLoadingState(container);
// שמירת המצב הנוכחי לפני השינוי
const previousEditor = this.currentEditor;
const previousValue = this.getValue();
const previousInstance = this.cmInstance;
try {
const newEditor = this.currentEditor === 'simple' ? 'codemirror' : 'simple';
// ולידציה של התוכן לפני המעבר
if (!this.validateCodeContent(previousValue)) {
throw new Error('תוכן הקוד מכיל אלמנטים לא בטוחים');
}
// שינוי זמני למצב החדש
this.currentEditor = newEditor;
// ניסיון אתחול העורך החדש
await this.initEditor(container, {
value: previousValue,
language: this.detectLanguage(previousValue),
theme: this.getThemePreference()
});
// רק אם הצלחנו - שומרים את ההעדפה
this.savePreference(newEditor);
console.log(`Successfully switched to ${newEditor} editor`);
} catch (error) {
console.error('Editor toggle failed:', error);
// שחזור למצב הקודם
this.currentEditor = previousEditor;
this.cmInstance = previousInstance;
// ניסיון לשחזר את העורך הקודם
try {
if (previousEditor === 'simple') {
this.initSimpleEditor(container, { value: previousValue });
} else if (previousInstance) {
// אם היה CodeMirror, נשתמש ב-instance השמור
this.textarea.style.display = 'none';
}
} catch (rollbackError) {
console.error('Failed to rollback editor:', rollbackError);
// כ-fallback אחרון, מציגים את ה-textarea
this.textarea.style.display = 'block';
this.textarea.value = previousValue;
}
this.showErrorNotification(error.message || 'שגיאה במעבר בין עורכים');
} finally {
this.hideLoadingState(container);
}
}
// בדיקת תקינות העורך
isEditorValid() {
if (this.currentEditor === 'codemirror') {
return this.cmInstance && this.cmInstance.state && !this.cmInstance.destroyed;
}
return this.textarea && this.textarea.parentNode;
}
// קבלת הערך הנוכחי
getValue() {
// בדיקת תקינות לפני גישה
if (!this.isEditorValid()) {
console.warn('Editor not valid, returning cached value');
return this.lastKnownValue || '';
}
let value;
if (this.cmInstance && !this.cmInstance.destroyed) {
value = this.cmInstance.state.doc.toString();
} else {
value = this.textarea.value;
}
// שמירת ערך אחרון ידוע
this.lastKnownValue = value;
return value;
}
// הגדרת ערך
setValue(value) {
// בדיקת תקינות לפני עדכון
if (!this.isEditorValid()) {
console.error('Cannot set value - editor not valid');
this.lastKnownValue = value;
return false;
}
try {
if (this.cmInstance && !this.cmInstance.destroyed) {
this.cmInstance.dispatch({
changes: {from: 0, to: this.cmInstance.state.doc.length, insert: value}
});
} else {
this.textarea.value = value;
}
this.lastKnownValue = value;
return true;
} catch (error) {
console.error('Failed to set editor value:', error);
this.lastKnownValue = value;
return false;
}
}
// זיהוי שפה
detectLanguage(code) {
// זיהוי בסיסי לפי תוכן
if (code.includes('def ') || code.includes('import ')) return 'python';
if (code.includes('function ') || code.includes('const ')) return 'javascript';
if (code.includes('<html') || code.includes('<div')) return 'html';
if (code.includes('SELECT ') || code.includes('CREATE TABLE')) return 'sql';
return 'text';
}
// טעינת תמיכת שפה - עם CDN URLs מלאים
async getLanguageSupport(lang) {
// מיפוי שפות לרשימת CDN-ים לפי סדר עדיפות
const langMap = {
'python': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-python@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-python@6/dist/index.js'
],
'javascript': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-javascript@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-javascript@6/dist/index.js'
],
'html': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-html@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-html@6/dist/index.js'
],
'css': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-css@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-css@6/dist/index.js'
],
'sql': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-sql@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-sql@6/dist/index.js'
],
'json': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-json@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-json@6/dist/index.js'
],
'markdown': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-markdown@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-markdown@6/dist/index.js'
],
'xml': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-xml@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-xml@6/dist/index.js'
],
'java': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-java@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-java@6/dist/index.js'
],
'cpp': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-cpp@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-cpp@6/dist/index.js'
],
'rust': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-rust@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-rust@6/dist/index.js'
],
'php': [
'https://cdn.jsdelivr.net/npm/@codemirror/lang-php@6/dist/index.js',
'https://unpkg.com/@codemirror/lang-php@6/dist/index.js'
]
};
const urls = langMap[lang];
if (!urls) return [];
try {
const module = await this.importWithFallback(urls);
switch (lang) {
case 'python': return module.python();
case 'javascript': return module.javascript();
case 'html': return module.html();
case 'css': return module.css();
case 'sql': return module.sql();
case 'json': return module.json();
case 'markdown': return module.markdown();
case 'xml': return module.xml();
case 'java': return module.java();
case 'cpp': return module.cpp();
case 'rust': return module.rust();
case 'php': return module.php();
default: return [];
}
} catch (e) {
console.warn(`Language ${lang} failed to load from all CDNs:`, e);
return [];
}
}
// קבלת ערכת נושא - עם CDN URL מלא
async getTheme(themeName) {
if (themeName === 'dark') {
try {
const module = await this.importWithFallback([
'https://cdn.jsdelivr.net/npm/@codemirror/theme-one-dark@6/dist/index.js',
'https://unpkg.com/@codemirror/theme-one-dark@6/dist/index.js'
]);
return module.oneDark;
} catch (e) {
console.warn('Dark theme failed to load from all CDNs:', e);
}
}
return []; // ברירת מחדל - בהיר
}
// הרחבות מותאמות
getCustomExtensions(options) {
const extensions = [];
// תמיכה ב-RTL לטקסט עברי/ערבי
if (this.detectRTL(options.value)) {
extensions.push(EditorView.baseTheme({
".cm-line": { direction: "rtl", textAlign: "right" }
}));
}
// הגבלת גודל קובץ
if (options.maxSize) {
extensions.push(EditorState.changeFilter.of((tr) => {
return tr.newDoc.length <= options.maxSize;
}));
}
return extensions;
}
// זיהוי RTL
detectRTL(text) {
const rtlChars = /[\u0590-\u05FF\u0600-\u06FF]/;
return rtlChars.test(text?.slice(0, 100) || '');
}
// טעינת CodeMirror (אם לא נטען)
async loadCodeMirror() {
// יש להטמיע את הטעינה הדינמית כאן
// או להשתמש ב-dynamic imports
console.log('Loading CodeMirror...');
}
// העדפת ערכת נושא
getThemePreference() {
return localStorage.getItem('editorTheme') || 'dark';
}
// עדכון שפת העורך בזמן אמת
async updateLanguage(lang) {
if (!this.cmInstance) return;
try {
const { languageCompartment } = window.CodeMirror6;
const langSupport = await this.getLanguageSupport(lang);
// עדכון השפה באמצעות compartment reconfiguration
this.cmInstance.dispatch({
effects: languageCompartment.reconfigure(langSupport)
});
console.log(`Language updated to: ${lang}`);
} catch (error) {
console.error('Failed to update language:', error);
this.showErrorNotification('שגיאה בעדכון הדגשת התחביר');
}
}
// עדכון נושא העורך בזמן אמת
async updateTheme(themeName) {
if (!this.cmInstance) return;
try {
const { themeCompartment } = window.CodeMirror6;
const themeExtension = await this.getTheme(themeName);
// עדכון הנושא באמצעות compartment reconfiguration
this.cmInstance.dispatch({
effects: themeCompartment.reconfigure(themeExtension)
});
localStorage.setItem('editorTheme', themeName);
console.log(`Theme updated to: ${themeName}`);
} catch (error) {
console.error('Failed to update theme:', error);
}
}
// *** פונקציות עזר חשובות לביצועים ואבטחה ***
// Debounce utility לביצועים
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// טעינה עם Fallback ל־CDN חלופי
async importWithFallback(urls) {
for (const url of urls) {
try {
return await import(url);
} catch (_) {
// ננסה את הבא
}
}
throw new Error('CDN load failed');
}
// ולידציה של תוכן הקוד לאבטחה
validateCodeContent(code) {
// מניעת הזרקת קוד מסוכן
const dangerousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/on\w+\s*=/gi
];
for (const pattern of dangerousPatterns) {
if (pattern.test(code)) {
console.warn('Dangerous pattern detected in code:', pattern);
return false;
}
}
return true;
}
// ניהול זיכרון - ניקוי משאבים
destroy() {
// ניקוי CodeMirror instance
if (this.cmInstance) {
this.cmInstance.destroy();
this.cmInstance = null;
}
// ניקוי event listeners
if (this.beforeUnloadHandler) {
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
this.beforeUnloadHandler = null;
}
// ניקוי timers
if (this.syncTimer) {
clearTimeout(this.syncTimer);
this.syncTimer = null;
}
// ניקוי DOM references
this.textarea = null;
this.loadingElement = null;
}
// הצגת מצב טעינה
showLoadingState(container) {
if (this.loadingElement) return;
this.loadingElement = document.createElement('div');
this.loadingElement.className = 'editor-loading';
this.loadingElement.innerHTML = `
<div class="spinner">
<i class="fas fa-spinner fa-spin"></i>
<span>טוען עורך...</span>
</div>
`;
container.classList.add('editor-transitioning');
container.appendChild(this.loadingElement);
}
// הסתרת מצב טעינה
hideLoadingState(container) {
if (this.loadingElement) {
this.loadingElement.remove();
this.loadingElement = null;
}
container.classList.remove('editor-transitioning');
}
// הצגת הודעת שגיאה
showErrorNotification(message) {
const notification = document.createElement('div');
notification.className = 'editor-notification error';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('fade-out');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// טיפול כללי בשגיאות (לשימושים אחרים)
handleError(error, context = '') {
console.error(`Editor error ${context}:`, error);
this.showErrorNotification(error.message || 'שגיאה בעורך');
// לוגינג לשרת אם צריך
if (this.errorTracker) {
this.errorTracker.logError({
type: 'editorError',
context,
message: error.message,
stack: error.stack
});
}
}
}
// יצירת instance גלובלי
window.editorManager = new EditorManager();
2. עדכון templates/edit_file.html
{% extends "base.html" %}
{% block title %}עריכת {{ file.file_name }} - Code Keeper Bot{% endblock %}
{% block head %}
<style>
/* סגנון לעורך CodeMirror */
.codemirror-container {
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.2);
background: rgba(255,255,255,0.1);
overflow: hidden;
}
.cm-editor {
min-height: 400px;
max-height: 600px;
font-family: 'Fira Code', 'Consolas', monospace;
}
.editor-switcher {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0.5rem 0;
padding: 0.5rem;
background: rgba(255,255,255,0.05);
border-radius: 8px;
}
.btn-switch-editor {
padding: 0.5rem 1rem;
background: rgba(100,100,255,0.2);
border: 1px solid rgba(100,100,255,0.5);
border-radius: 6px;
color: white;
cursor: pointer;
transition: all 0.3s;
}
.btn-switch-editor:hover {
background: rgba(100,100,255,0.3);
transform: translateY(-1px);
}
.editor-info {
font-size: 0.85rem;
opacity: 0.8;
}
/* אנימציית מעבר */
.editor-transitioning {
opacity: 0.5;
pointer-events: none;
}
</style>
{% endblock %}
{% block content %}
<h1 class="page-title">עריכת קובץ</h1>
<div class="glass-card">
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
<form method="post" style="display: grid; gap: 1rem;">
<div>
<label>שם קובץ</label>
<input type="text" name="file_name" value="{{ file.file_name }}"
style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;">
</div>
<div>
<label>שפה</label>
<select name="language" id="languageSelect"
style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;">
<option value="{{ file.language }}">— {{ file.language }} (נוכחית) —</option>
<option value="text">זהה לפי סיומת/תוכן</option>
{% for lang in languages %}
<option value="{{ lang }}">{{ lang }}</option>
{% endfor %}
</select>
</div>
<div>
<label>תיאור</label>
<input type="text" name="description" value="{{ file.description }}"
style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;">
</div>
<div>
<label>תגיות</label>
<input type="text" name="tags" value="{{ file.tags | join(', ') }}"
placeholder="#utils, #repo:owner/name"
style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;">
</div>
<div id="editorContainer">
<label>קוד</label>
<!-- העורך יוכנס כאן דינמית -->
<textarea name="code" rows="18"
style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white; font-family: 'Fira Code', 'Consolas', monospace;">{{ code_value }}</textarea>
</div>
<div style="display: flex; gap: 1rem;">
<button type="submit" class="btn btn-primary btn-icon">
<i class="fas fa-save"></i> שמור גרסה חדשה
</button>
<a href="/file/{{ file.id }}" class="btn btn-secondary btn-icon">
<i class="fas fa-times"></i> ביטול
</a>
</div>
</form>
</div>
<script type="module">
// אתחול עורך הקוד
document.addEventListener('DOMContentLoaded', async () => {
const container = document.getElementById('editorContainer');
const languageSelect = document.getElementById('languageSelect');
// אתחול העורך
await window.editorManager.initEditor(container, {
language: '{{ file.language }}',
value: {{ code_value | tojson }},
theme: 'dark'
});
// עדכון שפה בעת שינוי
languageSelect.addEventListener('change', async (e) => {
const lang = e.target.value;
// שימוש בפונקציה המובנית לעדכון שפה
await window.editorManager.updateLanguage(lang);
});
// אופציונלי: הוספת מחליף נושא
const themeToggle = document.createElement('button');
themeToggle.className = 'theme-toggle';
themeToggle.innerHTML = '<i class="fas fa-moon"></i>';
themeToggle.title = 'החלף נושא';
themeToggle.onclick = async () => {
const currentTheme = window.editorManager.getThemePreference();
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
await window.editorManager.updateTheme(newTheme);
themeToggle.innerHTML = newTheme === 'dark' ? '<i class="fas fa-moon"></i>' : '<i class="fas fa-sun"></i>';
};
// הוספת כפתור הנושא לצד מחליף העורך (אם קיים)
const switcher = document.querySelector('.editor-switcher');
if (switcher) {
switcher.appendChild(themeToggle);
}
});
</script>
{% endblock %}
3. עדכון templates/upload.html באופן דומה
<!-- אותן תוספות כמו ב-edit_file.html -->
4. Auto‑Save מקומי (ללא תלות בשרת)
שומר טיוטה מקומית כל כמה שניות ומציע שחזור בביקור הבא. המפתח ממוספר לפי כתובת העמוד כדי למנוע דריסות בין קבצים שונים.
<script>
(function(){
const KEY = `autosave:${location.pathname}`;
const ta = () => document.querySelector('textarea[name="code"]');
const getValue = () => (window.editorManager?.getValue?.() || ta()?.value || '');
function safeGet(){ try { return localStorage.getItem(KEY); } catch(_) { return null; } }
function safeSet(v){ try { localStorage.setItem(KEY, v); } catch(_) {} }
// שמירה מחזורית עם throttle עדין
let last = 0;
setInterval(() => {
const now = Date.now();
if (now - last < 1000) return;
last = now;
const val = getValue();
if (val) safeSet(val);
}, 5000);
// שחזור טיוטה אם העורך ריק
window.addEventListener('DOMContentLoaded', () => {
const draft = safeGet();
const textarea = ta();
if (draft && textarea && !textarea.value) textarea.value = draft;
});
})();
</script>
5. התאמת גובה העורך במובייל (visualViewport)
מונע דחיפת העורך מתחת למקלדת וירטואלית. עובד גם עם CodeMirror וגם עם textarea.
<script>
(function(){
const target = () => document.querySelector('.cm-editor') || document.querySelector('textarea[name="code"]');
function adjust(){
const vp = window.visualViewport;
const h = (vp && vp.height) || window.innerHeight;
const top = (vp && vp.offsetTop) || 0;
const reserved = 140; // ריווח לניווט/כותרת
const el = target();
if (!el) return;
el.style.maxHeight = Math.max(200, h - reserved - top) + 'px';
}
(window.visualViewport || window).addEventListener('resize', adjust);
window.addEventListener('orientationchange', () => setTimeout(adjust, 200));
window.addEventListener('focusin', adjust);
window.addEventListener('DOMContentLoaded', adjust);
})();
</script>
6. טעינת CDN עם Fallback
הוספנו מתודת importWithFallback ודאגנו ש־getLanguageSupport ו־getTheme ינסו קודם jsDelivr ואז unpkg. כך טעינת חבילות השפה/הנושא תישאר יציבה גם ברשת לא אידיאלית.
🔧 מימוש Backend
1. שמירת העדפות משתמש
ב-app.py, הוסף endpoint:
@app.route('/api/user/preferences', methods=['POST'])
def update_user_preferences():
"""עדכון העדפות משתמש"""
if not session.get('user_id'):
return jsonify({'error': 'Not authenticated'}), 401
try:
data = request.json
editor_type = data.get('editor_type')
if editor_type not in ['simple', 'codemirror']:
return jsonify({'error': 'Invalid editor type'}), 400
# שמירה ב-session או DB
session['preferred_editor'] = editor_type
# אופציונלי: שמירה ב-DB
users_collection.update_one(
{'user_id': session['user_id']},
{'$set': {'preferences.editor': editor_type}},
upsert=True
)
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
2. העברת העדפה לתבניות
@app.route('/edit/<file_id>')
def edit_file(file_id):
# ... קוד קיים ...
# קבלת העדפת עורך
preferred_editor = session.get('preferred_editor', 'simple')
return render_template('edit_file.html',
file=file_data,
code_value=code,
languages=SUPPORTED_LANGUAGES,
use_codemirror=(preferred_editor == 'codemirror'))
🎨 עיצוב CSS מותאם
צור static/css/codemirror-custom.css:
/* Loading States */
.editor-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(5px);
z-index: 1000;
border-radius: 10px;
}
.editor-loading .spinner {
text-align: center;
color: white;
}
.editor-loading .spinner i {
font-size: 2rem;
margin-bottom: 0.5rem;
display: block;
}
.editor-transitioning {
position: relative;
pointer-events: none;
opacity: 0.7;
}
/* Notifications */
.editor-notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
background: rgba(40,40,50,0.95);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
color: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
z-index: 10000;
animation: slideIn 0.3s ease;
}
.editor-notification.error {
border-color: #ff4444;
background: rgba(80,20,20,0.95);
}
.editor-notification.fade-out {
animation: fadeOut 0.3s ease;
opacity: 0;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeOut {
to {
opacity: 0;
transform: translateX(100%);
}
}
/* התאמות לעורך CodeMirror */
.cm-editor {
direction: ltr;
text-align: left;
}
.cm-editor.cm-rtl {
direction: rtl;
text-align: right;
}
/* התאמה לנושא הכהה של האתר */
.cm-editor.cm-focused {
outline: 2px solid rgba(100,100,255,0.5);
}
.cm-selectionBackground {
background-color: rgba(100,100,255,0.3) !important;
}
/* מספרי שורות */
.cm-gutters {
background: rgba(0,0,0,0.3);
border-right: 1px solid rgba(255,255,255,0.1);
}
.cm-activeLineGutter {
background: rgba(100,100,255,0.2);
}
/* חיפוש והדגשה */
.cm-searchMatch {
background-color: rgba(255,200,0,0.3);
border: 1px solid rgba(255,200,0,0.8);
}
.cm-searchMatch.cm-searchMatch-selected {
background-color: rgba(255,150,0,0.5);
}
/* השלמה אוטומטית */
.cm-tooltip.cm-autocomplete {
background: rgba(30,30,40,0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(100,100,255,0.3);
border-radius: 8px;
}
.cm-tooltip.cm-autocomplete > ul > li[aria-selected] {
background: rgba(100,100,255,0.2);
}
/* מיני-מפה */
.cm-minimap {
position: fixed;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 100px;
background: rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
padding: 5px;
}
/* אנימציות */
.cm-editor {
transition: outline 0.3s ease;
}
@keyframes highlight-flash {
0% { background: rgba(255,255,100,0.5); }
100% { background: transparent; }
}
.cm-line.flash {
animation: highlight-flash 0.5s ease;
}
/* רספונסיביות */
@media (max-width: 768px) {
.cm-editor {
font-size: 14px;
}
.cm-minimap {
display: none;
}
.editor-switcher {
flex-direction: column;
gap: 0.5rem;
}
}
⚡ ניטור ביצועים ואופטימיזציה
Performance Monitoring
class PerformanceMonitor {
constructor() {
this.metrics = {
editorLoadTime: 0,
switchTime: 0,
memoryUsage: 0,
renderTime: 0
};
}
// מדידת זמן טעינת העורך
measureEditorLoad(startTime) {
this.metrics.editorLoadTime = performance.now() - startTime;
console.log(`Editor loaded in ${this.metrics.editorLoadTime}ms`);
// שליחה לאנליטיקס
if (window.gtag) {
gtag('event', 'timing_complete', {
name: 'editor_load',
value: Math.round(this.metrics.editorLoadTime)
});
}
}
// מדידת זיכרון
checkMemoryUsage() {
if (performance.memory) {
this.metrics.memoryUsage = performance.memory.usedJSHeapSize / 1048576;
// התראה על שימוש גבוה בזיכרון
if (this.metrics.memoryUsage > 100) { // 100MB
console.warn(`High memory usage: ${this.metrics.memoryUsage.toFixed(2)}MB`);
this.suggestOptimization();
}
}
}
// הצעות אופטימיזציה
suggestOptimization() {
console.log('Performance tip: Consider clearing undo history or reducing file size');
}
// דיווח מטריקות
reportMetrics() {
return {
...this.metrics,
timestamp: Date.now(),
userAgent: navigator.userAgent
};
}
}
// שילוב ב-EditorManager
class EditorManager {
constructor() {
// ... קוד קיים ...
this.perfMonitor = new PerformanceMonitor();
}
async initCodeMirror(container, options) {
const startTime = performance.now();
try {
// ... קוד אתחול ...
// מדידת ביצועים
this.perfMonitor.measureEditorLoad(startTime);
this.perfMonitor.checkMemoryUsage();
} catch (error) {
// ... טיפול בשגיאות ...
}
}
}
אופטימיזציות מומלצות
- Lazy Loading - טען שפות רק בעת הצורך:
const langLoaders = {
python: () => import('@codemirror/lang-python'),
javascript: () => import('@codemirror/lang-javascript'),
// ... שאר השפות
};
async function loadLanguageOnDemand(lang) {
if (!langCache[lang] && langLoaders[lang]) {
langCache[lang] = await langLoaders[lang]();
}
return langCache[lang];
}
- Virtual Scrolling לקבצים גדולים:
// הגבלת מספר השורות המרונדרות
EditorView.theme({
".cm-content": {
"max-height": "600px",
"overflow-y": "auto"
}
})
- Throttling לאירועים תכופים:
const throttle = (func, limit) => {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}
};
// שימוש בחיפוש
const throttledSearch = throttle(performSearch, 300);
🧪 בדיקות ואימות
בדיקות פונקציונליות
-
בדיקת טעינה:
- ודא ש-CodeMirror נטען רק כשנבחר
- ודא שה-fallback לעורך פשוט עובד
-
בדיקת תכונות:
- הדגשת תחביר לכל השפות
- חיפוש והחלפה (Ctrl+F, Ctrl+H)
- השלמה אוטומטית (Ctrl+Space)
- קיפול קוד
- מספרי שורות
-
בדיקת ביצועים:
- טעינת קבצים גדולים (>1MB)
- עריכה בזמן אמת
- זכרון ו-CPU
-
בדיקת תאימות:
- דפדפנים שונים (Chrome, Firefox, Safari, Edge)
- מובייל ו-טאבלט
- מצב offline
סקריפט בדיקה אוטומטי
// tests/editor.test.js
describe('CodeMirror Editor Tests', () => {
let editor;
beforeEach(() => {
editor = new EditorManager();
});
test('Toggle between editors', async () => {
// אתחול עורך פשוט
await editor.initEditor(container, {value: 'test'});
expect(editor.currentEditor).toBe('simple');
// מעבר ל-CodeMirror
await editor.toggleEditor(container);
expect(editor.currentEditor).toBe('codemirror');
expect(editor.getValue()).toBe('test');
});
test('Language detection', () => {
expect(editor.detectLanguage('def main():')).toBe('python');
expect(editor.detectLanguage('function test() {}')).toBe('javascript');
expect(editor.detectLanguage('<html>')).toBe('html');
});
test('RTL detection', () => {
expect(editor.detectRTL('שלום עולם')).toBe(true);
expect(editor.detectRTL('Hello World')).toBe(false);
});
test('Preference persistence', () => {
editor.savePreference('codemirror');
expect(localStorage.getItem('preferredEditor')).toBe('codemirror');
});
});
📈 שיפורים עתידיים
שלב 1 - תכונות בסיס (Sprint 1)
שלב 2 - תכונות מתקדמות (Sprint 2)
שלב 3 - אינטגרציות (Sprint 3)
🚀 הוראות הטמעה
שלב 1: הכנה
# יצירת branch חדש
git checkout -b feature/codemirror-editor
# יצירת הקבצים החדשים
touch webapp/static/js/editor-manager.js
touch webapp/static/js/codemirror-setup.js
touch webapp/static/css/codemirror-custom.css
שלב 2: הטמעת הקוד
- העתק את הקוד מהמדריך לקבצים המתאימים
- עדכן את
base.html עם הטעינה המותנית
- עדכן את
edit_file.html ו-upload.html
- הוסף את ה-endpoint ב-
app.py
שלב 3: בדיקות
# הרצה מקומית
cd webapp
python app.py
# בדיקה ידנית
# 1. גש ל-http://localhost:5000
# 2. ערוך קובץ קיים
# 3. נסה להחליף בין העורכים
# 4. ודא ששמירה עובדת
שלב 4: Deployment
# קומיט ו-push
git add -A
git commit -m "feat: Add CodeMirror editor option alongside simple textarea"
git push origin feature/codemirror-editor
# יצירת PR
gh pr create --title "הוספת אפשרות עורך CodeMirror" \
--body "הוספת עורך קוד מתקדם CodeMirror 6 כאופציה לצד העורך הפשוט הקיים"
📚 משאבים
תיעוד רשמי
מדריכים מומלצים
קהילה ותמיכה
🔍 מעקב שגיאות ולוגינג
Error Tracking System
class ErrorTracker {
constructor() {
this.errors = [];
this.maxErrors = 50; // הגבלת מספר שגיאות בזיכרון
this.setupGlobalErrorHandlers();
}
setupGlobalErrorHandlers() {
// תפיסת שגיאות לא מטופלות
window.addEventListener('unhandledrejection', (event) => {
this.logError({
type: 'unhandledRejection',
message: event.reason?.message || event.reason,
stack: event.reason?.stack,
context: 'CodeMirror Editor'
});
});
// תפיסת שגיאות רגילות
const originalConsoleError = console.error;
console.error = (...args) => {
if (args[0]?.includes('CodeMirror') || args[0]?.includes('editor')) {
this.logError({
type: 'consoleError',
message: args.join(' '),
timestamp: Date.now()
});
}
originalConsoleError.apply(console, args);
};
}
logError(error) {
// הוספת metadata
const enrichedError = {
...error,
timestamp: error.timestamp || Date.now(),
userAgent: navigator.userAgent,
url: window.location.href,
editorType: window.editorManager?.currentEditor
};
// שמירה מקומית
this.errors.push(enrichedError);
if (this.errors.length > this.maxErrors) {
this.errors.shift(); // הסרת השגיאה הישנה ביותר
}
// שליחה לשרת (אם מוגדר)
this.sendToServer(enrichedError);
// לוג מקומי
console.warn('[Editor Error]', enrichedError);
}
sendToServer(error) {
// שליחה אסינכרונית לשרת
if (window.location.hostname !== 'localhost') {
fetch('/api/log/editor-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(error)
}).catch(() => {
// נכשל בשקט - לא רוצים ליצור עוד שגיאות
});
}
}
getRecentErrors(count = 10) {
return this.errors.slice(-count);
}
clearErrors() {
this.errors = [];
}
}
// אינטגרציה ב-EditorManager
class EditorManager {
constructor() {
// ... קוד קיים ...
this.errorTracker = new ErrorTracker();
}
// עטיפת פעולות קריטיות ב-try-catch
async safeExecute(operation, fallback) {
try {
return await operation();
} catch (error) {
this.errorTracker.logError({
type: 'editorOperation',
operation: operation.name,
message: error.message,
stack: error.stack
});
if (fallback) {
return fallback(error);
}
throw error;
}
}
}
Backend Error Logging
# app.py - הוספת endpoint ללוגינג
@app.route('/api/log/editor-error', methods=['POST'])
def log_editor_error():
"""לוגינג של שגיאות עורך מה-frontend"""
try:
error_data = request.json
# ולידציה בסיסית
if not error_data or not isinstance(error_data, dict):
return jsonify({'error': 'Invalid data'}), 400
# הוספת מידע משתמש
error_data['user_id'] = session.get('user_id', 'anonymous')
error_data['session_id'] = session.get('session_id')
error_data['server_timestamp'] = datetime.now(timezone.utc)
# שמירה ב-collection נפרד
editor_errors_collection.insert_one(error_data)
# התראה על שגיאות קריטיות
if error_data.get('type') == 'criticalError':
notify_admins(error_data)
return jsonify({'success': True})
except Exception as e:
# לוג שקט - לא רוצים שגיאה בלוגינג של שגיאות
app.logger.error(f"Error logging failed: {e}")
return jsonify({'success': False}), 500
🔒 אבטחה
המלצות אבטחה
- סינון קלט: ודא סינון XSS בכל קלט
- הגבלת גודל: הגבל גודל מסמכים ל-10MB
- Rate limiting: הגבל מספר שמירות לדקה
- CSP Headers: הוסף Content-Security-Policy
- Sanitization: נקה HTML בתצוגה מקדימה
דוגמת הגנה
# ב-app.py
from markupsafe import Markup, escape
def sanitize_code(code):
"""ניקוי קוד מתגי HTML מסוכנים"""
# רק בתצוגה, לא בשמירה!
if '<script' in code.lower():
return escape(code)
return code
✅ צ'קליסט להטמעה
📝 הערות סיום
מדריך זה מספק תשתית מלאה להטמעת עורך CodeMirror 6 ב-WebApp. העורך מתוכנן לעבוד לצד העורך הקיים כך שמשתמשים יוכלו לבחור את החוויה המועדפת עליהם.
חשוב לזכור:
- העורך הפשוט נשאר ברירת המחדל
- המעבר בין העורכים חייב להיות חלק
- התוכן חייב להישמר במעבר
- ביצועים חשובים - טען CodeMirror רק בעת הצורך
בהצלחה בהטמעה! 🚀
📝 מדריך מימוש: עורך קוד CodeMirror ב-WebApp
📋 סקירה כללית
מדריך זה מתאר כיצד להוסיף עורך קוד מתקדם מבוסס CodeMirror 6 לצד העורך הקיים (textarea פשוט) ב-WebApp של CodeBot. המטרה היא לתת למשתמשים אפשרות בחירה בין עורך פשוט לעורך מתקדם עם יכולות עריכה משופרות.
🎯 יעדי הפיצ'ר
🌟 תכונות CodeMirror שיוטמעו
תכונות בסיס
תכונות מתקדמות
🏗️ ארכיטקטורה
מבנה הקומפוננטה
📦 התקנה ותלויות
1. הוספת CDN לבסיס התבנית
ב-
templates/base.html, בתוך ה-<head>:2. אופציה חלופית - שימוש עם Bundler
אם אתה מעדיף להשתמש ב-bundler (Webpack/Vite/Rollup) במקום CDN:
# התקנת החבילות הנדרשות npm install --save \ codemirror \ @codemirror/state \ @codemirror/view \ @codemirror/commands \ @codemirror/language \ @codemirror/search \ @codemirror/autocomplete \ @codemirror/lint \ @codemirror/theme-one-dark \ @codemirror/lang-python \ @codemirror/lang-javascript \ @codemirror/lang-html \ @codemirror/lang-css \ @codemirror/lang-markdown \ @codemirror/lang-sql \ @codemirror/lang-json3. הערות חשובות לגבי טעינת מודולים
💻 מימוש Frontend
1. מנהל העורכים -
static/js/editor-manager.js2. עדכון
templates/edit_file.html{% extends "base.html" %} {% block title %}עריכת {{ file.file_name }} - Code Keeper Bot{% endblock %} {% block head %} <style> /* סגנון לעורך CodeMirror */ .codemirror-container { border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); overflow: hidden; } .cm-editor { min-height: 400px; max-height: 600px; font-family: 'Fira Code', 'Consolas', monospace; } .editor-switcher { display: flex; justify-content: space-between; align-items: center; margin: 0.5rem 0; padding: 0.5rem; background: rgba(255,255,255,0.05); border-radius: 8px; } .btn-switch-editor { padding: 0.5rem 1rem; background: rgba(100,100,255,0.2); border: 1px solid rgba(100,100,255,0.5); border-radius: 6px; color: white; cursor: pointer; transition: all 0.3s; } .btn-switch-editor:hover { background: rgba(100,100,255,0.3); transform: translateY(-1px); } .editor-info { font-size: 0.85rem; opacity: 0.8; } /* אנימציית מעבר */ .editor-transitioning { opacity: 0.5; pointer-events: none; } </style> {% endblock %} {% block content %} <h1 class="page-title">עריכת קובץ</h1> <div class="glass-card"> {% if error %} <div class="alert alert-error">{{ error }}</div> {% endif %} {% if success %} <div class="alert alert-success">{{ success }}</div> {% endif %} <form method="post" style="display: grid; gap: 1rem;"> <div> <label>שם קובץ</label> <input type="text" name="file_name" value="{{ file.file_name }}" style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;"> </div> <div> <label>שפה</label> <select name="language" id="languageSelect" style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;"> <option value="{{ file.language }}">— {{ file.language }} (נוכחית) —</option> <option value="text">זהה לפי סיומת/תוכן</option> {% for lang in languages %} <option value="{{ lang }}">{{ lang }}</option> {% endfor %} </select> </div> <div> <label>תיאור</label> <input type="text" name="description" value="{{ file.description }}" style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;"> </div> <div> <label>תגיות</label> <input type="text" name="tags" value="{{ file.tags | join(', ') }}" placeholder="#utils, #repo:owner/name" style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;"> </div> <div id="editorContainer"> <label>קוד</label> <!-- העורך יוכנס כאן דינמית --> <textarea name="code" rows="18" style="width: 100%; padding: .75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white; font-family: 'Fira Code', 'Consolas', monospace;">{{ code_value }}</textarea> </div> <div style="display: flex; gap: 1rem;"> <button type="submit" class="btn btn-primary btn-icon"> <i class="fas fa-save"></i> שמור גרסה חדשה </button> <a href="/file/{{ file.id }}" class="btn btn-secondary btn-icon"> <i class="fas fa-times"></i> ביטול </a> </div> </form> </div> <script type="module"> // אתחול עורך הקוד document.addEventListener('DOMContentLoaded', async () => { const container = document.getElementById('editorContainer'); const languageSelect = document.getElementById('languageSelect'); // אתחול העורך await window.editorManager.initEditor(container, { language: '{{ file.language }}', value: {{ code_value | tojson }}, theme: 'dark' }); // עדכון שפה בעת שינוי languageSelect.addEventListener('change', async (e) => { const lang = e.target.value; // שימוש בפונקציה המובנית לעדכון שפה await window.editorManager.updateLanguage(lang); }); // אופציונלי: הוספת מחליף נושא const themeToggle = document.createElement('button'); themeToggle.className = 'theme-toggle'; themeToggle.innerHTML = '<i class="fas fa-moon"></i>'; themeToggle.title = 'החלף נושא'; themeToggle.onclick = async () => { const currentTheme = window.editorManager.getThemePreference(); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; await window.editorManager.updateTheme(newTheme); themeToggle.innerHTML = newTheme === 'dark' ? '<i class="fas fa-moon"></i>' : '<i class="fas fa-sun"></i>'; }; // הוספת כפתור הנושא לצד מחליף העורך (אם קיים) const switcher = document.querySelector('.editor-switcher'); if (switcher) { switcher.appendChild(themeToggle); } }); </script> {% endblock %}3. עדכון
templates/upload.htmlבאופן דומה<!-- אותן תוספות כמו ב-edit_file.html -->4. Auto‑Save מקומי (ללא תלות בשרת)
שומר טיוטה מקומית כל כמה שניות ומציע שחזור בביקור הבא. המפתח ממוספר לפי כתובת העמוד כדי למנוע דריסות בין קבצים שונים.
5. התאמת גובה העורך במובייל (visualViewport)
מונע דחיפת העורך מתחת למקלדת וירטואלית. עובד גם עם CodeMirror וגם עם textarea.
6. טעינת CDN עם Fallback
הוספנו מתודת
importWithFallbackודאגנו ש־getLanguageSupportו־getThemeינסו קודם jsDelivr ואז unpkg. כך טעינת חבילות השפה/הנושא תישאר יציבה גם ברשת לא אידיאלית.🔧 מימוש Backend
1. שמירת העדפות משתמש
ב-
app.py, הוסף endpoint:2. העברת העדפה לתבניות
🎨 עיצוב CSS מותאם
צור
static/css/codemirror-custom.css:⚡ ניטור ביצועים ואופטימיזציה
Performance Monitoring
אופטימיזציות מומלצות
🧪 בדיקות ואימות
בדיקות פונקציונליות
בדיקת טעינה:
בדיקת תכונות:
בדיקת ביצועים:
בדיקת תאימות:
סקריפט בדיקה אוטומטי
📈 שיפורים עתידיים
שלב 1 - תכונות בסיס (Sprint 1)
שלב 2 - תכונות מתקדמות (Sprint 2)
שלב 3 - אינטגרציות (Sprint 3)
🚀 הוראות הטמעה
שלב 1: הכנה
שלב 2: הטמעת הקוד
base.htmlעם הטעינה המותניתedit_file.htmlו-upload.htmlapp.pyשלב 3: בדיקות
שלב 4: Deployment
📚 משאבים
תיעוד רשמי
מדריכים מומלצים
קהילה ותמיכה
🔍 מעקב שגיאות ולוגינג
Error Tracking System
Backend Error Logging
🔒 אבטחה
המלצות אבטחה
דוגמת הגנה
✅ צ'קליסט להטמעה
📝 הערות סיום
מדריך זה מספק תשתית מלאה להטמעת עורך CodeMirror 6 ב-WebApp. העורך מתוכנן לעבוד לצד העורך הקיים כך שמשתמשים יוכלו לבחור את החוויה המועדפת עליהם.
חשוב לזכור:
בהצלחה בהטמעה! 🚀