Web editor refactoring and some calendar occurrence summary stuff.
This commit is contained in:
+697
-71
@@ -8,6 +8,7 @@ const joditConfig = {
|
||||
"showCharsCounter": false,
|
||||
"showWordsCounter": false,
|
||||
"showXPathInStatusbar": false,
|
||||
"spellcheck": true,
|
||||
"link": {
|
||||
"processVideoLink": false
|
||||
},
|
||||
@@ -17,92 +18,78 @@ const joditConfig = {
|
||||
"insertImageAsBase64URI": true
|
||||
},
|
||||
"enter": "DIV"
|
||||
}
|
||||
};
|
||||
|
||||
let editor;
|
||||
let editorDomObserver;
|
||||
let selectionChangeHandler;
|
||||
let stateSyncQueued = false;
|
||||
let imageInputBound = false;
|
||||
let inlineFontsPluginRegistered = false;
|
||||
let lastKnownRange = null;
|
||||
|
||||
// This method should be called first all the time.
|
||||
function initializeJodit(fonts, defaultComposerFont, defaultComposerFontSize, defaultReaderFont, defaultReaderFontSize) {
|
||||
const fontsWithFallabckObject = fonts.reduce((acc, font) => { acc[`'${font}',Arial,sans-serif`] = font; return acc; }, {});
|
||||
if (editor) {
|
||||
scheduleStateSync();
|
||||
return true;
|
||||
}
|
||||
|
||||
const fontsWithFallbackObject = fonts.reduce((acc, font) => {
|
||||
acc[`'${font}',Arial,sans-serif`] = font;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const mergedConfig = {
|
||||
...joditConfig,
|
||||
controls: {
|
||||
font: {
|
||||
list: Jodit.atom(fontsWithFallabckObject)
|
||||
list: Jodit.atom(fontsWithFallbackObject)
|
||||
}
|
||||
},
|
||||
style: { font: `${defaultReaderFontSize}px ${defaultReaderFont}` },
|
||||
}
|
||||
style: { font: `${defaultReaderFontSize}px ${defaultReaderFont}` }
|
||||
};
|
||||
|
||||
Jodit.plugins.add('inlineFonts', jodit => {
|
||||
jodit.events.on('afterEnter', e => {
|
||||
const current = jodit.selection.current().parentNode;
|
||||
current.style.fontFamily = `'${defaultComposerFont}',Arial,sans-serif`;
|
||||
current.style.fontSize = `${defaultComposerFontSize}px`;
|
||||
if (!inlineFontsPluginRegistered) {
|
||||
Jodit.plugins.add('inlineFonts', jodit => {
|
||||
jodit.events.on('afterEnter', () => {
|
||||
const current = getSelectionElement();
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
current.style.fontFamily = `'${defaultComposerFont}',Arial,sans-serif`;
|
||||
current.style.fontSize = `${defaultComposerFontSize}px`;
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Don't add const/let/var here, it should be global
|
||||
editor = Jodit.make("#editor", mergedConfig);
|
||||
|
||||
// Handle the image input change event
|
||||
imageInput.addEventListener('change', () => {
|
||||
const file = imageInput.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
const base64Image = event.target.result;
|
||||
insertImages([{ data: base64Image, name: file.name }]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Listeners for button events
|
||||
const disabledButtons = ["indent", "outdent"];
|
||||
const ariaPressedButtons = ["bold", "italic", "underline", "strikethrough", "ul", "ol"];
|
||||
|
||||
const alignmentButton = document.querySelector(`[ref='left']`).firstChild.firstChild;
|
||||
const alignmentObserver = new MutationObserver(function () {
|
||||
const value = alignmentButton.firstChild.getAttribute('class').split(' ')[0];
|
||||
window.chrome.webview.postMessage({ type: 'alignment', value: value });
|
||||
});
|
||||
alignmentObserver.observe(alignmentButton, { childList: true, attributes: true, attributeFilter: ["class"] });
|
||||
|
||||
const ariaObservers = ariaPressedButtons.map(button => {
|
||||
const buttonContainer = document.querySelector(`[ref='${button}']`);
|
||||
const observer = new MutationObserver(function () { pressedChanged(buttonContainer) });
|
||||
observer.observe(buttonContainer.firstChild, { attributes: true, attributeFilter: ["aria-pressed"] });
|
||||
|
||||
return observer;
|
||||
});
|
||||
|
||||
const disabledObservers = disabledButtons.map(button => {
|
||||
const buttonContainer = document.querySelector(`[ref='${button}']`);
|
||||
const observer = new MutationObserver(function () { disabledButtonChanged(buttonContainer) });
|
||||
observer.observe(buttonContainer.firstChild, { attributes: true, attributeFilter: ["disabled"] });
|
||||
|
||||
return observer;
|
||||
});
|
||||
|
||||
function pressedChanged(buttonContainer) {
|
||||
const ref = buttonContainer.getAttribute('ref');
|
||||
const value = buttonContainer.firstChild.getAttribute('aria-pressed');
|
||||
window.chrome.webview.postMessage({ type: ref, value: value });
|
||||
inlineFontsPluginRegistered = true;
|
||||
}
|
||||
|
||||
function disabledButtonChanged(buttonContainer) {
|
||||
const ref = buttonContainer.getAttribute('ref');
|
||||
const value = buttonContainer.firstChild.getAttribute('disabled');
|
||||
window.chrome.webview.postMessage({ type: ref, value: value });
|
||||
}
|
||||
editor = Jodit.make('#editor', mergedConfig);
|
||||
|
||||
bindImageInput();
|
||||
bindEditorStateTracking();
|
||||
toggleToolbar(false);
|
||||
scheduleStateSync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function RenderHTML(htmlString) {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.value = htmlString;
|
||||
editor.synchronizeValues();
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function GetHTMLContent() {
|
||||
return editor.value;
|
||||
return editor ? editor.value : '';
|
||||
}
|
||||
|
||||
function SetLightEditor() {
|
||||
@@ -115,16 +102,655 @@ function SetDarkEditor() {
|
||||
|
||||
function toggleToolbar(enable) {
|
||||
const toolbar = document.querySelector('.jodit-toolbar__box');
|
||||
if (enable) {
|
||||
toolbar.style.display = 'flex';
|
||||
if (toolbar) {
|
||||
toolbar.style.display = enable ? 'flex' : 'none';
|
||||
}
|
||||
else {
|
||||
toolbar.style.display = 'none';
|
||||
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function setSpellCheck(enable) {
|
||||
if (!editor || !editor.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isEnabled = !!enable;
|
||||
editor.options.spellcheck = isEnabled;
|
||||
editor.editor.spellcheck = isEnabled;
|
||||
editor.editor.setAttribute('spellcheck', isEnabled ? 'true' : 'false');
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function insertImages(imagesInfo) {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
restoreEditorSelection();
|
||||
|
||||
imagesInfo.forEach(imageInfo => {
|
||||
editor.selection.insertHTML(`<img src="${imageInfo.data}" alt="${imageInfo.name}">`);
|
||||
editor.selection.insertHTML(`<img src="${escapeHtmlAttribute(imageInfo.data)}" alt="${escapeHtmlAttribute(imageInfo.name)}">`);
|
||||
});
|
||||
};
|
||||
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function focusEditor() {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (restoreEditorSelection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.selection.focus();
|
||||
|
||||
const lastChild = editor.editor.lastChild;
|
||||
if (lastChild) {
|
||||
editor.selection.setCursorIn(lastChild, false);
|
||||
}
|
||||
}
|
||||
|
||||
function getEditorState() {
|
||||
return buildEditorState();
|
||||
}
|
||||
|
||||
function executeEditorCommand(commandName) {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
restoreEditorSelection();
|
||||
editor.execCommand(commandName);
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function setFontFamily(fontFamily) {
|
||||
applyInlineStyleToSelection({ fontFamily: `'${fontFamily}',Arial,sans-serif` });
|
||||
}
|
||||
|
||||
function setFontSize(fontSize) {
|
||||
applyInlineStyleToSelection({ fontSize: `${fontSize}px` });
|
||||
}
|
||||
|
||||
function setTextColor(color) {
|
||||
applyInlineStyleToSelection({ color: color || '' });
|
||||
}
|
||||
|
||||
function setHighlightColor(color) {
|
||||
applyInlineStyleToSelection({ backgroundColor: color || '' });
|
||||
}
|
||||
|
||||
function setParagraphStyle(tagName) {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
restoreEditorSelection();
|
||||
|
||||
const normalizedTag = (tagName || 'div').toLowerCase();
|
||||
|
||||
try {
|
||||
document.execCommand('formatBlock', false, normalizedTag);
|
||||
}
|
||||
catch {
|
||||
const block = getCurrentBlockElement();
|
||||
if (block && block.tagName.toLowerCase() !== normalizedTag) {
|
||||
const replacement = document.createElement(normalizedTag);
|
||||
while (block.firstChild) {
|
||||
replacement.appendChild(block.firstChild);
|
||||
}
|
||||
|
||||
block.parentNode.replaceChild(replacement, block);
|
||||
}
|
||||
}
|
||||
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function setLineHeight(lineHeight) {
|
||||
restoreEditorSelection();
|
||||
|
||||
const block = getCurrentBlockElement();
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
|
||||
block.style.lineHeight = lineHeight || '';
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function upsertLink(linkArgs) {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
restoreEditorSelection();
|
||||
|
||||
const normalizedUrl = normalizeLinkUrl(linkArgs && linkArgs.url ? linkArgs.url : '');
|
||||
if (!normalizedUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkText = linkArgs && linkArgs.text ? linkArgs.text.trim() : '';
|
||||
const existingLink = getSelectionElement() ? getSelectionElement().closest('a[href]') : null;
|
||||
|
||||
if (existingLink) {
|
||||
existingLink.setAttribute('href', normalizedUrl);
|
||||
if (linkArgs.openInNewWindow) {
|
||||
existingLink.setAttribute('target', '_blank');
|
||||
existingLink.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
else {
|
||||
existingLink.removeAttribute('target');
|
||||
existingLink.removeAttribute('rel');
|
||||
}
|
||||
|
||||
if (linkText) {
|
||||
existingLink.textContent = linkText;
|
||||
}
|
||||
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0 && !selection.isCollapsed && isSelectionInsideEditor()) {
|
||||
try {
|
||||
document.execCommand('createLink', false, normalizedUrl);
|
||||
const createdLink = getSelectionElement() ? getSelectionElement().closest('a[href]') : null;
|
||||
if (createdLink) {
|
||||
if (linkArgs.openInNewWindow) {
|
||||
createdLink.setAttribute('target', '_blank');
|
||||
createdLink.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
if (linkText) {
|
||||
createdLink.textContent = linkText;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
const selectedText = linkText || selection.toString() || normalizedUrl;
|
||||
editor.selection.insertHTML(`<a href="${escapeHtmlAttribute(normalizedUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtmlText(selectedText)}</a>`);
|
||||
}
|
||||
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
return;
|
||||
}
|
||||
|
||||
const text = linkText || normalizedUrl;
|
||||
editor.selection.insertHTML(`<a href="${escapeHtmlAttribute(normalizedUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtmlText(text)}</a>`);
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function removeLink() {
|
||||
restoreEditorSelection();
|
||||
|
||||
const selectionElement = getSelectionElement();
|
||||
const linkElement = selectionElement ? selectionElement.closest('a[href]') : null;
|
||||
if (!linkElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
document.execCommand('unlink');
|
||||
}
|
||||
catch {
|
||||
unwrapElement(linkElement);
|
||||
}
|
||||
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function insertTableHtml(tableArgs) {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
restoreEditorSelection();
|
||||
|
||||
const rows = clampInteger(tableArgs && tableArgs.rows, 1, 10);
|
||||
const columns = clampInteger(tableArgs && tableArgs.columns, 1, 10);
|
||||
const htmlRows = [];
|
||||
|
||||
for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) {
|
||||
const cells = [];
|
||||
for (let columnIndex = 0; columnIndex < columns; columnIndex += 1) {
|
||||
cells.push('<td style="border:1px solid #c7c7c7;padding:6px;min-width:32px;"><br></td>');
|
||||
}
|
||||
|
||||
htmlRows.push(`<tr>${cells.join('')}</tr>`);
|
||||
}
|
||||
|
||||
editor.selection.insertHTML(`<table style="border-collapse:collapse;width:100%;">${htmlRows.join('')}</table><div><br></div>`);
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function bindImageInput() {
|
||||
if (imageInputBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageInput.addEventListener('change', () => {
|
||||
const file = imageInput.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
const base64Image = event.target.result;
|
||||
insertImages([{ data: base64Image, name: file.name }]);
|
||||
imageInput.value = '';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
imageInputBound = true;
|
||||
}
|
||||
|
||||
function bindEditorStateTracking() {
|
||||
if (!editor || !editor.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncHandler = () => {
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
};
|
||||
|
||||
['keyup', 'mouseup', 'click', 'input', 'focus', 'blur'].forEach(eventName => {
|
||||
editor.editor.addEventListener(eventName, syncHandler);
|
||||
});
|
||||
|
||||
if (editor.events && editor.events.on) {
|
||||
editor.events.on('afterSetMode change afterCommand', syncHandler);
|
||||
}
|
||||
|
||||
editorDomObserver = new MutationObserver(() => scheduleStateSync());
|
||||
editorDomObserver.observe(editor.editor, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
attributeFilter: ['style', 'class', 'href', 'spellcheck']
|
||||
});
|
||||
|
||||
selectionChangeHandler = () => {
|
||||
if (isSelectionInsideEditor()) {
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('selectionchange', selectionChangeHandler);
|
||||
}
|
||||
|
||||
function scheduleStateSync() {
|
||||
if (stateSyncQueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateSyncQueued = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
stateSyncQueued = false;
|
||||
notifyState();
|
||||
});
|
||||
}
|
||||
|
||||
function notifyState() {
|
||||
if (!window.chrome || !window.chrome.webview) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.chrome.webview.postMessage({
|
||||
type: 'state',
|
||||
state: buildEditorState()
|
||||
});
|
||||
}
|
||||
|
||||
function buildEditorState() {
|
||||
const selectionElement = getSelectionElement();
|
||||
const contextElement = selectionElement || (editor && editor.editor ? editor.editor : document.body);
|
||||
const blockElement = getCurrentBlockElement() || contextElement;
|
||||
const style = window.getComputedStyle(contextElement);
|
||||
const blockStyle = window.getComputedStyle(blockElement);
|
||||
const selection = window.getSelection();
|
||||
const listElement = selectionElement ? selectionElement.closest('ol,ul') : null;
|
||||
const linkElement = selectionElement ? selectionElement.closest('a[href]') : null;
|
||||
const fontSize = parsePixelSize(style.fontSize);
|
||||
|
||||
return {
|
||||
bold: queryCommandState('bold', isBoldStyle(style)),
|
||||
italic: queryCommandState('italic', style.fontStyle === 'italic'),
|
||||
underline: queryCommandState('underline', (style.textDecorationLine || '').includes('underline')),
|
||||
strikethrough: queryCommandState('strikeThrough', (style.textDecorationLine || '').includes('line-through')),
|
||||
orderedList: !!(listElement && listElement.tagName.toLowerCase() === 'ol'),
|
||||
unorderedList: !!(listElement && listElement.tagName.toLowerCase() === 'ul'),
|
||||
canIndent: queryCommandEnabled('indent', true),
|
||||
canOutdent: queryCommandEnabled('outdent', !!listElement || !!(selectionElement && selectionElement.closest('blockquote'))),
|
||||
hasSelection: !!(selection && selection.rangeCount > 0 && !selection.isCollapsed && isSelectionInsideEditor()),
|
||||
isSpellCheckEnabled: !!(editor && editor.editor && editor.editor.spellcheck),
|
||||
alignment: normalizeAlignment(blockStyle.textAlign),
|
||||
fontFamily: normalizeFontFamily(style.fontFamily),
|
||||
fontSize: fontSize,
|
||||
paragraphStyle: normalizeParagraphTag(blockElement),
|
||||
textColor: normalizeColor(style.color),
|
||||
highlightColor: normalizeColor(style.backgroundColor),
|
||||
lineHeight: normalizeLineHeight(blockStyle.lineHeight, fontSize),
|
||||
linkUrl: linkElement ? linkElement.getAttribute('href') || '' : '',
|
||||
selectedText: selection && isSelectionInsideEditor() ? selection.toString() : ''
|
||||
};
|
||||
}
|
||||
|
||||
function getSelectionElement() {
|
||||
if (!editor || !editor.editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return editor.editor;
|
||||
}
|
||||
|
||||
const node = selection.anchorNode;
|
||||
if (!node) {
|
||||
return editor.editor;
|
||||
}
|
||||
|
||||
const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
|
||||
if (!element || !editor.editor.contains(element)) {
|
||||
return editor.editor;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function getCurrentBlockElement() {
|
||||
const selectionElement = getSelectionElement();
|
||||
if (!selectionElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return selectionElement.closest('h1,h2,h3,h4,h5,h6,p,blockquote,pre,div,li,td,th') || selectionElement;
|
||||
}
|
||||
|
||||
function rememberSelection() {
|
||||
if (!editor || !editor.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0 || !isSelectionInsideEditor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
lastKnownRange = selection.getRangeAt(0).cloneRange();
|
||||
}
|
||||
catch {
|
||||
lastKnownRange = null;
|
||||
}
|
||||
}
|
||||
|
||||
function restoreEditorSelection() {
|
||||
if (!editor || !editor.editor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
editor.selection.focus();
|
||||
|
||||
if (!lastKnownRange) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const selection = window.getSelection();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const restoredRange = lastKnownRange.cloneRange();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(restoredRange);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
lastKnownRange = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isSelectionInsideEditor() {
|
||||
if (!editor || !editor.editor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const anchorNode = selection.anchorNode;
|
||||
const focusNode = selection.focusNode;
|
||||
return !!anchorNode && !!focusNode && editor.editor.contains(anchorNode) && editor.editor.contains(focusNode);
|
||||
}
|
||||
|
||||
function queryCommandState(commandName, fallbackValue) {
|
||||
try {
|
||||
const value = document.queryCommandState(commandName);
|
||||
return typeof value === 'boolean' ? value : fallbackValue;
|
||||
}
|
||||
catch {
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
function queryCommandEnabled(commandName, fallbackValue) {
|
||||
try {
|
||||
const value = document.queryCommandEnabled(commandName);
|
||||
return typeof value === 'boolean' ? value : fallbackValue;
|
||||
}
|
||||
catch {
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
function isBoldStyle(style) {
|
||||
const fontWeight = style.fontWeight || '400';
|
||||
const numericWeight = parseInt(fontWeight, 10);
|
||||
return fontWeight === 'bold' || Number.isFinite(numericWeight) && numericWeight >= 600;
|
||||
}
|
||||
|
||||
function normalizeAlignment(value) {
|
||||
const normalized = (value || '').toLowerCase();
|
||||
if (normalized === 'center' || normalized === 'right' || normalized === 'justify') {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return 'left';
|
||||
}
|
||||
|
||||
function normalizeFontFamily(value) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value.split(',')[0].replace(/["']/g, '').trim();
|
||||
}
|
||||
|
||||
function normalizeParagraphTag(element) {
|
||||
return element && element.tagName ? element.tagName.toLowerCase() : 'div';
|
||||
}
|
||||
|
||||
function normalizeLineHeight(value, fontSize) {
|
||||
if (!value || value === 'normal') {
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
const numericValue = parseFloat(value);
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value.endsWith('px') && fontSize) {
|
||||
const ratio = numericValue / fontSize;
|
||||
return Number.isInteger(ratio) ? `${ratio}` : ratio.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
|
||||
}
|
||||
|
||||
return Number.isInteger(numericValue) ? `${numericValue}` : numericValue.toString();
|
||||
}
|
||||
|
||||
function parsePixelSize(value) {
|
||||
const numericValue = parseFloat(value || '');
|
||||
return Number.isFinite(numericValue) ? Math.round(numericValue) : null;
|
||||
}
|
||||
|
||||
function normalizeColor(value) {
|
||||
if (!value || value === 'transparent' || value === 'rgba(0, 0, 0, 0)') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (value.startsWith('#')) {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
|
||||
const rgbaMatch = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
||||
if (!rgbaMatch) {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
|
||||
const [, red, green, blue] = rgbaMatch;
|
||||
return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
|
||||
}
|
||||
|
||||
function toHex(value) {
|
||||
return Number(value).toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
function applyInlineStyleToSelection(styles) {
|
||||
if (!editor || !editor.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
restoreEditorSelection();
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0 || !isSelectionInsideEditor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
if (selection.isCollapsed) {
|
||||
const contextElement = getSelectionElement();
|
||||
if (contextElement) {
|
||||
Object.entries(styles).forEach(([propertyName, propertyValue]) => {
|
||||
contextElement.style[propertyName] = propertyValue || '';
|
||||
});
|
||||
}
|
||||
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
return;
|
||||
}
|
||||
|
||||
const span = document.createElement('span');
|
||||
Object.entries(styles).forEach(([propertyName, propertyValue]) => {
|
||||
if (propertyValue) {
|
||||
span.style[propertyName] = propertyValue;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
span.appendChild(range.extractContents());
|
||||
range.insertNode(span);
|
||||
selection.removeAllRanges();
|
||||
const newRange = document.createRange();
|
||||
newRange.selectNodeContents(span);
|
||||
selection.addRange(newRange);
|
||||
}
|
||||
catch {
|
||||
const css = styleObjectToCss(styles);
|
||||
const selectedText = escapeHtmlText(selection.toString());
|
||||
editor.selection.insertHTML(`<span style="${css}">${selectedText}</span>`);
|
||||
}
|
||||
|
||||
rememberSelection();
|
||||
scheduleStateSync();
|
||||
}
|
||||
|
||||
function styleObjectToCss(styles) {
|
||||
return Object.entries(styles)
|
||||
.filter(([, value]) => value)
|
||||
.map(([propertyName, propertyValue]) => `${camelToKebabCase(propertyName)}:${propertyValue}`)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
function camelToKebabCase(value) {
|
||||
return value.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
|
||||
}
|
||||
|
||||
function unwrapElement(element) {
|
||||
const parent = element.parentNode;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (element.firstChild) {
|
||||
parent.insertBefore(element.firstChild, element);
|
||||
}
|
||||
|
||||
parent.removeChild(element);
|
||||
}
|
||||
|
||||
function clampInteger(value, min, max) {
|
||||
const numericValue = parseInt(value, 10);
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return min;
|
||||
}
|
||||
|
||||
return Math.min(max, Math.max(min, numericValue));
|
||||
}
|
||||
|
||||
function normalizeLinkUrl(url) {
|
||||
const trimmed = (url || '').trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (/^[a-z]+:/i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (trimmed.includes('@') && !trimmed.includes('/')) {
|
||||
return `mailto:${trimmed}`;
|
||||
}
|
||||
|
||||
return `https://${trimmed}`;
|
||||
}
|
||||
|
||||
function escapeHtmlAttribute(value) {
|
||||
return `${value || ''}`
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function escapeHtmlText(value) {
|
||||
return `${value || ''}`
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user