| Current Path : /home/rtorresani/www/vendor/magento/module-page-builder/view/adminhtml/web/ts/js/utils/ |
| Current File : //home/rtorresani/www/vendor/magento/module-page-builder/view/adminhtml/web/ts/js/utils/editor.ts |
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
import $ from "jquery";
import "mage/adminhtml/tools";
import $t from "mage/translate";
import mageUtils from "mageUtils";
import {Editor} from "tinymce";
import Config from "../config";
/**
* Is the inline WYSIWYG supported?
*/
export function isWysiwygSupported() {
return Config.getConfig("can_use_inline_editing_on_stage");
}
/**
* Encode content for TinyMCE
*
* @param content
*/
export function encodeContent(content: string) {
if (isWysiwygSupported()) {
return convertVariablesToHtmlPreview(
convertWidgetsToHtmlPreview(
unescapeDoubleQuoteWithinWidgetDirective(
removeInvalidPlaceholders(content),
),
),
);
}
return content;
}
/**
* Prior to parsing the content remove any invalid placeholders within the content
*
* @param content
*/
function removeInvalidPlaceholders(content: string) {
if (content.indexOf("magento-placeholder") !== -1) {
const html = new DOMParser().parseFromString(content, "text/html");
const placeholders = html.querySelectorAll("span.magento-placeholder");
if (placeholders.length > 0) {
[].slice.call(placeholders).forEach((placeholder: HTMLSpanElement) => {
// If the invalid placeholder contains a directive, let's insert it back where it belongs
if (placeholder.innerText.indexOf("{{") !== -1) {
placeholder.parentNode.insertBefore(document.createTextNode(placeholder.innerText), placeholder);
}
placeholder.remove();
});
}
return html.body.innerHTML;
}
return content;
}
/**
* Convert all variables to their HTML preview counterparts
*
* Re-implements logic from lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js to parse
* and replace the variables within the content.
*
* @param content
*/
function convertVariablesToHtmlPreview(content: string) {
const config = Config.getConfig("tinymce").variables;
const magentoVariables = JSON.parse(config.placeholders);
return content.replace(/{\{\s?(?:customVar code=|config path=\")([^\}\"]+)[\"]?\s?\}\}/ig, (match, path) => {
const id = btoa(path).replace(/\+/g, ":").replace(/\//g, "_").replace(/=/g, "-");
const placeholder = $("<span />")
.addClass("magento-variable")
.addClass("magento-placeholder")
.addClass("mceNonEditable")
.prop("id", id)
.prop("contentEditable", "false");
if (magentoVariables[path].variable_type === "custom") {
placeholder.addClass("magento-custom-var");
}
const variableType = magentoVariables[path].variable_type;
if (magentoVariables[path] && (variableType === "default" || variableType === "custom")) {
placeholder.text(magentoVariables[path].variable_name);
} else {
// If we're unable to find the placeholder we need to attach an error class
placeholder.addClass("magento-placeholder-error");
placeholder.text((variableType === "custom" ? path : $t("Not Found")));
}
return placeholder[0].outerHTML;
});
}
/**
* Convert widgets within content to their HTML counterparts
*
* @param content
*/
function convertWidgetsToHtmlPreview(content: string) {
const config = Config.getConfig("tinymce").widgets;
return content.replace(/\{\{widget([\S\s]*?)\}\}/ig, (match, widgetBody) => {
const attributes = parseAttributesString(widgetBody);
let imageSrc;
if (attributes.type) {
const placeholder = $("<span />")
.addClass("magento-placeholder")
.addClass("magento-widget")
.addClass("mceNonEditable")
.prop("id", mageUtils.uniqueid())
.prop("contentEditable", "false");
attributes.type = attributes.type.replace(/\\\\/g, "\\");
imageSrc = config.placeholders[attributes.type];
if (!imageSrc) {
imageSrc = config.error_image_url;
placeholder.addClass("magento-placeholder-error");
}
const image = $("<img />")
.prop("id", window.Base64.idEncode(match))
.prop("src", imageSrc);
placeholder.append(image);
let widgetType = "";
if (config.types[attributes.type]) {
widgetType += config.types[attributes.type];
}
placeholder.append($(document.createTextNode(widgetType)));
return placeholder[0].outerHTML;
}
});
}
/**
* Parse attributes into a string
*
* @param attributes
*/
export function parseAttributesString(attributes: string): { [key: string]: string; } {
const result: {
[key: string]: string;
} = {};
attributes.replace(
/(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
(match: string, key: string, value: string) => {
result[key] = value ? value.replace(/"e;/g, "\"") : value;
return "";
},
);
return result;
}
/**
* Lock all image sizes before initializing TinyMCE to avoid content jumps
*
* @param element
*/
export function lockImageSize(element: HTMLElement) {
[].slice.call(element.querySelectorAll("img")).forEach((image: HTMLImageElement) => {
if (image.style.width.length === 0) {
image.style.width = /^\d+$/.test(image.getAttribute("width")) ?
image.getAttribute("width") + "px" :
image.getAttribute("width");
image.setAttribute("data-width-locked", "true");
}
if (image.style.height.length === 0) {
image.style.height = /^\d+$/.test(image.getAttribute("height")) ?
image.getAttribute("height") + "px" :
image.getAttribute("height");
image.setAttribute("data-height-locked", "true");
}
});
}
/**
* Reverse forced image size after TinyMCE is finished initializing
*
* @param element
*/
export function unlockImageSize(element: HTMLElement) {
[].slice.call(element.querySelectorAll("img")).forEach((image: HTMLImageElement) => {
if (image.getAttribute("data-width-locked")) {
image.style.width = null;
image.removeAttribute("data-width-locked");
}
if (image.getAttribute("data-height-locked")) {
image.style.height = null;
image.removeAttribute("data-height-locked");
}
});
}
/**
* Create a bookmark within the content to be restored later
*/
export function createBookmark(event: JQueryEventObject): Bookmark {
const wrapperElement = $(event.target).parents(".inline-wysiwyg");
/**
* Create an element bookmark
*
* @param element
*/
const createElementBookmark = (element: Element) => {
return {
name: element.nodeName,
index: findNodeIndex(
wrapperElement[0],
element.nodeName,
element,
),
};
};
// Handle direct clicks onto an IMG
if (event.target.nodeName === "IMG") {
return createElementBookmark(event.target);
}
if (window.getSelection) {
const selection = window.getSelection();
const id = mageUtils.uniqueid();
if (selection.getRangeAt && selection.rangeCount) {
const range = normalizeTableCellSelection(selection.getRangeAt(0).cloneRange());
// Determine if the current node is an image or span that we want to select instead of text
const currentNode = range.startContainer as HTMLElement;
if (currentNode.nodeType === Node.ELEMENT_NODE && (currentNode.nodeName === "IMG"
|| (currentNode.nodeName === "SPAN" && currentNode.classList.contains("magento-placeholder")))
) {
return createElementBookmark(currentNode);
}
// Also check if the direct parent is either of these
const parentNode = range.startContainer.parentNode as HTMLElement;
if (parentNode.nodeName === "IMG"
|| (parentNode.nodeName === "SPAN" && parentNode.classList.contains("magento-placeholder"))
) {
return createElementBookmark(parentNode);
}
if (!range.collapsed) {
range.collapse(false);
const endBookmarkNode = createBookmarkSpan(id + "_end");
range.insertNode(endBookmarkNode);
}
const range2 = normalizeTableCellSelection(selection.getRangeAt(0));
range2.collapse(true);
const startBookmarkNode = createBookmarkSpan(id + "_start");
range2.insertNode(startBookmarkNode);
return {id};
}
}
return null;
}
/**
* Move the cursor to our new bookmark
*
* @param bookmark
*/
export function moveToBookmark(bookmark: Bookmark) {
((window as any).tinymce.activeEditor as Editor).selection.moveToBookmark(bookmark);
}
/**
* Retrieve active editor from TinyMCE
*/
export function getActiveEditor(): Editor {
return ((window as any).tinymce.activeEditor as Editor);
}
/**
* Create a bookmark span for the selection
*
* @param id
*/
function createBookmarkSpan(id: string) {
const bookmark = document.createElement("span");
bookmark.setAttribute("data-mce-type", "bookmark");
bookmark.id = id;
bookmark.style.overflow = "hidden";
bookmark.style.lineHeight = "0px";
return bookmark;
}
/**
* Find the index of an element within a wrapper
*
* @param wrapperElement
* @param name
* @param element
*/
export function findNodeIndex(wrapperElement: HTMLElement, name: string, element: Element) {
const selector = name.toLowerCase() + ':not([data-mce-bogus="all"])';
// If there is no ID on the element add a unique ID so we can efficiently find it
if (!element.id) {
element.id = mageUtils.uniqueid();
}
return $(wrapperElement).find(selector).toArray().findIndex((node) => {
return node.id === element.id;
});
}
/**
* Get a node by index within a wrapper
*
* @param wrapperElement
* @param name
* @param index
*/
export function getNodeByIndex(wrapperElement: HTMLElement, name: string, index: number): HTMLElement {
const selector = name.toLowerCase() + ':not([data-mce-bogus="all"])';
return $(wrapperElement).find(selector).get(index);
}
/**
* Create a double click event that works in all browsers
*/
export function createDoubleClickEvent(): Event {
try {
return new MouseEvent("dblclick", {
view: window,
bubbles: true,
cancelable: true,
});
} catch (e) {
const dblClickEvent = document.createEvent("MouseEvent");
dblClickEvent.initMouseEvent(
"dblclick",
true,
true,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
);
return dblClickEvent;
}
}
/**
* Replace all desktop styles that left unprocessed back to style element to prevent data corruption.
*/
export function processInlineStyles(html: string) {
const name = Config.getConfig("defaultViewport");
const searchPattern = new RegExp(`data-${name}-style=`, "g");
return html.replace(searchPattern, "style=");
}
/**
* Move the end point of a range to handle tables
*
* @param range
* @param start
*/
function moveEndPoint(range: Range, start: boolean) {
let container;
let offset;
let childNodes;
if (start) {
container = range.startContainer;
offset = range.startOffset;
} else {
container = range.endContainer;
offset = range.endOffset;
}
if (container.nodeType === Node.ELEMENT_NODE && container.nodeName === "TR") {
childNodes = container.childNodes;
container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
if (container) {
offset = start ? 0 : container.childNodes.length;
if (start) {
range.setStart(container, offset);
} else {
range.setEnd(container, offset);
}
}
}
}
/**
* Normalize the table sell selection within a range to better handle selections being inside of tables
*
* @param range
*/
function normalizeTableCellSelection(range: Range): Range {
moveEndPoint(range, true);
moveEndPoint(range, false);
return range;
}
/**
* Convert HTML encoded double quote to double quote with backslash within widget directives
*
* @param {string} content
* @returns {string}
*/
export function escapeDoubleQuoteWithinWidgetDirective(content: string): string {
return content.replace(/\{\{widget[\S\s]*?\}\}/ig, (match: string) => {
return match.replace(/"/g, "\\\"");
});
}
/**
* Convert double quote with backslash to HTML encoded double quote within widget directives
*
* @param {string} content
* @returns {string}
*/
export function unescapeDoubleQuoteWithinWidgetDirective(content: string): string {
return content.replace(/\{\{widget[\S\s]*?\}\}/ig, (match: string) => {
return match.replace(/\\+"/g, """);
});
}
/**
* Convert double quote to single quote within magento variable directives
*
* @param {string} content
* @returns {string}
*/
export function replaceDoubleQuoteWithSingleQuoteWithinVariableDirective(content: string): string {
// Find html elements which attributes contain magento variables directives
return content.replace(
/<([a-z0-9\-\_]+)([^>]+?[a-z0-9\-\_]+="[^"]*?\{\{.+?\}\}.*?".*?)>/gi,
(match1: string, tag: string, attributes: string) => {
// Replace double quote with single quote within magento variable directive
const sanitizedAttributes = attributes.replace(
/\{\{[^\{\}]+\}\}/gi,
(match2: string) => match2.replace(/"/g, "'"),
);
return "<" + tag + sanitizedAttributes + ">";
},
);
}
interface IdBookmark {
id: string;
keep?: boolean;
}
interface IndexBookmark {
name: string;
index: number;
}
export type Bookmark = IdBookmark | IndexBookmark;