Files
elink-test-master/ApiTestManager/src/components/EditableInput.js
T
2024-04-12 11:11:54 +09:00

284 lines
8.7 KiB
JavaScript

// 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 `
<div contenteditable="true" class="editableInputContent form-control" data-value="${this.model.getValue()}">
${contentHtml}
</div>
`;
}
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);
}
});
}
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 += `<span class="variable">${item}</span>`;
} else {
content += `<span class="text">${item}</span>`;
}
});
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 === '&#8203;') {
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;