1583 lines
69 KiB
JavaScript
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 = ' ';
|
|
|
|
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 = '​'
|
|
|
|
const spaceBefore = document.createTextNode('\u00A0') //
|
|
const spaceAfter = document.createTextNode('\u00A0') //  .
|
|
|
|
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);
|
|
}
|
|
} |