// 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, options) {
this.model = model;
this.controller = controller;
this.options = options;
}
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;
let styleString = '';
if (this.options.style && typeof this.options.style === 'object') {
const styles = Object.entries(this.options.style);
styleString = styles.map(([prop, value]) => `${prop}: ${value};`).join(' ');
}
let style = styleString ? `style="${styleString}"` : '';
return `
${contentHtml}
`;
}
attachEventListeners() {
this.editableDiv.addEventListener('input', (event) => {
this.controller.handleInput(this.editableDiv);
if (this.controller.additionalCallback) {
this.controller.additionalCallback(this.model.getValue());
}
});
// this.editableDiv.addEventListener('paste', (event) => {
// event.preventDefault();
// if (navigator.clipboard && navigator.clipboard.readText) {
// navigator.clipboard.readText().then(text => {
// this.insertTextAtCursor(text);
// }).catch(err => {
// console.error('Failed to read clipboard contents: ', err);
// // Fallback to using clipboardData from the paste event
// const text = (event.clipboardData || window.clipboardData).getData('text');
// this.insertTextAtCursor(text);
// });
// } else {
// const text = (event.clipboardData || window.clipboardData).getData('text');
// this.insertTextAtCursor(text);
// }
// });
this.editableDiv.addEventListener('paste', (event) => {
event.preventDefault();
let text = (event.clipboardData || window.clipboardData).getData('text/plain'); // Using 'text/plain' for compatibility
if (text) {
console.log('Pasted text:', text); // Debug: Log pasted text
this.insertTextAtCursor(text);
if (this.controller.additionalCallback) {
this.controller.additionalCallback(this.model.getValue());
}
} else if (navigator.clipboard && navigator.clipboard.readText) {
navigator.clipboard.readText().then(text => {
console.log('Pasted text from clipboard API:', text); // Debug: Log pasted text
this.insertTextAtCursor(text);
if (this.controller.additionalCallback) {
this.controller.additionalCallback(this.model.getValue());
}
}).catch(err => {
console.error('Failed to read clipboard contents:', err);
});
} else {
console.error('No text found to paste and clipboard API not available.');
}
});
}
insertTextAtCursor(text) {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
range.deleteContents();
const textNode = document.createTextNode(text);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
this.controller.handleInput(this.editableDiv);
}
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, options) {
this.model = model;
this.view = new EditableInputView(this.model, this, options);
}
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);
const content = preCaretRange.toString().trim();
if (content === '\u200B') {
return 0; // Return 0 if the content is only the zero-width space character
}
return content.replace('\u200B', '').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('');
let 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);
}
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);
}
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;
}
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, options);
// 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;