/home/edulekha/crm.edulekha.com/modules/einvoice/assets/codemirror.js
/* 3.3.1 */
// Import only the necessary CodeMirror modules
import {EditorState} from '@codemirror/state';
import {EditorView, keymap, lineNumbers} from '@codemirror/view';
import {defaultKeymap, history, historyKeymap, undo, redo} from '@codemirror/commands';
import {syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldGutter, indentUnit} from '@codemirror/language';
import {linter, lintGutter} from '@codemirror/lint';
import {xml} from '@codemirror/lang-xml';
import {json} from '@codemirror/lang-json';
// Adjust editor height based on content
function adjustEditorHeight(view) {
// Get the editor container element
const editorElement = view.dom;
// Get total content height (in pixels)
const contentHeight = view.contentHeight;
// Calculate available space (80% of viewport height)
const availableHeight = window.innerHeight * 0.8;
// Set height to content height, but not less than 50vh and not more than available height
const minHeight = window.innerHeight * 0.5; // 50vh
const newHeight = Math.max(minHeight, Math.min(contentHeight, availableHeight));
// Apply the height
editorElement.style.height = newHeight + 'px';
}
// Initialize CodeMirror editor with XML or JSON linting
window.initCodeMirror = function(elementId, initialContent, readOnly = false, contentType = 'xml', onChangeCallback = null) {
// XML linter to validate XML in real-time
const xmlLinter = linter(view => {
const doc = view.state.doc.toString();
const diagnostics = [];
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(doc, 'text/xml');
// Check for parsing errors
const parserError = xmlDoc.getElementsByTagName('parsererror');
if (parserError.length > 0) {
// Try to extract line number from error message
const errorText = parserError[0].textContent;
const lineMatch = errorText.match(/line (\d+)/i);
const line = lineMatch ? parseInt(lineMatch[1], 10) - 1 : 0;
// Get the position in the document
const pos = view.state.doc.line(Math.max(1, line)).from;
diagnostics.push({
from: pos,
to: pos,
severity: 'error',
message: 'XML Error: ' + errorText.split('\n')[0]
});
}
} catch (e) {
diagnostics.push({
from: 0,
to: 0,
severity: 'error',
message: 'XML Error: ' + e.message
});
}
return diagnostics;
});
// JSON linter to validate JSON in real-time
const jsonLinter = linter(view => {
const doc = view.state.doc.toString();
const diagnostics = [];
try {
// Remove all mustache syntax before validating JSON
// This handles:
// - {{variable}} - simple variables
// - {{{variable}}} - unescaped variables
// - {{#section}} - section opening
// - {{/section}} - section closing
// - {{^inverted}} - inverted section opening
// - {{!comment}} - comments
// - {{>partial}} - partials
let cleanedDoc = doc
// Remove mustache comments first: {{! comment }}
.replace(/\{\{![^}]*\}\}/g, '')
// Remove triple mustache (unescaped): {{{variable}}}
.replace(/\{\{\{[^}]*\}\}\}/g, 'MUSTACHE_PLACEHOLDER')
// Remove section tags: {{#section}}, {{/section}}, {{^inverted}}
.replace(/\{\{[#/^][^}]*\}\}/g, '')
// Remove partials: {{>partial}}
.replace(/\{\{>[^}]*\}\}/g, 'MUSTACHE_PLACEHOLDER')
// Remove simple variables: {{variable}}
.replace(/\{\{[^}]*\}\}/g, 'MUSTACHE_PLACEHOLDER');
// Remove trailing commas (same pattern as backend PHP)
// Pattern matches comma followed by optional whitespace and closing bracket/brace
let pattern = /,(\s*[}\]])/g;
let count;
do {
const before = cleanedDoc;
cleanedDoc = cleanedDoc.replace(pattern, '$1');
count = before !== cleanedDoc;
} while (count);
JSON.parse(cleanedDoc);
} catch (e) {
// Try to extract line number from error message
const lineMatch = e.message.match(/line (\d+)/i) || e.message.match(/position (\d+)/i);
let line = 0;
let pos = 0;
if (lineMatch) {
const position = parseInt(lineMatch[1], 10);
// For position-based errors, convert to line
const lines = doc.substring(0, position).split('\n');
line = Math.max(0, lines.length - 1);
pos = view.state.doc.line(line + 1).from;
} else {
// If we can't determine the line, highlight the first character
pos = 0;
}
diagnostics.push({
from: pos,
to: pos,
severity: 'error',
message: 'JSON Error: ' + e.message
});
}
return diagnostics;
});
// Configure editor extensions (plugins)
const extensions = [
syntaxHighlighting(defaultHighlightStyle),
bracketMatching(),
foldGutter(),
lineNumbers(),
// Language-specific features based on content type
contentType === 'json' ? json() : xml(),
lintGutter(), // Shows lint markers in the gutter
contentType === 'json' ? jsonLinter : xmlLinter, // Apply appropriate linter
// Editor behavior
history(), // Enable undo/redo history tracking
keymap.of([ // Add keyboard shortcuts
...historyKeymap, // Add history-related keyboard shortcuts (Ctrl+Z, Ctrl+Y)
...defaultKeymap
]),
indentUnit.of(' '),
EditorState.tabSize.of(2),
];
// Add read-only mode if specified
if (readOnly) {
extensions.push(EditorView.editable.of(false));
}
// Create the editor instance
const element = document.getElementById(elementId);
if (!element) return;
const view = new EditorView({
state: EditorState.create({
doc: initialContent || '',
extensions: extensions
}),
lineNumbers: true,
parent: element
});
// Add content change listener if callback is provided
if (onChangeCallback && typeof onChangeCallback === 'function') {
// Use a more compatible approach with DOM events and MutationObserver
const editorElement = view.dom;
let lastContent = view.state.doc.toString();
function handleContentChange() {
const currentContent = view.state.doc.toString();
if (currentContent !== lastContent) {
lastContent = currentContent;
setTimeout(() => onChangeCallback(currentContent), 0);
}
}
// Listen for various content change events
editorElement.addEventListener('input', handleContentChange);
editorElement.addEventListener('paste', () => {
setTimeout(handleContentChange, 10); // Small delay for paste to complete
});
editorElement.addEventListener('keyup', handleContentChange);
editorElement.addEventListener('keydown', () => {
setTimeout(handleContentChange, 0);
});
// Use MutationObserver as additional safety net
const observer = new MutationObserver(handleContentChange);
observer.observe(editorElement, {
childList: true,
subtree: true,
characterData: true
});
// Store the cleanup function on the view object
view._cleanupCallback = function() {
editorElement.removeEventListener('input', handleContentChange);
editorElement.removeEventListener('paste', handleContentChange);
editorElement.removeEventListener('keyup', handleContentChange);
editorElement.removeEventListener('keydown', handleContentChange);
observer.disconnect();
};
}
return view;
};
// Helper to get content from editor
window.getCodeMirrorValue = function(editorView) {
if (!editorView) return '';
return editorView.state.doc.toString();
};
// Undo the last change in the editor
window.undoCodeMirrorChange = function(editorView) {
if (!editorView) return;
undo(editorView);
};
// Redo the last undone change
window.redoCodeMirrorChange = function(editorView) {
if (!editorView) return;
redo(editorView);
};
// Check if undo is available
window.canUndoCodeMirror = function(editorView) {
if (!editorView) return false;
// Use internal state to check if undo is available
return editorView.state.field(history.field).undoDepth > 0;
};
// Check if redo is available
window.canRedoCodeMirror = function(editorView) {
if (!editorView) return false;
// Use internal state to check if redo is available
return editorView.state.field(history.field).redoDepth > 0;
};