Files
P42_UI/app/thirdparty/eicui/plugins/HtmlEditor/HtmlEditor.js
T
2025-08-27 07:03:09 +00:00

1583 lines
69 KiB
JavaScript

class HtmlEditor extends EicComponent {
constructor(el, context, options = {}) {
super();
this.el = el
Object.assign(this, app.helpers.activeAttributes)
Object.assign(this, app.helpers.basicDialogs)
this.components = [];
this.context = context;
this.externalHandlers = options.externalHandlers || {};
this.images = options.images || [];
this.tokens = options.tokens || [];
this.tplStatus = options.templateStatus || [];
this.buttonMap = this.buttonMap || {
subject: ['heUndo','heToken'],
header: ['heUndo', 'heLink', 'heTextAlignment', 'heToogleStyle', 'heChangeFontSize', 'heColorText', 'heImage', 'heToken'],
main: ['heUndo', 'heTable', 'heAddTable', 'heLink', 'heList', 'heTextAlignment','heToogleStyle', 'heChangeFontSize', 'heColorText', 'heImage', 'heToken'],
footer: ['heUndo', 'heLink', 'heTextAlignment', 'heToogleStyle', 'heChangeFontSize', 'heColorText', 'heImage', 'heToken'],
altText: ['heUndo', 'heToken']
};
this.initialize();
this.undoStack = [];
this.redoStack = [];
this.isUndoRedo = false;
this.boldTextButton = null;
this.lastModifiedSpan = null;
this.linkUrlInput = null;
this.spaceBefore = document.createTextNode('\u00A0');
this.spaceAfter = document.createTextNode('\u00A0');
if (HtmlEditor.instance) {
return HtmlEditor.instance;
}
HtmlEditor.instance = this;
this.injectStyles();
}
initialize() {
const loadingMessage = this.el.querySelector('.tpl-loadingTools');
const toolButtonsContainer = this.el.querySelector('.tpl-toolButtons');
// Afficher "Loading..." et cacher le menu
loadingMessage.style.display = 'block';
toolButtonsContainer.style.display = 'none';
this.buildToolButtons('subject');
setTimeout(() => {
this.setCursorToField('subject');
}, 0);
// Attendre le chargement des boutons puis afficher le menu et masquer "Loading..."
setTimeout(() => {
loadingMessage.style.display = 'none';
toolButtonsContainer.style.display = 'block';
}, 500); // Ajuste le délai si nécessaire
this.components = Array.from(toolButtonsContainer.querySelectorAll('.tool-button')).map(button => ({
el: button
}));
this.initEditorEvents();
//*sanitize copy/paste
['.template-field.subject','.template-field.header', '.template-field.main', '.template-field.footer', '.template-field.altText'].forEach(selector => {
const editor = this.el.querySelector(selector);
editor.contentEditable = 'true';
editor.addEventListener('paste', (event) => this.handlePaste(event));
editor.addEventListener('input', (event) => this.saveState(event));
});
}
buildToolButtons(fieldType) {
const availableButtons = this.buttonMap[fieldType] || []
const toolButtonsContainer = this.el.querySelector('.tpl-toolButtons')
this.setCursorToField(fieldType);
toolButtonsContainer.innerHTML = ''; // Init buttons
//Manage tools buttons if Status != Submitted
const isSubmitted = this.tplStatus === 'submitted' ? 'disabled' : '';
const buttonTemplates = {
// Add buttons by field permissions
heUndo: `
<button eicbutton xsmall type="button" class="tool-button icon-back he-undo" data-tool="heUndo" data-trigger="undo" ${isSubmitted}></button>
`,heTable: `
<button eicbutton xsmall type="button" class="tool-button medium icon-table he-addTable" data-tool="heTable" title="Add table" data-trigger="showTableInput" ${isSubmitted}></button>
<div id="he-tableContainer" class="he-tableContainer" style="display:none;">
<label xsmall for="he-tableRows">Number of Rows:</label>
<input eicinput xsmall type="number" id="he-tableRows" class="he-tableRows" min="1" placeholder="Enter rows">
<label xsmall for="he-tableCols">Number of Columns:</label>
<input eicinput xxsmall type="number" id="he-tableCols" class="he-tableCols" min="1" placeholder="Enter columns">
<div class="cols-2">
<div>
<label xsmall for="he-addTableBorders">Add Borders:</label>
<input eiccheckbox type="checkbox" id="he-addTableBorders" class="he-addTableBorders">
</div>
<div>
<label xsmall for="he-addTableCenter">Center in page:</label>
<input eiccheckbox type="checkbox" id="he-addTableCenter" class="he-addTableCenter">
</div>
</div>
<button eicbutton xsmall class="info tool-button he-confirmAddTable" data-trigger="createTable">Add Table</button>
</div>
`,
heLink: `
<button eicbutton xsmall type="button" class="tool-button medium icon-link he-addLink" data-tool="heLink" title="Add link" data-trigger="showLinkInput" data-value="showLinkInput" ${isSubmitted}></button>
<div id="he-linkContainer" class="he-linkContainer" style="display:none;">
<input eicinput xsmall type="text" id="he-linkText" name="he-linkText" class="he-linkText" placeholder="Enter Link Text" style="display:none;">
<input eicinput xsmall type="text" id="he-linkUrl" name="he-linkUrl" class="he-linkUrl" placeholder="Enter URL or em@il">
<button eicbutton xsmall class="info tool-button he-confirmAddLink" data-trigger="createLink">Add Link</button>
</div>
`,
heList: `
<button eicbutton xsmall type="button" class="tool-button medium icon-list-ul he-addList" data-tool="heList" title="Add list" data-trigger="showListSelector" ${isSubmitted}></button>
<div id="he-addListContainer" class="he-addListContainer" style="display:none;">
<textarea eictextarea class="he-listItems" placeholder="Enter list items, one per line"></textarea>
<label xsmall>Choose list type :</label>
<label xsmall>
<input type="radio" class="he-listTypeBullet" name="listType" checked>
Bullet
</label>
<label xsmall>
<input type="radio" class="he-listTypeDash" name="listType">
Dash
</label>
<label xsmall>
<input type="radio" class="he-listTypeNumbered" name="listType">
Numbered
</label>
<button eicbutton xsmall class="info tool-button he-confirmAddListButton" data-trigger="confirmList">Add List</button>
</div>
`,
heTextAlignment: `
<button type="button" eicbutton xsmall class="tool-button icon-align-center he-alignCenter" data-tool="heTextAlignment" data-trigger="applyTextAlignment" data-value="center" ${isSubmitted}></button>
<button type="button" eicbutton xsmall class="tool-button icon-align-left he-alignLeft" data-tool="heTextAlignment" data-trigger="applyTextAlignment" data-value="left" ${isSubmitted}></button>
<button type="button" eicbutton xsmall class="tool-button icon-align-right he-alignRight" data-tool="heTextAlignment" data-trigger="applyTextAlignment" data-value="right" ${isSubmitted}></button>
`,
heToogleStyle: `
<button type="button" eicbutton xsmall class="tool-button icon-format-underline he-underlineText" data-tool="heToogleStyle" data-trigger="toggleStyle" data-value="underline" ${isSubmitted}></button>
<button type="button" eicbutton xsmall class="tool-button icon-format-bold he-boldText" data-tool="heToogleStyle" data-trigger="toggleStyle" data-value="bold" ${isSubmitted}></button>
<button type="button" eicbutton xsmall class="tool-button icon-format-italic he-italicText" data-tool="heToogleStyle" data-trigger="toggleStyle" data-value="italic" ${isSubmitted}></button>
`,
heChangeFontSize:`
<button type="button" eicbutton xsmall class="tool-button icon-font-size-up he-increaseFontSize" data-tool="heChangeFontSize" data-trigger="changeFontSize" data-value="1" ${isSubmitted}></button>
<button type="button" eicbutton xsmall class="tool-button icon-font-size-down he-decreaseFontSize" data-tool="heChangeFontSize" data-trigger="changeFontSize" data-value="-1" ${isSubmitted}></button>
`,
heColorText: `
<button type="button" eicbutton xsmall class="tool-button icon-format-color he-colorText" data-tool="heColorText" data-trigger="showColorPalette" ${isSubmitted}></button>
<div id="he-colorPalette" class="he-colorPalette" style="display:none;">
<div class="he-colorOption black" style="background-color: var(--eicui-base-color-black);" data-trigger="applyColorToText" data-value="var(--eicui-base-color-black)"></div>
<div class="he-colorOption primary" style="background-color:var(--eicui-base-color-primary-100);" data-trigger="applyColorToText" data-value="var(--eicui-base-color-primary-100)"></div>
<div class="he-colorOption info" style="background-color: var(--eicui-base-color-info-100);" data-trigger="applyColorToText" data-value="var(--eicui-base-color-info-100)"></div>
<div class="he-colorOption success" style="background-color: var(--eicui-base-color-success-100);" data-trigger="applyColorToText" data-value="var(--eicui-base-color-success-100)"></div>
<div class="he-colorOption danger" style="background-color:var(--eicui-base-color-danger-100);" data-trigger="applyColorToText" data-value="var(--eicui-base-color-danger-100)"></div>
<div class="he-colorOption warning" style="background-color:var(--eicui-base-color-warning-100);" data-trigger="applyColorToText" data-value=var(--eicui-base-color-warning-100)"></div>
<div class="he-colorOption accent" style="background-color: var(--eicui-base-color-accent-100);" data-trigger="applyColorToText" data-value="var(--eicui-base-color-accent-100)"></div>
<div class="he-colorOption velvet" style="background-color: #55378c;" data-trigger="applyColorToText" data-value="#55378c"></div>
<div class="he-colorOption grey" style="background-color: #dcd8e7;" data-trigger="applyColorToText" data-value="#dcd8e7"></div>
</div>
`,heImage: `
<button eicbutton xsmall type="button" class="tool-button medium icon-image he-addImg" data-tool="heImage" title="Add image" data-trigger="showImageSelector" ${isSubmitted}></button>
<div id="he-imgSelectContainer" class="eicui-input-container he-imgSelectContainer" style="display:none;">
<label xsmall for="he-imgSelect">Select an image:</label>
<select eicselect id="he-imgSelect" class="selects he-imgSelect"></select>
<label xsmall for="he-imgUrl">Add link to image? Add URL or Em@il:</label>
</div>
`,
heToken: `
<button eicbutton xsmall type="button" class="tool-button icon-loop he-addToken" data-tool="heToken" title="Add content variable" data-trigger="showTokenInput" ${isSubmitted}></button>
<div id="he-tokenContainer" class="he-tokenContainer" style="display:none;">
<label small for="he-tokenText">You can write your own custom content variable in the text field below:</label>
<input eicinput small type="text" id="he-tokenText" name="he-tokenText" class="he-tokenText" placeholder="Enter content variable">
<label small"><b>OR</b></label>
<label small for="he-tokenSelect">Select a predefined content variable in the list below :</label>
<select eicselect small id="he-tokenSelect" name="he-tokenSelect" class="he-tokenSelect"></select>
</br>
<button eicbutton small class="info tool-button he-confirmAddToken" data-trigger="createToken">Add content variable</button>
</div>
`
};
// Gen. HTML buttons
toolButtonsContainer.innerHTML = availableButtons.map(btn => buttonTemplates[btn] || '').join('')
this.attachButtonTriggers(toolButtonsContainer)
this.toggleButtonsByFieldType(fieldType)
}
setCursorToField(fieldType) {
const field = this.el.querySelector(`.template-field.${fieldType}`);
if (field && field.contentEditable !== "false") {
field.focus();
}
}
/*
LISTENER
*/
initEditorEvents() {
const triggers = this.el.querySelectorAll('[data-trigger]');
triggers.forEach(element => {
const triggerMethods = element.dataset.trigger.split(',').map(method => method.trim());
element.addEventListener('click', () => {
const changeValue = element.dataset.value || null;
const numericChange = (changeValue !== null && !isNaN(changeValue))
? parseFloat(changeValue)
: changeValue;
triggerMethods.forEach(triggerMethod => {
let method = this[triggerMethod];
// 👇 Fallback vers externalHandlers si la méthode n'existe pas dans la classe
if (typeof method !== 'function' && this.externalHandlers) {
method = this.externalHandlers[triggerMethod];
}
if (typeof method === 'function') {
method.call(this, numericChange); // on garde `this` comme contexte
} else {
console.error(`Method ${triggerMethod} doesn't exist`);
}
});
});
});
}
attachButtonTriggers(container) {
const triggers = container.querySelectorAll('[data-trigger]');
triggers.forEach(element => {
const triggerMethods = element.dataset.trigger.split(',').map(method => method.trim());
element.addEventListener('click', () => {
const changeValue = element.dataset.value || null;
let numericChange = isNaN(changeValue) ? changeValue : parseFloat(changeValue);
triggerMethods.forEach(triggerMethod => {
if (typeof this[triggerMethod] === 'function') {
this[triggerMethod](numericChange);
} else {
console.error(`Method ${triggerMethod} does not exist`);
}
});
});
});
}
toggleButtonsByFieldType(fieldType) {
const availableButtons = this.buttonMap[fieldType] || [];
availableButtons.forEach(button => {
const btnElement = this.el.querySelector(`.tpl-toolButtons .${button}`);
if (btnElement) {
btnElement.classList.remove('hidden');
}
});
}
isInsertionAllowed(range) {
if (!range || !range.startContainer) return false
let parentElement = null
if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
parentElement = range.startContainer.closest('.template-field')
} else if (range.startContainer.nodeType === Node.TEXT_NODE) {
parentElement = range.startContainer.parentNode?.closest('.template-field')
}
return parentElement !== null
}
saveSelection() {
const selection = window.getSelection()
if (selection.rangeCount > 0) {
this.savedRange = selection.getRangeAt(0).cloneRange()
this.selectedText = selection.toString()
}
}
restoreSelection() {
if (this.savedRange) {
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(this.savedRange)
}
}
saveState() {
if (this.isUndoRedo) return
const state = {
subject: this.el.querySelector('.template-field.subject').innerHTML,
header: this.el.querySelector('.template-field.header').innerHTML,
main: this.el.querySelector('.template-field.main').innerHTML,
footer: this.el.querySelector('.template-field.footer').innerHTML,
altText: this.el.querySelector('.template-field.altText').innerHTML
};
this.undoStack.push(state)
this.redoStack = []
}
restoreState(state) {
this.el.querySelector('.template-field.subject').innerHTML = state.subject
this.el.querySelector('.template-field.header').innerHTML = state.header
this.el.querySelector('.template-field.main').innerHTML = state.main
this.el.querySelector('.template-field.footer').innerHTML = state.footer
this.el.querySelector('.template-field.altText').innerText = state.altText
}
undo() {
if (this.undoStack.length > 1) {
this.isUndoRedo = true
const currentState = this.undoStack.pop()
this.redoStack.push(currentState)
const lastState = this.undoStack[this.undoStack.length - 1]
this.restoreState(lastState)
this.isUndoRedo = false
}
}
redo() {
if (this.redoStack.length > 0) {
this.isUndoRedo = true
const nextState = this.redoStack.pop()
this.undoStack.push(nextState)
this.restoreState(nextState)
this.isUndoRedo = false
}
}
//clear copy/paste content
handlePaste(event) {
event.preventDefault();
const text = event.clipboardData.getData('text/plain');
const range = window.getSelection().getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
}
/* Ensure not alternateTextEditor */
isSelectionInsideAlternateTextEditor() {
const alternateTextEditor = this.el.querySelector('.template-field.altText');
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const selectedElement = range.commonAncestorContainer;
// Check if selected content not in alternateTextEditor
return alternateTextEditor && alternateTextEditor.contains(selectedElement);
}
return false;
}
/* MANAGE STYLES
Bold Underline Italic */
toggleStyle(styleType) {
if (this.isSelectionInsideAlternateTextEditor()) return;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const selectedText = range.toString();
let isStyled = false;
let parentStyledNode = null;
let currentNode = range.startContainer;
let tagName, styleProperty, styleValue;
if (styleType === 'bold') {
tagName = 'SPAN';
styleProperty = 'fontWeight';
styleValue = 'bold';
} else if (styleType === 'italic') {
tagName = 'EM';
styleProperty = 'fontStyle';
styleValue = 'italic';
} else {
tagName = 'U';
styleProperty = 'textDecoration';
styleValue = 'underline';
}
if (currentNode.nodeType === Node.TEXT_NODE) {
currentNode = currentNode.parentNode;
}
while (currentNode && currentNode.nodeType !== Node.DOCUMENT_NODE) {
if (currentNode.nodeType === Node.ELEMENT_NODE && currentNode.nodeName === tagName) {
if (currentNode.style[styleProperty] === styleValue) {
isStyled = true;
parentStyledNode = currentNode;
break;
}
}
currentNode = currentNode.parentNode;
}
if (isStyled) {
const textNode = document.createTextNode(selectedText);
if (parentStyledNode) {
parentStyledNode.parentNode.replaceChild(textNode, parentStyledNode);
selection.removeAllRanges();
selection.addRange(range);
}
} else {
const element = document.createElement(tagName);
element.style[styleProperty] = styleValue;
range.deleteContents();
range.insertNode(this.spaceAfter);
range.insertNode(element);
range.insertNode(this.spaceBefore);
element.appendChild(document.createTextNode(selectedText));
selection.removeAllRanges();
const newRange = document.createRange();
newRange.selectNodeContents(element);
selection.addRange(newRange);
this.saveState();
}
}
}
applyTextAlignment(alignment) {
if (this.isSelectionInsideAlternateTextEditor()) return;
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
// Check if the cursor is inside a text node
let blockElement = this.findBlockElement(range.startContainer);
if (!blockElement) return;
const isAligned = blockElement.style.textAlign === alignment;
if (isAligned) {
this.removeAlignment(blockElement);
} else {
this.applyAlignment(blockElement, alignment);
}
this.saveState();
}
applyAlignment(blockElement, alignment) {
blockElement.style.textAlign = alignment;
}
removeAlignment(blockElement) {
blockElement.style.textAlign = '';
}
findBlockElement(node) {
while (node && node.nodeType !== Node.DOCUMENT_NODE) {
if (node.nodeType === Node.ELEMENT_NODE && (node.nodeName === 'P' || node.nodeName === 'DIV')) {
return node;
}
node = node.parentNode;
}
return null;
}
changeFontSize(change) {
if (this.isSelectionInsideAlternateTextEditor()) return;
const selection = window.getSelection();
let targetSpan = null;
if (selection.rangeCount) {
const range = selection.getRangeAt(0);
const selectedText = range.toString().trim();
if (selectedText.length > 0) {
let span = range.startContainer.parentNode;
if (span.tagName === 'SPAN' && span.id.startsWith('highlighted-text-')) {
targetSpan = span;
} else {
const uniqueId = `highlighted-text-${Date.now()}`;
span = document.createElement('span');
span.id = uniqueId;
span.classList.add('highlight');
range.surroundContents(span);
targetSpan = span;
}
this.lastModifiedSpan = targetSpan;
}
}
if (this.lastModifiedSpan) {
const currentFontSize = window.getComputedStyle(this.lastModifiedSpan).getPropertyValue('font-size');
const newFontSize = (parseFloat(currentFontSize) + change) + 'px';
this.lastModifiedSpan.style.fontSize = newFontSize;
}
selection.removeAllRanges();
this.saveState();
}
//COLOR TEXT
changeColor(color) {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
// Ensure alternateTextEditor is a valid node
const alternateTextEditor = this.el.querySelector('.template-field.altText');
if (!alternateTextEditor) return; // If the editor element doesn't exist, exit
// Check if a node is within the alternate text editor
const isNodeWithinEditor = (node) => {
while (node) {
if (node === alternateTextEditor) return true;
node = node.parentNode;
}
return false;
};
if (isNodeWithinEditor(range.commonAncestorContainer)) {
return;
}
if (range.collapsed) return;
// Create a span to apply the color
const span = document.createElement('span');
span.style.color = color;
// Wrap the selected range with the span
range.surroundContents(span);
this.saveState();
}
/*MANAGE TABLE */
showTableInput() {
this.saveSelection();
this.tableContainer = this.el.querySelector('.he-tableContainer');
this.buttonRect = null;
this.rowsInput = this.tableContainer.querySelector('.he-tableRows');
this.colsInput = this.tableContainer.querySelector('.he-tableCols');
this.borderCheckbox = this.tableContainer.querySelector('.he-addTableBorders');
this.centerCheckbox = this.tableContainer.querySelector('.he-addTableCenter');
const addTableButton = this.el.querySelector('.he-addTable');
const isTableContainerVisible = window.getComputedStyle(this.tableContainer).display !== "none";
// Reset the input values when opening
this.rowsInput.value = '';
this.colsInput.value = '';
if (!isTableContainerVisible) {
if (addTableButton) {
this.buttonRect = addTableButton.getBoundingClientRect();
}
// Display the table input under the button
this.tableContainer.style.display = 'block';
this.tableContainer.style.position = 'fixed';
this.tableContainer.style.border = '1px solid #ccc';
this.tableContainer.style.padding = '10px';
this.tableContainer.style.background = '#fff';
this.tableContainer.style.zIndex = '999';
const containerWidth = this.tableContainer.offsetWidth;
const buttonRect = addTableButton.getBoundingClientRect();
const topPosition = buttonRect.bottom + window.scrollY;
const leftPosition = buttonRect.left + window.scrollX - containerWidth;
this.tableContainer.style.top = `${topPosition}px`;
this.tableContainer.style.left = `${leftPosition}px`;
// Update button to "cancel" mode
addTableButton.classList.remove("icon-table");
addTableButton.classList.add("icon-cancel");
addTableButton.setAttribute("title", "close");
} else {
this.closeTableInput();
}
}
closeTableInput() {
// Hide the table input
this.tableContainer.style.display = "none";
// Reset the button back to "add table" mode
const addTableButton = this.el.querySelector('.he-addTable');
addTableButton.classList.remove("icon-cancel");
addTableButton.classList.add("icon-table");
addTableButton.setAttribute("title", "add table");
// Reset the button rect
this.buttonRect = null;
return
}
createTable() {
this.restoreSelection();
const numRows = parseInt(this.rowsInput.value) || 1;
const numCols = parseInt(this.colsInput.value) || 1;
const hasBorders = this.borderCheckbox.checked;
const tabCenter = this.centerCheckbox.checked;
// Validate inputs
if (numRows < 1 || numCols < 1) {
ui.growl.append('Please enter valid numbers for rows and columns.');
return;
}
// Insert the table
this.insertTable(numRows, numCols, hasBorders, tabCenter);
this.closeTableInput();
}
insertTable(rows, cols, hasBorders, tabCenter) {
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
if (!this.isInsertionAllowed(range)) {
ui.growl.append('select a valid location', 'error');
return;
}
// Créer l'élément table
const tplTable = document.createElement('table');
tplTable.classList.add('tpl-table');
if (hasBorders) {
tplTable.classList.add('tpl-table-bordered');
tplTable.style.border = "1px solid #999";
tplTable.style.borderCollapse = "collapse";
}
if (tabCenter) {
tplTable.style.margin = "auto";
}
// Add rows and cols
for (let i = 0; i < rows; i++) {
const row = document.createElement('tr');
for (let j = 0; j < cols; j++) {
const cell = document.createElement('td');
cell.innerHTML = '&nbsp;';
if (hasBorders) {
cell.style.border = "1px solid #999";
tplTable.style.borderCollapse = "collapse";
}
row.appendChild(cell);
}
tplTable.appendChild(row);
}
// Insérer le tableau à la position du curseur
range.deleteContents();
range.insertNode(tplTable);
}
/*MANAGE COLORS */
showColorPalette() {
this.saveSelection();
this.colorPalette = this.el.querySelector('.he-colorPalette');
this.buttonRect = null;
const colorTextButton = this.el.querySelector('.he-colorText');
const isPaletteVisible = window.getComputedStyle(this.colorPalette).display !== "none";
if (!isPaletteVisible) {
this.buttonRect = colorTextButton.getBoundingClientRect();
this.colorPalette.style.display = "flex";
this.colorPalette.style.gridTemplateColumns = "repeat(auto-fill, minmax(50px, 1fr))";
this.colorPalette.style.position = 'fixed';
this.colorPalette.style.zIndex = '999';
this.colorPalette.style.top = `${this.buttonRect.bottom + window.scrollY}px`;
// Vérifie si le sélecteur dépasse la largeur de l'écran
const paletteWidth = this.colorPalette.offsetWidth;
const windowWidth = window.innerWidth;
const defaultLeft = this.buttonRect.left + window.scrollX;
if (defaultLeft + paletteWidth > windowWidth) {
// Ouvrir vers la gauche
this.colorPalette.style.left = `${defaultLeft - paletteWidth}px`;
} else {
// Ouvrir vers la droite (comportement par défaut)
this.colorPalette.style.left = `${defaultLeft}px`;
}
colorTextButton.classList.remove("icon-format-color");
colorTextButton.classList.add("icon-cancel");
colorTextButton.setAttribute("title", "close");
} else {
this.closeColorPalette();
}
}
closeColorPalette() {
this.colorPalette.style.display = "none";
const colorTextButton = this.el.querySelector('.he-colorText');
colorTextButton.classList.remove("icon-cancel");
colorTextButton.classList.add("icon-format-color");
colorTextButton.setAttribute("title", "color text");
this.buttonRect = null;
return
}
applyColorToText(color) {
this.restoreSelection();
if (!this.savedRange) {
console.error('No selection saved to apply color');
return;
}
const selection = window.getSelection();
if (!selection.rangeCount) {
console.error('No range found in selection');
return;
}
const range = selection.getRangeAt(0);
const selectedTextContent = range.extractContents();
const spanColor = document.createElement('span');
spanColor.style.color = color;
spanColor.appendChild(selectedTextContent);
if (range) {
range.deleteContents();
range.insertNode(this.spaceBefore);
range.insertNode(spanColor);
range.insertNode(this.spaceAfter);
}
window.getSelection().removeAllRanges();
this.closeColorPalette();
}
/*MANAGE IMAGES SELECTOR*/
async showImageSelector() {
this.saveSelection()
this.imgSelectContainer = this.el.querySelector('.he-imgSelectContainer')
this.selectedImageUrl = "";
this.linkInput = this.el.querySelector('.he-imgUrl')
const addImageButton = this.el.querySelector('.he-addImg')
const isImgContainerVisible = window.getComputedStyle(this.imgSelectContainer).display !== "none"
if (!isImgContainerVisible) {
this.selectedImageUrl = ""
// Always reload images before showing the selector
if (typeof this.context.templates?.getImages === 'function') {
this.showLoading(addImageButton)
this.images = await this.context.templates.getImages(this.context.templateId)
}
await this.generateImageSelector()
this.imgSelectContainer.style.display = "block"
this.imgSelectContainer.style.position = 'fixed'
this.imgSelectContainer.style.border = '1px solid #ccc'
this.imgSelectContainer.style.width = '20%'
this.imgSelectContainer.style.padding = '10px'
this.imgSelectContainer.style.background = '#fff'
this.imgSelectContainer.style.zIndex = '999'
this.imgSelectContainer.style.visibility = 'hidden'
const containerWidth = this.imgSelectContainer.offsetWidth
const buttonRect = addImageButton.getBoundingClientRect()
const topPosition = buttonRect.bottom + window.scrollY
const leftPosition = buttonRect.left + window.scrollX - containerWidth
this.imgSelectContainer.style.top = `${topPosition}px`
this.imgSelectContainer.style.left = `${leftPosition}px`
this.imgSelectContainer.style.visibility = 'visible'
addImageButton.classList.remove("icon-image")
addImageButton.classList.add("icon-cancel")
addImageButton.setAttribute("title", "close image selector")
this.hideLoading(addImageButton, '')
} else {
this.closeImageSelector();
}
}
closeImageSelector() {
this.imgSelectContainer.style.display = "none";
const addImgButton = this.el.querySelector('.he-addImg');
if (addImgButton) {
addImgButton.classList.remove("icon-cancel");
addImgButton.classList.add("icon-image");
addImgButton.setAttribute("title", "Add a picture");
}
}
handleImageClick() {
this.restoreSelection();
if (!this.selectedImageUrl) {
document.innerHTML = `<div eicalert danger>Selected an image</div>`;
return;
}
this.insertImage(this.selectedImageUrl, this.selectedImageTitle, this.selectedImageId);
this.closeImageSelector();
}
insertImage(url, title, id) {
let selection = window.getSelection();
if (!selection.rangeCount) {
console.warn("No valid selection found.");
return;
}
let range = selection.getRangeAt(0);
let selectedNode = range.startContainer;
let parentImage = selectedNode.nodeType === Node.ELEMENT_NODE &&
(selectedNode.tagName === "IMG" || selectedNode.closest('.tpl-resizer'));
if (parentImage) {
range = document.createRange();
range.setStartAfter(selectedNode.closest('.tpl-resizer'));
range.setEndAfter(selectedNode.closest('.tpl-resizer'));
selection.removeAllRanges();
selection.addRange(range);
}
if (range.startContainer.nodeType === Node.TEXT_NODE) {
range.setStartAfter(range.startContainer);
range.setEndAfter(range.startContainer);
}
if (range.startContainer.nodeType === Node.ELEMENT_NODE &&
range.startContainer.querySelector('.tpl-resizer')) {
range.setStartAfter(range.startContainer.querySelector('.tpl-resizer'));
}
const container = document.createElement('div');
container.className = 'tpl-resizer';
const imgElement = document.createElement('img');
imgElement.src = url;
imgElement.id = id;
imgElement.title = title;
imgElement.alt = title;
imgElement.onload = () => {
const naturalWidth = imgElement.naturalWidth;
const naturalHeight = imgElement.naturalHeight;
container.style.maxWidth = `${naturalWidth}px`;
container.style.maxHeight = `${naturalHeight}px`;
imgElement.style.width = '100%';
imgElement.style.height = 'auto';
};
container.appendChild(imgElement);
const spaceAfter = document.createTextNode("\u00A0");
this.linkValue = this.linkInput.value.trim();
if (this.linkValue) {
// Vérification si le lien est une adresse e-mail
const isEmail = this.linkValue.startsWith('mailto:') || /^[\w.-]+@[\w.-]+\.\w+$/.test(this.linkValue);
if (!this.linkValue.startsWith('http://') && !this.linkValue.startsWith('https://') && !isEmail) {
this.linkValue = 'http://' + this.linkValue;
} else if (/^[\w.-]+@[\w.-]+\.\w+$/.test(this.linkValue)) {
this.linkValue = 'mailto:' + this.linkValue;
}
if (!this.validateURL(this.linkValue) && !isEmail) {
ui.growl.append('Invalid URL or email address', 'error');
return;
}
const linkElement = document.createElement('a');
linkElement.href = this.linkValue;
linkElement.target = isEmail ? "_self" : "_blank"; // Ouvrir les emails dans la même fenêtre
linkElement.appendChild(container);
range.insertNode(linkElement);
range.insertNode(spaceAfter);
} else {
range.insertNode(container);
range.insertNode(spaceAfter);
}
const newRange = document.createRange();
newRange.setStartAfter(spaceAfter);
newRange.setEndAfter(spaceAfter);
selection.removeAllRanges();
selection.addRange(newRange);
document.dispatchEvent(new Event('insertImage'));
}
async generateImageSelector() {
this.imgSelectContainer.innerHTML = "";
// Preloaded Images Selector
const imgSelect = document.createElement("div");
imgSelect.className = "he-imgSelect";
imgSelect.style.display = "flex";
imgSelect.style.flexDirection = "column";
imgSelect.style.maxHeight = "10em";
imgSelect.style.overflowY = "auto";
this.images = await this.images;
const host = window.location.hostname;
let envUrl = (host.split('.')[1] !== 'eismea') ? host.split('.')[1] + '.' : '';
this.images.forEach(image => {
const genUrl = `https://myeic.${envUrl}eismea.eu/public/images${image.object.path}/${image.object.name}`;
const imgOption = document.createElement("div");
imgOption.className = "he-imgItem";
imgOption.style.display = "flex";
imgOption.style.alignItems = "center";
imgOption.style.padding = "5px";
imgOption.style.cursor = "pointer";
imgOption.dataset.url = genUrl;
imgOption.dataset.id = image.object.name.replace(/\.[^/.]+$/, "");
imgOption.dataset.title = image.object.title;
const imgThumbnail = document.createElement("img");
imgThumbnail.src = genUrl;
imgThumbnail.alt = image.object.title;
imgThumbnail.style.width = "100px";
imgThumbnail.style.height = "auto";
imgThumbnail.style.marginRight = "10px";
const imgTitle = document.createElement("span");
imgTitle.textContent = image.object.title;
imgTitle.style.flexGrow = "1";
const deleteButton = document.createElement("button");
deleteButton.setAttribute("eicbutton", "");
deleteButton.className = "icon-cancel delete-img";
deleteButton.setAttribute("xxsmall", "");
deleteButton.setAttribute("rounded", "");
deleteButton.setAttribute("danger", "");
deleteButton.setAttribute("type", "button");
deleteButton.dataset.id = genUrl;
deleteButton.dataset.trigger = "onDeleteImage";
imgOption.appendChild(imgThumbnail);
imgOption.appendChild(imgTitle);
imgOption.appendChild(deleteButton);
imgOption.addEventListener("click", () => {
this.selectedImageUrl = imgOption.dataset.url;
this.selectedImageTitle = imgOption.dataset.title;
this.selectedImageId = imgOption.dataset.id;
this.highlightSelectedImage(imgOption);
});
deleteButton.addEventListener("click", (event) => {
event.stopPropagation();
this.onDeleteImage(image.object.id, image.object.name, image.object.title);
});
imgSelect.appendChild(imgOption);
});
this.imgSelectContainer.appendChild(imgSelect);
this.addUrlInputAndButton();
}
// Delete from MySQL database and s3 bucket
async onDeleteImage(imgId, imgName, imgTitle) {
const host = window.location.hostname
let envUrl = (host.split('.')[1] !== 'eismea') ? host.split('.')[1] + '.' : ''
let deleteUrl = `https://myeic.${envUrl}eismea.eu/public/images/common/${imgName}`
let result = await this.context.confirmDialog({
title: 'DELETE template ?',
message: `<p>You are about to permanently delete the image "<b>${imgTitle}</b>".</p>
<p>Are you sure?</p>`,
okLabel: 'Delete',
severity: 'danger',
okPromise: async () => {
await this.context.templates.deleteImage(imgId, deleteUrl);
}
});
if(result) {
ui.growl.append("Image deleted !", 'success')
// Hide the image item in the selector
const imgItem = this.el.querySelector(`.he-imgItem[data-id="${imgId}"]`)
if (imgItem) imgItem.style.display = 'none'
} else {
ui.growl.append("Error deleting Image!", 'error')
}
}
highlightSelectedImage(selectedOption) {
const imgItems = this.imgSelectContainer.querySelectorAll('.he-imgItem');
imgItems.forEach(item => {
item.style.backgroundColor = "";
});
selectedOption.style.backgroundColor = "#e0e0e0";
}
addUrlInputAndButton() {
const imgUrlLabel = document.createElement('h5');
imgUrlLabel.setAttribute('for', 'he-imgUrl');
imgUrlLabel.setAttribute('xsmall', '');
imgUrlLabel.setAttribute('primary', '');
imgUrlLabel.textContent = "Optional - Add a link to the image:";
const imgUrlInput = document.createElement('input');
imgUrlInput.setAttribute('eicinput','');
imgUrlInput.setAttribute('small', '');
imgUrlInput.id = 'he-imgUrl';
imgUrlInput.name = 'he-imgUrl';
imgUrlInput.placeholder = 'Enter url or em@il (optional)';
this.linkInput = imgUrlInput;
const addButton = document.createElement('button');
addButton.setAttribute('eicbutton', '');
addButton.setAttribute('xsmall', '');
addButton.setAttribute('primary', '');
addButton.className = 'tool-button he-confirmAddImg';
addButton.textContent = 'Add image';
addButton.addEventListener('click', () => this.handleImageClick());
this.imgSelectContainer.appendChild(imgUrlLabel);
this.imgSelectContainer.appendChild(imgUrlInput);
this.imgSelectContainer.appendChild(addButton);
}
/*MANAGE LINKS */
showLinkInput() {
this.saveSelection();
this.linkContainer = this.el.querySelector('.he-linkContainer');
this.linkUrlInput = this.el.querySelector('.he-linkUrl');
this.linkTextInput = this.el.querySelector('.he-linkText');
this.selectedText = '';
this.selectedRange = null;
this.addLinkButton = this.el.querySelector('.he-addLink');
const selection = window.getSelection();
const isLinkContainerVisible = window.getComputedStyle(this.linkContainer).display !== "none";
if (!isLinkContainerVisible) {
if (selection.rangeCount > 0 && !selection.isCollapsed) {
// Text is selected
this.selectedRange = selection.getRangeAt(0);
this.selectedText = this.selectedRange.toString();
this.linkTextInput.style.display = 'none'; // Hide the text input since text is already selected
} else {
// No text selected, show input for link text
this.selectedText = '';
this.linkTextInput.style.display = 'block'; // Show the input to enter the link text
}
const buttonRect = this.addLinkButton.getBoundingClientRect();
this.linkContainer.style.display = 'block';
this.linkContainer.style.position = 'fixed';
this.linkContainer.style.border = '1px solid #ccc';
this.linkContainer.style.padding = '10px';
this.linkContainer.style.background = '#fff';
this.linkContainer.style.zIndex = 1000;
// Div on left side of button
const containerWidth = this.linkContainer.offsetWidth;
this.linkContainer.style.top = `${buttonRect.bottom + window.scrollY}px`;
this.linkContainer.style.left = `${buttonRect.left + window.scrollX - containerWidth}px`;
this.linkUrlInput.value = '';
this.linkTextInput.value = ''; // Clear the text input in case it was used before
this.linkUrlInput.focus();
this.addLinkButton.classList.remove("icon-link");
this.addLinkButton.classList.add("icon-cancel");
this.addLinkButton.setAttribute("title", "close add link");
window.addEventListener('scroll', this.updateLinkContainerPosition.bind(this));
} else {
this.closeLinkInput();
}
}
updateLinkContainerPosition() {
const buttonRect = this.addLinkButton.getBoundingClientRect();
const containerWidth = this.linkContainer.offsetWidth;
this.linkContainer.style.top = `${buttonRect.bottom + window.scrollY}px`;
this.linkContainer.style.left = `${buttonRect.left + window.scrollX - containerWidth}px`;
}
validateURL(url) {
const pattern = new RegExp('^(https?:\\/\\/)?' +
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' +
'((\\d{1,3}\\.){3}\\d{1,3}))' +
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' +
'(\\?[;&a-z\\d%_.~+=-]*)?' +
'(\\#[-a-z\\d_]*)?$', 'i');
return !!pattern.test(url);
}
createLink() {
this.restoreSelection();
this.linkUrlInput = this.el.querySelector('.he-linkUrl');
let url = this.linkUrlInput.value.trim();
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const text = this.selectedText || this.linkTextInput.value.trim(); // Use selected text
if (!url || !text) {
ui.growl.append('Both URL and link text are required to create a link', 'error');
return;
}
// Check if mail adress (mailto:)
const isEmail = url.startsWith('mailto:') || /^[\w.-]+@[\w.-]+\.\w+$/.test(url);
// Add "mailto:" if e-mail without mailto
if (!url.startsWith('http://') && !url.startsWith('https://') && !isEmail) {
url = 'http://' + url;
} else if (/^[\w.-]+@[\w.-]+\.\w+$/.test(url)) {
url = 'mailto:' + url;
}
// Check URL
if (!this.validateURL(url) && !isEmail) {
ui.growl.append('Invalid URL or email address', 'error');
return;
}
if (!this.isInsertionAllowed(range)) {
ui.growl.append('Select a valid location', 'error');
return;
}
const linkElement = document.createElement('a');
linkElement.href = url;
linkElement.target = isEmail ? '_self' : '_blank'; // mailto: open in same page
linkElement.title = url;
linkElement.alt = url;
linkElement.textContent = text;
if (this.savedRange) {
this.savedRange.deleteContents();
this.savedRange.insertNode(linkElement);
} else {
// Insérer à la position du curseur
const range = window.getSelection().getRangeAt(0);
range.insertNode(this.spaceBefore);
range.insertNode(linkElement);
range.insertNode(this.spaceAfter);
}
window.getSelection().removeAllRanges();
this.closeLinkInput();
}
closeLinkInput() {
this.linkContainer.style.display = 'none';
this.addLinkButton.classList.remove("icon-cancel");
this.addLinkButton.classList.add("icon-link");
this.addLinkButton.setAttribute("title", "add link");
}
/*MANAGE LIST */
showListSelector() {
this.saveSelection();
this.listContainer = this.el.querySelector('.he-addListContainer');
this.listItemsInput = this.el.querySelector('.he-listItems');
const addListButton = this.el.querySelector('.he-addList');
const isListContainerVisible = window.getComputedStyle(this.listContainer).display !== "none";
// Reset the input value when opening
this.listItemsInput.value = '';
if (!isListContainerVisible) {
const buttonRect = addListButton.getBoundingClientRect();
// Display the list selector under the button
this.listContainer.style.display = "block";
this.listContainer.style.position = 'fixed';
this.listContainer.style.border = '1px solid #ccc';
this.listContainer.style.padding = '10px';
this.listContainer.style.background = '#fff';
const containerWidth = this.listContainer.offsetWidth;
this.listContainer.style.top = `${buttonRect.bottom + window.scrollY}px`;
this.listContainer.style.left = `${buttonRect.left + window.scrollX - containerWidth}px`;
// Update button to "cancel" mode
addListButton.classList.remove("icon-list-ul");
addListButton.classList.add("icon-cancel");
addListButton.setAttribute("title", "close");
} else {
this.closeListSelector();
}
}
closeListSelector() {
const listSelectContainer = this.listContainer;
const addListButton = this.el.querySelector('.he-addList');
// Hide the list selector
listSelectContainer.style.display = "none";
// Reset the button back to "add list" mode
addListButton.classList.remove("icon-cancel");
addListButton.classList.add("icon-list-ul");
addListButton.setAttribute("title", "add list items");
// Reset the button rect
this.buttonRect = null;
}
confirmList() {
this.restoreSelection();
const listItemsTextarea = this.el.querySelector('.he-listItems');
const listItems = listItemsTextarea.value.trim().split('\n');
if (listItems.length === 0 || (listItems.length === 1 && listItems[0] === '')) {
alert("Please enter at least one item for the list.");
return;
}
let listType;
if (this.el.querySelector('.he-listTypeBullet').checked) {
listType = 'bullet';
} else if (this.el.querySelector('.he-listTypeDash').checked) {
listType = 'dash';
} else if (this.el.querySelector('.he-listTypeNumbered').checked) {
listType = 'numbered';
}
this.insertList(listItems, listType);
listItemsTextarea.value = '';
this.closeListSelector();
}
getListType() {
const bulletType = this.listContainer.querySelector('.he-listTypeBullet').checked;
const dashType = this.listContainer.querySelector('.he-listTypeDash').checked;
const numberedType = this.listContainer.querySelector('.he-listTypeNumbered').checked;
if (bulletType) return 'bullet';
if (dashType) return 'dash';
if (numberedType) return 'numbered';
return 'bullet';
}
insertList(items, type) {
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
if (!this.isInsertionAllowed(range)) {
ui.growl.append('select a valid location', 'error');
return;
}
// Check if insert in authorized fields
let editor = range.commonAncestorContainer;
while (editor && editor.nodeType !== Node.ELEMENT_NODE) {
editor = editor.parentNode;
}
if (!editor || (!editor.classList.contains('template-field.main'))) {
editor = this.el.querySelector('.template-field.main');
}
// Create list elements
let heList;
if (type === 'bullet' || type === 'dash') {
heList = document.createElement('ul');
if (type === 'dash') {
heList.style.listStyleType = 'none';
}
} else if (type === 'numbered') {
heList = document.createElement('ol');
}
heList.style.marginLeft = '1em';
heList.style.paddingLeft = '1em';
// Add items
items.forEach(item => {
const li = document.createElement('li');
if (type === 'dash') {
li.textContent = `- ${item}`;
} else {
li.textContent = item;
}
heList.appendChild(li);
});
// Insert list at cursor position
if (range) {
range.deleteContents();
range.insertNode(heList);
} else {
editor.appendChild(heList);
}
}
/*MANAGE TOKENS */
showTokenInput() {
this.saveSelection();
this.tokenTextInput = this.el.querySelector('.he-tokenText');
this.tokenSelect = this.el.querySelector('.he-tokenSelect');
this.tokenContainer = this.el.querySelector('.he-tokenContainer');
this.addTokenButton = this.el.querySelector('.he-addToken');
this.selectedText = '';
this.savedRange = null;
const selection = window.getSelection();
const isTokenContainerVisible = window.getComputedStyle(this.tokenContainer).display !== "none";
if (!isTokenContainerVisible) {
if (selection.rangeCount > 0 && !selection.isCollapsed) {
// Selected text
this.savedRange = selection.getRangeAt(0);
this.selectedText = this.savedRange.toString().trim();
this.tokenTextInput.style.display = 'none';
this.tokenSelect.style.display = 'none';
} else {
this.populateTokenSelect();
this.selectedText = '';
this.savedRange = selection.getRangeAt(0);
this.tokenTextInput.style.display = 'block';
this.tokenSelect.style.display = 'block';
}
const buttonRect = this.addTokenButton.getBoundingClientRect();
this.tokenContainer.style.display = 'block';
this.tokenContainer.style.position = 'fixed';
this.tokenContainer.style.border = '1px solid #ccc';
this.tokenContainer.style.padding = '10px';
this.tokenContainer.style.background = '#fff';
this.tokenContainer.style.zIndex = 1000;
const containerWidth = this.tokenContainer.offsetWidth;
this.tokenContainer.style.top = `${buttonRect.bottom + window.scrollY}px`;
this.tokenContainer.style.left = `${buttonRect.left + window.scrollX - containerWidth}px`;
this.tokenTextInput.value = '';
this.tokenTextInput.focus();
this.addTokenButton.classList.remove("icon-loop");
this.addTokenButton.classList.add("icon-cancel");
this.addTokenButton.setAttribute("title", "close Add variable");
} else {
this.closeTokenInput();
}
}
async populateTokenSelect() {
this.tokenSelect.innerHTML = "";
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.text = 'Choose a replacement variable';
this.tokenSelect.appendChild(defaultOption);
// Add tokens list
const tokens = await this.tokens;
tokens.forEach(token => {
const option = document.createElement('option');
option.value = token.name;
option.text = token.name;
this.tokenSelect.appendChild(option);
});
}
createToken() {
this.restoreSelection()
document.querySelectorAll('.caret-holder').forEach(el => el.remove())
let tokenText = this.selectedText || this.tokenTextInput.value.trim()
const selection = window.getSelection()
if (selection.rangeCount === 0) return
const range = selection.getRangeAt(0)
const selectedToken = this.el.querySelector('.he-tokenSelect').value
if (!tokenText && selectedToken) {
tokenText = selectedToken
}
if (!tokenText) {
ui.growl.append('Required text for variables', 'error')
return
}
if (!this.isInsertionAllowed(range)) {
ui.growl.append('select a valid location', 'error')
return
}
let parentTemplateField = null
if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
parentTemplateField = range.startContainer.closest('.template-field')
} else if (range.startContainer.nodeType === Node.TEXT_NODE) {
parentTemplateField = range.startContainer.parentNode?.closest('.template-field')
}
if (!parentTemplateField) {
ui.growl.append('Select a valid location !', 'error')
return
}
const tokenElement = document.createElement('span')
tokenElement.classList.add('tpl-token')
tokenElement.setAttribute('eicchip', '')
tokenElement.setAttribute('success', '')
tokenElement.setAttribute('data-original', tokenText)
tokenElement.textContent = tokenText
const caretHolder = document.createElement('span')
caretHolder.className = 'caret-holder'
caretHolder.contentEditable = 'true'
caretHolder.innerHTML = '&#8203;'
const spaceBefore = document.createTextNode('\u00A0') // &nbsp;
const spaceAfter = document.createTextNode('\u00A0') // &nbsp.
if (range) {
const fragment = document.createDocumentFragment()
fragment.appendChild(spaceBefore)
fragment.appendChild(tokenElement)
fragment.appendChild(spaceAfter)
fragment.appendChild(caretHolder)
range.deleteContents()
range.insertNode(fragment)
}
this.tokenInserted = true
this.tokenSelect.value = ''
this.closeTokenInput()
}
closeTokenInput() {
this.tokenContainer.style.display = 'none'
this.addTokenButton.classList.remove("icon-cancel")
this.addTokenButton.classList.add("icon-loop")
this.addTokenButton.setAttribute("title", "Add content variable")
if (this.tokenInserted) {
const caretHolder = document.querySelector('.caret-holder')
if (caretHolder) {
const range = document.createRange()
range.setStart(caretHolder, 1)
range.collapse(true)
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
}
} else {
this.restoreSelection()
}
this.tokenInserted = false;
}
// Add style into document head
injectStyles() {
const style = document.createElement('style');
style.innerHTML = `
.resize-container {
position: relative;
display: inline-block;
}
.tpl-resizer {
resize: both;
overflow: hidden;
display: inline-block;
max-width: 100%;
}
.he-imgItem.selected {
border: 2px solid blue;
background-color: lightgray;
}
.he-imgItem {
transition: background-color 0.3s ease;
}
.he-imgItem:hover {
background-color: #f0f0f0;
}
.he-color-palette {
display: grid;
grid-template-columns: repeat(3, 1.5em);
gap: 0.5em;
position: fixed;
border: 1px solid #ccc;
padding: 2px;
background-color: #fff;
z-index: 1000;
}
.he-colorOption {
width: 1.5em;
height: 1.5em;
cursor: pointer;
pointer-events: auto;
}
.he-colorOption:hover {
border: 2px solid #000;
}
.tpl-table {
border-collapse: collapse;
}
tpl-table td {
border: 1px solid #ccc;
padding: 10px;
position: relative;
}
.he-listItems{
min-height: 100px;
}
`;
document.head.appendChild(style);
}
}