// const ZERO_WIDTH_SPACE = '​'; const ZERO_WIDTH_SPACE = '​'; class EditableInputModel { constructor(value = '') { this.value = value; } setValue(newValue) { this.value = newValue; } getValue() { return this.value; } } class EditableInputView { constructor(model, controller) { this.model = model; this.controller = controller; } render(element) { this.element = element; this.element.innerHTML = this.generateInputHtml(); this.editableDiv = this.element.querySelector('.editableInputContent'); this.attachEventListeners(); } generateInputHtml() { let contentHtml = this.model.getValue() ? this.renderContent(this.parseContent(this.model.getValue())) : ZERO_WIDTH_SPACE; return `
${contentHtml}
`; } attachEventListeners() { this.editableDiv.addEventListener('input', (event) => { this.controller.handleInput(this.editableDiv); if (this.controller.additionalCallback) { this.controller.additionalCallback(this.model.getValue()); } }); } updateUI() { let content = this.model.getValue(); if (content) { this.editableDiv.innerHTML = this.renderContent(this.parseContent(content)); } else { this.editableDiv.innerHTML = ZERO_WIDTH_SPACE; // Insert a zero-width space } this.editableDiv.setAttribute('data-value', content); } renderContent(contentArray) { let content = ''; contentArray.forEach((item) => { if (item.startsWith('{{') && item.endsWith('}}')) { content += `${item}`; } else { content += `${item}`; } }); return content; } parseContent(inputString) { let returnStringArray = []; let regex = /{{.*?}}/g; let lastIndex = 0; inputString.replace(regex, (match, index) => { // Add the string before the match if (index > lastIndex) { returnStringArray.push(inputString.slice(lastIndex, index)); } // Add the matched string including the delimiters returnStringArray.push(match); // Update lastIndex for the next iteration lastIndex = index + match.length; }); // Add any remaining string after the last match if (lastIndex < inputString.length) { returnStringArray.push(inputString.slice(lastIndex)); } return returnStringArray; } } class EditableInputController { constructor(model) { this.model = model; this.view = new EditableInputView(this.model, this); } init(element, callback) { this.view.render(element); this.additionalCallback = callback; } getCurrentCursor(element) { const selection = window.getSelection(); if (selection.rangeCount === 0) return null; const range = selection.getRangeAt(0); const preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(element); preCaretRange.setEnd(range.endContainer, range.endOffset); return preCaretRange.toString().trim().length; } handleInput(editableDiv) { // Check and remove zero-width space if it's the only content if (editableDiv.innerHTML === '​') { editableDiv.innerHTML = ''; } let cursor = this.getCurrentCursor(editableDiv); let mergedString = Array.from(editableDiv.childNodes) .filter(function (node) { // Keep only SPAN elements and text nodes return node.nodeName.toUpperCase() === 'SPAN' || node.nodeType === 3; }) .map(node => { return node.nodeType === 3 ? node.data.trim() : node.innerText.trim(); }) .join(''); var result = mergedString.replace(/[\u200B-\u200D\uFEFF]/g, ''); let content = this.parseContent(result); this.model.setValue(content.join('')); // Optionally, update the view if needed this.view.updateUI(); this.restoreCursor(editableDiv, cursor); } parseContent(inputString) { let returnStringArray = []; let regex = /{{.*?}}/g; let lastIndex = 0; inputString.replace(regex, (match, index) => { // Add the string before the match if (index > lastIndex) { returnStringArray.push(inputString.slice(lastIndex, index)); } // Add the matched string including the delimiters returnStringArray.push(match); // Update lastIndex for the next iteration lastIndex = index + match.length; }); // Add any remaining string after the last match if (lastIndex < inputString.length) { returnStringArray.push(inputString.slice(lastIndex)); } return returnStringArray; } restoreCursor(element, position) { const selection = window.getSelection(); const range = document.createRange(); range.setStart(element, 0); range.collapse(true); const nodeStack = [element]; let node, foundStart = false, stop = false; let charIndex = 0; while (!stop && (node = nodeStack.pop())) { if (node.nodeType === 3) { const nextCharIndex = charIndex + node.length; if (!foundStart && position >= charIndex && position <= nextCharIndex) { range.setStart(node, position - charIndex); foundStart = true; } if (foundStart && position >= charIndex && position <= nextCharIndex) { range.setEnd(node, position - charIndex); stop = true; } charIndex = nextCharIndex; } else { let i = node.childNodes.length; while (i--) { nodeStack.push(node.childNodes[i]); } } } selection.removeAllRanges(); selection.addRange(range); } updateUI() { this.view.updateUI(); } } class EditableInput { constructor(options = {}) { // Initialize the model with the value this.model = new EditableInputModel(options.value || ''); this.controller = new EditableInputController(this.model); // Additional properties as required this.name = options.name || ''; this.id = options.id || ''; this.class = options.class || 'form-control'; this.placeholder = options.placeholder || ''; } init(element, callback) { // Initialize the controller with the DOM element this.controller.init(element, callback); } getValue() { // Delegate to the model return this.model.getValue(); } setValue(newValue) { // Delegate to the model and update the view this.model.setValue(newValue); this.controller.updateUI(); } // Any additional methods required for interaction with this component } export default EditableInput;