@wiris/mathtype-html-integration-devkit
Version:
Allows to integrate MathType Web into any JavaScript HTML WYSIWYG rich text editor.
1,306 lines (1,174 loc) • 57.3 kB
JavaScript
// eslint-disable-next-line max-classes-per-file
import PopUpMessage from "./popupmessage";
import Util from "./util";
import Configuration from "./configuration";
import Listeners from "./listeners";
import StringManager from "./stringmanager";
import ContentManager from "./contentmanager";
import Telemeter from "./telemeter";
import IntegrationModel from "./integrationmodel";
import Core from "./core.src";
import focusProtection from "./focusprotection";
import closeIcon from "../styles/icons/general/close_icon.svg"; //eslint-disable-line
import closeHoverIcon from "../styles/icons/hover/close_icon_h.svg"; //eslint-disable-line
import fullsIcon from "../styles/icons/general/fulls_icon.svg"; //eslint-disable-line
import fullsHoverIcon from "../styles/icons/hover/fulls_icon_h.svg"; //eslint-disable-line
import minIcon from "../styles/icons/general/min_icon.svg"; //eslint-disable-line
import minHoverIcon from "../styles/icons/hover/min_icon_h.svg"; //eslint-disable-line
import minsIcon from "../styles/icons/general/mins_icon.svg"; //eslint-disable-line
import minsHoverIcon from "../styles/icons/hover/mins_icon_h.svg"; //eslint-disable-line
import maxIcon from "../styles/icons/general/max_icon.svg"; //eslint-disable-line
import maxHoverIcon from "../styles/icons/hover/max_icon_h.svg"; //eslint-disable-line
const { unprotect, protect } = focusProtection();
/**
* @typedef {Object} DeviceProperties
* @property {String} DeviceProperties.orientation - Indicates of the orientation of the device.
* @property {Boolean} DeviceProperties.isAndroid - True if the device is Android. False otherwise.
* @property {Boolean} DeviceProperties.isIOS - True if the device is iOS. False otherwise.
* @property {Boolean} DeviceProperties.isMobile - True if the device is a mobile one.
* False otherwise.
* @property {Boolean} DeviceProperties.isDesktop - True if the device is a desktop one.
* False otherwise.
*/
export default class ModalDialog {
/**
* @classdesc
* This class represents a modal dialog. The modal dialog admits
* a {@link ContentManager} instance to manage the content of the dialog.
* @constructs
* @param {Object} modalDialogAttributes - An object containing all modal dialog attributes.
*/
constructor(modalDialogAttributes) {
this.attributes = modalDialogAttributes;
// Metrics.
const ua = navigator.userAgent.toLowerCase();
const isAndroid = ua.indexOf("android") > -1;
const isIOS = ContentManager.isIOS();
this.iosSoftkeyboardOpened = false;
this.iosMeasureUnit = ua.indexOf("crios") === -1 ? "%" : "vh";
this.iosDivHeight = `auto`;
const deviceWidth = window.outerWidth;
const deviceHeight = window.outerHeight;
const landscape = deviceWidth > deviceHeight;
const portrait = deviceWidth < deviceHeight;
// TODO: Detect isMobile without using editor metrics.
const isLandscape = landscape && this.attributes.height > deviceHeight;
const isPortrait = portrait && this.attributes.width > deviceWidth;
const isMobile = ContentManager.isMobile();
// Obtain number of current instance.
this.instanceId = document.getElementsByClassName("wrs_modal_dialogContainer").length;
// Device object properties.
/**
* @type {DeviceProperties}
*/
this.deviceProperties = {
orientation: landscape ? "landscape" : "portrait",
isAndroid,
isIOS,
isMobile,
isDesktop: !isMobile && !isIOS && !isAndroid,
};
this.properties = {
created: false,
state: "",
previousState: "",
position: { bottom: 0, right: 10 },
size: { height: 338, width: 580 },
};
/**
* Object to keep website's style before change it on lock scroll for mobile devices.
* @type {Object}
* @property {String} bodyStylePosition - Previous body style position.
* @property {String} bodyStyleOverflow - Previous body style overflow.
* @property {String} htmlStyleOverflow - Previous body style overflow.
* @property {String} windowScrollX - Previous window's scroll Y.
* @property {String} windowScrollY - Previous window's scroll X.
*/
this.websiteBeforeLockParameters = null;
let attributes = {};
attributes.class = "wrs_modal_overlay";
attributes.id = this.getElementId(attributes.class);
this.overlay = Util.createElement("div", attributes);
attributes = {};
attributes.class = "wrs_modal_title_bar";
attributes.id = this.getElementId(attributes.class);
this.titleBar = Util.createElement("div", attributes);
attributes = {};
attributes.class = "wrs_modal_title";
attributes.id = this.getElementId(attributes.class);
this.title = Util.createElement("div", attributes);
this.title.innerHTML = "offline";
attributes = {};
attributes.class = "wrs_modal_close_button";
attributes.id = this.getElementId(attributes.class);
attributes.title = StringManager.get("close");
attributes.style = {};
this.closeDiv = Util.createElement("a", attributes);
this.closeDiv.setAttribute("role", "button");
this.closeDiv.setAttribute("tabindex", 3);
// Apply styles and events after the creation as createElement doesn't process them correctly
let generalStyle = `background-size: 10px; background-image: url(data:image/svg+xml;base64,${window.btoa(closeIcon)})`;
let hoverStyle = `background-size: 10px; background-image: url(data:image/svg+xml;base64,${window.btoa(closeHoverIcon)})`;
this.closeDiv.setAttribute("style", generalStyle);
this.closeDiv.setAttribute("onmouseover", `this.style = "${hoverStyle}";`);
this.closeDiv.setAttribute("onmouseout", `this.style = "${generalStyle}";`);
// To identifiy the element in automated testing
this.closeDiv.setAttribute("data-testid", "mtcteditor-close-button");
attributes = {};
attributes.class = "wrs_modal_stack_button";
attributes.id = this.getElementId(attributes.class);
attributes.title = StringManager.get("exit_fullscreen");
this.stackDiv = Util.createElement("a", attributes);
this.stackDiv.setAttribute("role", "button");
this.stackDiv.setAttribute("tabindex", 2);
generalStyle = `background-size: 10px; background-image: url(data:image/svg+xml;base64,${window.btoa(minsIcon)})`;
hoverStyle = `background-size: 10px; background-image: url(data:image/svg+xml;base64,${window.btoa(minsHoverIcon)})`;
this.stackDiv.setAttribute("style", generalStyle);
this.stackDiv.setAttribute("onmouseover", `this.style = "${hoverStyle}";`);
this.stackDiv.setAttribute("onmouseout", `this.style = "${generalStyle}";`);
// To identifiy the element in automated testing
this.stackDiv.setAttribute("data-testid", "mtcteditor-fullscreen-disable-button");
attributes = {};
attributes.class = "wrs_modal_maximize_button";
attributes.id = this.getElementId(attributes.class);
attributes.title = StringManager.get("fullscreen");
this.maximizeDiv = Util.createElement("a", attributes);
this.maximizeDiv.setAttribute("role", "button");
this.maximizeDiv.setAttribute("tabindex", 2);
generalStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(fullsIcon)})`;
hoverStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(fullsHoverIcon)})`;
this.maximizeDiv.setAttribute("style", generalStyle);
this.maximizeDiv.setAttribute("onmouseover", `this.style = "${hoverStyle}";`);
this.maximizeDiv.setAttribute("onmouseout", `this.style = "${generalStyle}";`);
// To identifiy the element in automated testing
this.maximizeDiv.setAttribute("data-testid", "mtcteditor-fullscreen-enable-button");
attributes = {};
attributes.class = "wrs_modal_minimize_button";
attributes.id = this.getElementId(attributes.class);
attributes.title = StringManager.get("minimize");
this.minimizeDiv = Util.createElement("a", attributes);
this.minimizeDiv.setAttribute("role", "button");
this.minimizeDiv.setAttribute("tabindex", 1);
generalStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(minIcon)})`;
hoverStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(minHoverIcon)})`;
this.minimizeDiv.setAttribute("style", generalStyle);
this.minimizeDiv.setAttribute("onmouseover", `this.style = "${hoverStyle}";`);
this.minimizeDiv.setAttribute("onmouseout", `this.style = "${generalStyle}";`);
// To identify the element in automated testing
this.minimizeDiv.setAttribute("data-testid", "mtcteditor-minimize-button");
attributes = {};
attributes.class = "wrs_modal_dialogContainer";
attributes.id = this.getElementId(attributes.class);
attributes.role = "dialog";
this.container = Util.createElement("div", attributes);
this.container.setAttribute("aria-labeledby", "wrs_modal_title[0]");
attributes = {};
attributes.class = "wrs_modal_wrapper";
attributes.id = this.getElementId(attributes.class);
this.wrapper = Util.createElement("div", attributes);
attributes = {};
attributes.class = "wrs_content_container";
attributes.id = this.getElementId(attributes.class);
this.contentContainer = Util.createElement("div", attributes);
attributes = {};
attributes.class = "wrs_modal_controls";
attributes.id = this.getElementId(attributes.class);
this.controls = Util.createElement("div", attributes);
attributes = {};
attributes.class = "wrs_modal_buttons_container";
attributes.id = this.getElementId(attributes.class);
this.buttonContainer = Util.createElement("div", attributes);
// Buttons: all button must be created using createSubmitButton method.
this.submitButton = this.createSubmitButton(
{
id: this.getElementId("wrs_modal_button_accept"),
class: "wrs_modal_button_accept",
innerHTML: StringManager.get("accept"),
// To identifiy the element in automated testing
"data-testid": "mtcteditor-insert-button",
},
this.submitAction.bind(this),
);
this.cancelButton = this.createSubmitButton(
{
id: this.getElementId("wrs_modal_button_cancel"),
class: "wrs_modal_button_cancel",
innerHTML: StringManager.get("cancel"),
// To identifiy the element in automated testing
"data-testid": "mtcteditor-cancel-button",
},
this.cancelAction.bind(this),
);
this.contentManager = null;
// Overlay popup.
const popupStrings = {
cancelString: StringManager.get("cancel"),
submitString: StringManager.get("close"),
message: StringManager.get("close_modal_warning"),
};
const callbacks = {
closeCallback: () => {
this.close("mtc_close");
},
cancelCallback: () => {
this.focus();
},
};
const popupupProperties = {
overlayElement: this.container,
callbacks,
strings: popupStrings,
};
this.popup = new PopUpMessage(popupupProperties);
/**
* Indicates if directionality of the modal dialog is RTL. false by default.
* @type {Boolean}
*/
this.rtl = false;
if ("rtl" in this.attributes) {
this.rtl = this.attributes.rtl;
}
// Event handlers need modal instance context.
this.handleOpenedIosSoftkeyboard = this.handleOpenedIosSoftkeyboard.bind(this);
this.handleClosedIosSoftkeyboard = this.handleClosedIosSoftkeyboard.bind(this);
}
/**
* This method sets an ContentManager instance to ModalDialog. ContentManager
* manages the logic of ModalDialog content: submit, update, close and changes.
* @param {ContentManager} contentManager - ContentManager instance.
*/
setContentManager(contentManager) {
this.contentManager = contentManager;
}
/**
* Returns the modal contentElement object.
* @returns {ContentManager} the instance of the ContentManager class.
*/
getContentManager() {
return this.contentManager;
}
/**
* This method is called when the modal object has been submitted. Calls
* contentElement submitAction method - if exists - and closes the modal
* object. No logic about the content should be placed here,
* contentElement.submitAction is the responsible of the content logic.
*/
async submitAction() {
if (typeof this.contentManager.submitAction !== "undefined") {
this.contentManager.submitAction();
}
await this.close("mtc_insert");
}
/**
* Performs the cancel action.
* If there are no changes in the content, it closes the modal.
* Otherwise, it shows a pop-up message to confirm the cancel action.
* @returns {Promise<void>} - A promise that resolves when the modal is closed.
*/
async cancelAction() {
if (typeof this.contentManager.hasChanges === "undefined" || !this.contentManager.hasChanges()) {
IntegrationModel.setActionsOnCancelButtons();
await this.close("mtc_close");
} else {
this.showPopUpMessage();
}
}
/**
* Returns a button element.
* @param {Object} properties - Input button properties.
* @param {String} properties.class - Input button class.
* @param {String} properties.innerHTML - Input button innerHTML.
* @param {Object} callback - Callback function associated to click event.
* @returns {HTMLButtonElement} The button element.
*
*/
// eslint-disable-next-line class-methods-use-this
createSubmitButton(properties, callback) {
class SubmitButton {
constructor() {
this.element = document.createElement("button");
this.element.id = properties.id;
this.element.className = properties.class;
this.element.innerHTML = properties.innerHTML;
this.element.dataset.testid = properties["data-testid"];
Util.addEvent(this.element, "click", callback);
}
getElement() {
return this.element;
}
}
return new SubmitButton(properties, callback).getElement();
}
/**
* Creates the modal window object inserting a contentElement object.
*/
create() {
/* Modal Window Structure
_____________________________________________________________________________________
|wrs_modal_dialog_Container |
| _________________________________________________________________________________ |
| |title_bar minimize_button stack_button close_button | |
| |_______________________________________________________________________________| |
| |wrapper | |
| | _____________________________________________________________________________ | |
| | |content | | |
| | | | | |
| | | | | |
| | |___________________________________________________________________________| | |
| | _____________________________________________________________________________ | |
| | |controls | | |
| | | ___________________________________ | | |
| | | |buttonContainer | | | |
| | | | _______________________________ | | | |
| | | | |button_accept | button_cancel| | | | |
| | | |_|_____________ |______________|_| | | |
| | |___________________________________________________________________________| | |
| |_______________________________________________________________________________| |
|___________________________________________________________________________________| */
this.titleBar.appendChild(this.closeDiv);
this.titleBar.appendChild(this.stackDiv);
this.titleBar.appendChild(this.maximizeDiv);
this.titleBar.appendChild(this.minimizeDiv);
this.titleBar.appendChild(this.title);
if (this.deviceProperties.isDesktop) {
this.container.appendChild(this.titleBar);
}
this.wrapper.appendChild(this.contentContainer);
this.wrapper.appendChild(this.controls);
this.controls.appendChild(this.buttonContainer);
this.buttonContainer.appendChild(this.submitButton);
this.buttonContainer.appendChild(this.cancelButton);
this.container.appendChild(this.wrapper);
// Check if browser has scrollBar before modal has modified.
this.recalculateScrollBar();
document.body.appendChild(this.container);
document.body.appendChild(this.overlay);
if (this.deviceProperties.isDesktop) {
// Desktop.
this.createModalWindowDesktop();
this.createResizeButtons();
this.addListeners();
// Maximize window only when the configuration is set and the device is not iOS or Android.
if (Configuration.get("modalWindowFullScreen")) {
this.maximize();
}
} else if (this.deviceProperties.isAndroid) {
this.createModalWindowAndroid();
} else if (this.deviceProperties.isIOS) {
this.createModalWindowIos();
}
if (this.contentManager != null) {
this.contentManager.insert(this);
}
this.properties.open = true;
this.properties.created = true;
// Checks language directionality.
if (this.isRTL()) {
this.container.style.right = `${window.innerWidth - this.scrollbarWidth - this.container.offsetWidth}px`;
this.container.className += " wrs_modal_rtl";
}
}
/**
* Creates a button in the modal object to resize it.
*/
createResizeButtons() {
// This is a definition of Resize Button Bottom-Right.
this.resizerBR = document.createElement("div");
this.resizerBR.className = "wrs_bottom_right_resizer";
this.resizerBR.innerHTML = "◢";
// To identifiy the element in automated testing
this.resizerBR.dataset.testid = "mtcteditor-resize-button-right";
// This is a definition of Resize Button Top-Left.
this.resizerTL = document.createElement("div");
this.resizerTL.className = "wrs_bottom_left_resizer";
// To identifiy the element in automated testing
this.resizerTL.dataset.testid = "mtcteditor-resize-button-left";
// Append resize buttons to modal.
this.container.appendChild(this.resizerBR);
this.titleBar.appendChild(this.resizerTL);
// Add events to resize on click and drag.
Util.addEvent(this.resizerBR, "mousedown", this.activateResizeStateBR.bind(this));
Util.addEvent(this.resizerTL, "mousedown", this.activateResizeStateTL.bind(this));
}
/**
* Initialize variables for Bottom-Right resize button
* @param {MouseEvent} mouseEvent - Mouse event.
*/
activateResizeStateBR(mouseEvent) {
this.initializeResizeProperties(mouseEvent, false);
}
/**
* Initialize variables for Top-Left resize button
* @param {MouseEvent} mouseEvent - Mouse event.
*/
activateResizeStateTL(mouseEvent) {
this.initializeResizeProperties(mouseEvent, true);
}
/**
* Common method to initialize variables at resize.
* @param {MouseEvent} mouseEvent - Mouse event.
*/
initializeResizeProperties(mouseEvent, leftOption) {
// Apply class for disable involuntary select text when drag.
Util.addClass(document.body, "wrs_noselect");
Util.addClass(this.overlay, "wrs_overlay_active");
this.resizeDataObject = {
x: this.eventClient(mouseEvent).X,
y: this.eventClient(mouseEvent).Y,
};
// Save Initial state of modal to compare on drag and obtain the difference.
this.initialWidth = parseInt(this.container.style.width, 10);
this.initialHeight = parseInt(this.container.style.height, 10);
if (!leftOption) {
this.initialRight = parseInt(this.container.style.right, 10);
this.initialBottom = parseInt(this.container.style.bottom, 10);
} else {
this.leftScale = true;
}
if (!this.initialRight) {
this.initialRight = 0;
}
if (!this.initialBottom) {
this.initialBottom = 0;
}
// Disable mouse events on editor when we start to drag modal.
document.body.style["user-select"] = "none";
}
/**
* This method opens the modal window, restoring the previous state, position and metrics,
* if exists. By default the modal object opens in stack mode.
*/
open() {
// Removing close class.
this.removeClass("wrs_closed");
// Hiding keyboard for mobile devices.
const { isIOS } = this.deviceProperties;
const { isAndroid } = this.deviceProperties;
const { isMobile } = this.deviceProperties;
if (isIOS || isAndroid || isMobile) {
// Restore scale to 1.
this.restoreWebsiteScale();
this.lockWebsiteScroll();
// Due to editor wait we need to wait until editor focus.
setTimeout(() => {
this.hideKeyboard();
}, 400);
}
// New modal window. He need to create the whole object.
if (!this.properties.created) {
this.create();
} else {
// Previous state closed. Open method can be called even the previous state is open,
// for example updating the content of the modal object.
if (!this.properties.open) {
this.properties.open = true;
// Restoring the previous open state: if the modal object has been closed
// re-open it should preserve the state and the metrics.
if (!this.deviceProperties.isAndroid && !this.deviceProperties.isIOS) {
this.restoreState();
}
}
// Maximize window only when the configuration is set and the device is not iOs or Android.
if (this.deviceProperties.isDesktop && Configuration.get("modalWindowFullScreen")) {
this.maximize();
}
// In iOS we need to recalculate the size of the modal object because
// iOS keyboard is a float div which can overlay the modal object.
if (this.deviceProperties.isIOS) {
this.iosSoftkeyboardOpened = false;
}
}
if (!ContentManager.isEditorLoaded()) {
const listener = Listeners.newListener("onLoad", () => {
this.displayEditor();
});
this.contentManager.addListener(listener);
} else {
this.displayEditor();
}
}
/**
* Prepares and displays the editor in the modal.
*
* This method is responsible for displaying the MathType editor inside the modal container.
*
* For Moodle environments, it applies focus protection to prevent external scripts
* from hijacking focus away from the editor while it's open. This is particularly
* important in Moodle which may have its own focus management scripts.
* @returns {void}
*/
displayEditor() {
if (this.contentManager.integrationModel.isMoodle) {
protect(this.container, this.overlay, this.contentContainer);
}
// Initialize and open the editor using the contentManager.
this.contentManager.onOpen(this);
}
/**
* Closes the modal.
* Removes specific CSS classes, saves modal properties, unlocks website scroll,
* sets the 'open' property to false, and triggers the 'onModalClose' event.
* If a close trigger is defined, it tracks the telemetry event 'CLOSED_MTCT_EDITOR' with the trigger.
* @returns {Promise<void>} A promise that resolves when the modal is closed.
*/
async close(trigger) {
// Remove focus protection before closing
unprotect(this.container);
this.removeClass("wrs_maximized");
this.removeClass("wrs_minimized");
this.removeClass("wrs_stack");
this.addClass("wrs_closed");
this.saveModalProperties();
this.unlockWebsiteScroll();
this.properties.open = false;
if (trigger) {
try {
await Telemeter.telemeter.track("CLOSED_MTCT_EDITOR", {
toolbar: this.contentManager.toolbar,
trigger,
});
} catch (error) {
console.error("Error tracking CLOSED_MTCT_EDITOR", error);
}
}
Core.globalListeners.fire("onModalClose", {});
}
/**
* Closes modal window and destroys the object.
*/
destroy() {
// Remove focus protection before destroying
unprotect(this.container);
// Close modal window.
this.close();
// Remove listeners and destroy the object.
this.removeListeners();
this.overlay.remove();
this.container.remove();
// Reset properties to allow open again.
this.properties.created = false;
}
/**
* Sets the website scale to one.
*/
// eslint-disable-next-line class-methods-use-this
restoreWebsiteScale() {
let viewportmeta = document.querySelector("meta[name=viewport]");
// Let the equal symbols in order to search and make meta's final content.
const contentAttrsToUpdate = ["initial-scale=", "minimum-scale=", "maximum-scale="];
const contentAttrsValuesToUpdate = ["1.0", "1.0", "1.0"];
const setMetaAttrFunc = (viewportelement, contentAttrs) => {
const contentAttr = viewportelement.getAttribute("content");
// If it exists, we need to maintain old values and put our values.
if (contentAttr) {
const attrArray = contentAttr.split(",");
let finalContentMeta = "";
const oldAttrs = [];
for (let i = 0; i < attrArray.length; i += 1) {
let isAttrToUpdate = false;
let j = 0;
while (!isAttrToUpdate && j < contentAttrs.length) {
if (attrArray[i].indexOf(contentAttrs[j])) {
isAttrToUpdate = true;
}
j += 1;
}
if (!isAttrToUpdate) {
oldAttrs.push(attrArray[i]);
}
}
for (let i = 0; i < contentAttrs.length; i += 1) {
const attr = contentAttrs[i] + contentAttrsValuesToUpdate[i];
finalContentMeta += i === 0 ? attr : `,${attr}`;
}
for (let i = 0; i < oldAttrs.length; i += 1) {
finalContentMeta += `,${oldAttrs[i]}`;
}
viewportelement.setAttribute("content", finalContentMeta);
// It needs to set to empty because setAttribute refresh only when attribute is different.
viewportelement.setAttribute("content", "");
viewportelement.setAttribute("content", contentAttr);
} else {
viewportelement.setAttribute("content", "initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0");
viewportelement.removeAttribute("content");
}
};
if (!viewportmeta) {
viewportmeta = document.createElement("meta");
document.getElementsByTagName("head")[0].appendChild(viewportmeta);
setMetaAttrFunc(viewportmeta, contentAttrsToUpdate, contentAttrsValuesToUpdate);
viewportmeta.remove();
} else {
setMetaAttrFunc(viewportmeta, contentAttrsToUpdate, contentAttrsValuesToUpdate);
}
}
/**
* Locks website scroll for mobile devices.
*/
lockWebsiteScroll() {
this.websiteBeforeLockParameters = {
bodyStylePosition: document.body.style.position ? document.body.style.position : "",
bodyStyleOverflow: document.body.style.overflow ? document.body.style.overflow : "",
htmlStyleOverflow: document.documentElement.style.overflow ? document.documentElement.style.overflow : "",
windowScrollX: window.scrollX,
windowScrollY: window.scrollY,
};
}
/**
* Unlocks website scroll for mobile devices.
*/
unlockWebsiteScroll() {
if (this.websiteBeforeLockParameters) {
document.body.style.position = this.websiteBeforeLockParameters.bodyStylePosition;
document.body.style.overflow = this.websiteBeforeLockParameters.bodyStyleOverflow;
document.documentElement.style.overflow = this.websiteBeforeLockParameters.htmlStyleOverflow;
const { windowScrollX } = this.websiteBeforeLockParameters;
const { windowScrollY } = this.websiteBeforeLockParameters;
window.scrollTo(windowScrollX, windowScrollY);
this.websiteBeforeLockParameters = null;
}
}
/**
* Util function to known if browser is IE11.
* @returns {Boolean} true if the browser is IE11. false otherwise.
*/
// eslint-disable-next-line class-methods-use-this
isIE11() {
if (
navigator.userAgent.search("Msie/") >= 0 ||
navigator.userAgent.search("Trident/") >= 0 ||
navigator.userAgent.search("Edge/") >= 0
) {
return true;
}
return false;
}
/**
* Returns if the current language type is RTL.
* @return {Boolean} true if current language is RTL. false otherwise.
*/
isRTL() {
if (this.attributes.language === "ar" || this.attributes.language === "he") {
return true;
}
return this.rtl;
}
/**
* Adds a class to all modal ModalDialog DOM elements.
* @param {String} className - Class name.
*/
addClass(className) {
Util.addClass(this.overlay, className);
Util.addClass(this.titleBar, className);
Util.addClass(this.overlay, className);
Util.addClass(this.container, className);
Util.addClass(this.contentContainer, className);
Util.addClass(this.stackDiv, className);
Util.addClass(this.minimizeDiv, className);
Util.addClass(this.maximizeDiv, className);
Util.addClass(this.wrapper, className);
}
/**
* Remove a class from all modal DOM elements.
* @param {String} className - Class name.
*/
removeClass(className) {
Util.removeClass(this.overlay, className);
Util.removeClass(this.titleBar, className);
Util.removeClass(this.overlay, className);
Util.removeClass(this.container, className);
Util.removeClass(this.contentContainer, className);
Util.removeClass(this.stackDiv, className);
Util.removeClass(this.minimizeDiv, className);
Util.removeClass(this.maximizeDiv, className);
Util.removeClass(this.wrapper, className);
}
/**
* Create modal dialog for desktop.
*/
createModalWindowDesktop() {
this.addClass("wrs_modal_desktop");
this.stack();
}
/**
* Create modal dialog for non android devices.
*/
createModalWindowAndroid() {
this.addClass("wrs_modal_android");
window.addEventListener("resize", this.orientationChangeAndroidSoftkeyboard.bind(this));
}
/**
* Create modal dialog for iOS devices.
*/
createModalWindowIos() {
this.addClass("wrs_modal_ios");
// Refresh the size when the orientation is changed.
window.addEventListener("resize", this.orientationChangeIosSoftkeyboard.bind(this));
}
/**
* Restore previous state, position and size of previous stacked modal dialog.
*/
restoreState() {
if (this.properties.state === "maximized") {
// Reset states for prevent return to stack state.
this.maximize();
} else if (this.properties.state === "minimized") {
// Reset states for prevent return to stack state.
this.properties.state = this.properties.previousState;
this.properties.previousState = "";
this.minimize();
} else {
this.stack();
}
}
/**
* Stacks the modal object.
*/
stack() {
this.properties.previousState = this.properties.state;
this.properties.state = "stack";
this.removeClass("wrs_maximized");
this.minimizeDiv.title = StringManager.get("minimize");
this.removeClass("wrs_minimized");
this.addClass("wrs_stack");
// Change maximize/minimize icon to minimize icon
const generalStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(minIcon)})`;
const hoverStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(minHoverIcon)})`;
this.minimizeDiv.setAttribute("style", generalStyle);
this.minimizeDiv.setAttribute("onmouseover", `this.style = "${hoverStyle}";`);
this.minimizeDiv.setAttribute("onmouseout", `this.style = "${generalStyle}";`);
this.restoreModalProperties();
if (typeof this.resizerBR !== "undefined" && typeof this.resizerTL !== "undefined") {
this.setResizeButtonsVisibility();
}
// Need recalculate position of actual modal because window can was changed in fullscreenmode.
this.recalculateScrollBar();
this.recalculatePosition();
this.recalculateScale();
this.focus();
}
/**
* Minimizes the modal object.
*/
minimize() {
// Saving width, height, top and bottom parameters to restore when opening.
this.saveModalProperties();
this.title.style.cursor = "pointer";
if (this.properties.state === "minimized" && this.properties.previousState === "stack") {
this.stack();
} else if (this.properties.state === "minimized" && this.properties.previousState === "maximized") {
this.maximize();
} else {
// Setting css to prevent important tag into css style.
this.container.style.height = "30px";
this.container.style.width = "250px";
this.container.style.bottom = "0px";
this.container.style.right = "10px";
this.removeListeners();
this.properties.previousState = this.properties.state;
this.properties.state = "minimized";
this.setResizeButtonsVisibility();
this.minimizeDiv.title = StringManager.get("maximize");
if (Util.containsClass(this.overlay, "wrs_stack")) {
this.removeClass("wrs_stack");
} else {
this.removeClass("wrs_maximized");
}
this.addClass("wrs_minimized");
// Change minimize icon to maximize icon
const generalStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(maxIcon)})`;
const hoverStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(maxHoverIcon)})`;
this.minimizeDiv.setAttribute("style", generalStyle);
this.minimizeDiv.setAttribute("onmouseover", `this.style = "${hoverStyle}";`);
this.minimizeDiv.setAttribute("onmouseout", `this.style = "${generalStyle}";`);
}
}
/**
* Maximizes the modal object.
*/
maximize() {
// Saving width, height, top and bottom parameters to restore when opening.
this.saveModalProperties();
if (this.properties.state !== "maximized") {
this.properties.previousState = this.properties.state;
this.properties.state = "maximized";
}
// Don't permit resize on maximize mode.
this.setResizeButtonsVisibility();
if (Util.containsClass(this.overlay, "wrs_minimized")) {
this.minimizeDiv.title = StringManager.get("minimize");
this.removeClass("wrs_minimized");
} else if (Util.containsClass(this.overlay, "wrs_stack")) {
this.container.style.left = null;
this.container.style.top = null;
this.removeClass("wrs_stack");
}
this.addClass("wrs_maximized");
// Change maximize icon to minimize icon
const generalStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(minIcon)})`;
const hoverStyle = `background-size: 10px; background-repeat: no-repeat; background-image: url(data:image/svg+xml;base64,${window.btoa(minHoverIcon)})`;
this.minimizeDiv.setAttribute("style", generalStyle);
this.minimizeDiv.setAttribute("onmouseover", `this.style = "${hoverStyle}";`);
this.minimizeDiv.setAttribute("onmouseout", `this.style = "${generalStyle}";`);
// Set size to 80% screen with a max size.
this.setSize(parseInt(window.innerHeight * 0.8, 10), parseInt(window.innerWidth * 0.8, 10));
if (this.container.clientHeight > 700) {
this.container.style.height = "700px";
}
if (this.container.clientWidth > 1200) {
this.container.style.width = "1200px";
}
// Setting modal position in center on screen.
const { innerHeight } = window;
const { innerWidth } = window;
const { offsetHeight } = this.container;
const { offsetWidth } = this.container;
const bottom = innerHeight / 2 - offsetHeight / 2;
const right = innerWidth / 2 - offsetWidth / 2;
this.setPosition(bottom, right);
this.recalculateScale();
this.recalculatePosition();
this.recalculateSize();
this.focus();
}
/**
* Expand again the modal object from a minimized state.
*/
reExpand() {
if (this.properties.state === "minimized") {
if (this.properties.previousState === "maximized") {
this.maximize();
} else {
this.stack();
}
this.title.style.cursor = "";
}
}
/**
* Sets modal size.
* @param {Number} height - Height of the ModalDialog
* @param {Number} width - Width of the ModalDialog.
*/
setSize(height, width) {
this.container.style.height = `${height}px`;
this.container.style.width = `${width}px`;
this.recalculateSize();
}
/**
* Sets modal position using bottom and right style attributes.
* @param {number} bottom - bottom attribute.
* @param {number} right - right attribute.
*/
setPosition(bottom, right) {
this.container.style.bottom = `${bottom}px`;
this.container.style.right = `${right}px`;
}
/**
* Saves position and size parameters of and open ModalDialog. This attributes
* are needed to restore it on re-open.
*/
saveModalProperties() {
// Saving values of modal only when modal is in stack state.
if (this.properties.state === "stack") {
this.properties.position.bottom = parseInt(this.container.style.bottom, 10);
this.properties.position.right = parseInt(this.container.style.right, 10);
this.properties.size.width = parseInt(this.container.style.width, 10);
this.properties.size.height = parseInt(this.container.style.height, 10);
}
}
/**
* Restore ModalDialog position and size parameters.
*/
restoreModalProperties() {
if (this.properties.state === "stack") {
// Restoring Bottom and Right values from last modal.
this.setPosition(this.properties.position.bottom, this.properties.position.right);
// Restoring Height and Left values from last modal.
this.setSize(this.properties.size.height, this.properties.size.width);
}
}
/**
* Sets the modal dialog initial size.
*/
recalculateSize() {
this.contentContainer.style.height = `${parseInt(this.wrapper.offsetHeight - 50, 10)}px`;
}
/**
* Enable or disable visibility of resize buttons in modal window depend on state.
*/
setResizeButtonsVisibility() {
if (this.properties.state === "stack") {
this.resizerTL.style.visibility = "visible";
this.resizerBR.style.visibility = "visible";
} else {
this.resizerTL.style.visibility = "hidden";
this.resizerBR.style.visibility = "hidden";
}
}
/**
* Makes an object draggable adding mouse and touch events.
*/
addListeners() {
// Button events (maximize, minimize, stack and close).
this.maximizeDiv.addEventListener("click", this.maximize.bind(this), true);
this.stackDiv.addEventListener("click", this.stack.bind(this), true);
this.minimizeDiv.addEventListener("click", this.minimize.bind(this), true);
this.closeDiv.addEventListener("click", this.cancelAction.bind(this));
this.maximizeDiv.addEventListener(
"keypress",
(e) => {
if (e.key === "Enter" || e.key === " " || e.keyCode === 13 || e.keyCode === 32) {
// Handle enter and space.
e.target.click();
}
},
true,
);
this.stackDiv.addEventListener(
"keypress",
(e) => {
if (e.key === "Enter" || e.key === " " || e.keyCode === 13 || e.keyCode === 32) {
// Handle enter and space.
e.target.click();
e.preventDefault();
}
},
true,
);
this.minimizeDiv.addEventListener(
"keypress",
(e) => {
if (e.key === "Enter" || e.key === " " || e.keyCode === 13 || e.keyCode === 32) {
// Handle enter and space.
e.target.click();
e.preventDefault();
}
},
true,
);
this.closeDiv.addEventListener("keypress", (e) => {
if (e.key === "Enter" || e.key === " " || e.keyCode === 13 || e.keyCode === 32) {
// Handle enter and space.
e.target.click();
e.preventDefault();
}
});
this.title.addEventListener("click", this.reExpand.bind(this));
// Overlay events (close).
this.overlay.addEventListener("click", this.cancelAction.bind(this));
// Mouse events.
Util.addEvent(window, "mousedown", this.startDrag.bind(this));
Util.addEvent(window, "mouseup", this.stopDrag.bind(this));
Util.addEvent(window, "mousemove", this.drag.bind(this));
Util.addEvent(window, "resize", this.onWindowResize.bind(this));
// Key events.
Util.addEvent(window, "keydown", this.onKeyDown.bind(this));
}
/**
* Removes draggable events from an object.
*/
removeListeners() {
// Mouse events.
Util.removeEvent(window, "mousedown", this.startDrag);
Util.removeEvent(window, "mouseup", this.stopDrag);
Util.removeEvent(window, "mousemove", this.drag);
Util.removeEvent(window, "resize", this.onWindowResize);
// Key events.
Util.removeEvent(window, "keydown", this.onKeyDown);
}
/**
* Returns mouse or touch coordinates (on touch events ev.ClientX doesn't exists)
* @param {MouseEvent} mouseEvent - Mouse event.
* @return {Object} With the X and Y coordinates.
*/
// eslint-disable-next-line class-methods-use-this
eventClient(mouseEvent) {
if (typeof mouseEvent.clientX === "undefined" && mouseEvent.changedTouches) {
const client = {
X: mouseEvent.changedTouches[0].clientX,
Y: mouseEvent.changedTouches[0].clientY,
};
return client;
}
const client = {
X: mouseEvent.clientX,
Y: mouseEvent.clientY,
};
return client;
}
/**
* Start drag function: set the object dragDataObject with the draggable
* object offsets coordinates.
* when drag starts (on touchstart or mousedown events).
* @param {MouseEvent} mouseEvent - Touchstart or mousedown event.
*/
startDrag(mouseEvent) {
if (this.properties.state === "minimized") {
return;
}
if (mouseEvent.target === this.title) {
if (typeof this.dragDataObject === "undefined" || this.dragDataObject === null) {
// Save first click mouse point on screen.
this.dragDataObject = {
x: this.eventClient(mouseEvent).X,
y: this.eventClient(mouseEvent).Y,
};
// Reset last drag position when start drag.
this.lastDrag = {
x: "0px",
y: "0px",
};
// Init right and bottom values for window modal if it isn't exist.
if (this.container.style.right === "") {
this.container.style.right = "0px";
}
if (this.container.style.bottom === "") {
this.container.style.bottom = "0px";
}
// Needed for IE11 for apply disabled mouse events on editor because
// internet explorer needs a dynamic object to apply this property.
if (this.isIE11()) {
// this.iframe.style['position'] = 'relative';
}
// Apply class for disable involuntary select text when drag.
Util.addClass(document.body, "wrs_noselect");
Util.addClass(this.overlay, "wrs_overlay_active");
// Obtain screen limits for prevent overflow.
this.limitWindow = this.getLimitWindow();
}
}
}
/**
* Updates dragDataObject with the draggable object coordinates when
* the draggable object is being moved.
* @param {MouseEvent} mouseEvent - The mouse event.
*/
drag(mouseEvent) {
if (this.dragDataObject) {
mouseEvent.preventDefault();
// Calculate max and min between actual mouse position and limit of screeen.
// It restric the movement of modal into window.
let limitY = Math.min(this.eventClient(mouseEvent).Y, this.limitWindow.minPointer.y);
limitY = Math.max(this.limitWindow.maxPointer.y, limitY);
let limitX = Math.min(this.eventClient(mouseEvent).X, this.limitWindow.minPointer.x);
limitX = Math.max(this.limitWindow.maxPointer.x, limitX);
// Subtract limit with first position to obtain relative pixels increment
// to the anchor point.
const dragX = `${limitX - this.dragDataObject.x}px`;
const dragY = `${limitY - this.dragDataObject.y}px`;
// Save last valid position of modal before window overflow.
this.lastDrag = {
x: dragX,
y: dragY,
};
// This move modal with hardware acceleration.
this.container.style.transform = `translate3d(${dragX},${dragY},0)`;
}
if (this.resizeDataObject) {
const { innerWidth } = window;
const { innerHeight } = window;
let limitX = Math.min(this.eventClient(mouseEvent).X, innerWidth - this.scrollbarWidth - 7);
let limitY = Math.min(this.eventClient(mouseEvent).Y, innerHeight - 7);
if (limitX < 0) {
limitX = 0;
}
if (limitY < 0) {
limitY = 0;
}
let scaleMultiplier;
if (this.leftScale) {
scaleMultiplier = -1;
} else {
scaleMultiplier = 1;
}
this.container.style.width = `${this.initialWidth + scaleMultiplier * (limitX - this.resizeDataObject.x)}px`;
this.container.style.height = `${this.initialHeight + scaleMultiplier * (limitY - this.resizeDataObject.y)}px`;
if (!this.leftScale) {
if (this.resizeDataObject.x - limitX - this.initialWidth < -580) {
this.container.style.right = `${this.initialRight - (limitX - this.resizeDataObject.x)}px`;
} else {
this.container.style.right = `${this.initialRight + this.initialWidth - 580}px`;
this.container.style.width = "580px";
}
if (this.resizeDataObject.y - limitY < this.initialHeight - 338) {
this.container.style.bottom = `${this.initialBottom - (limitY - this.resizeDataObject.y)}px`;
} else {
this.container.style.bottom = `${this.initialBottom + this.initialHeight - 338}px`;
this.container.style.height = "338px";
}
}
this.recalculateScale();
this.recalculatePosition();
}
}
/**
* Returns the boundaries of actual window to limit modal movement.
* @return {Object} Object containing mouseX and mouseY coordinates of actual mouse on screen.
*/
getLimitWindow() {
// Obtain dimensions of window page.
const maxWidth = window.innerWidth;
const maxHeight = window.innerHeight;
// Calculate relative position of mouse point into window.
const { offsetHeight } = this.container;
const contStyleBottom = parseInt(this.container.style.bottom, 10);
const contStyleRight = parseInt(this.container.style.right, 10);
const { pageXOffset } = window;
const dragY = this.dragDataObject.y;
const dragX = this.dragDataObject.x;
const offSetToolbarY = offsetHeight + contStyleBottom - (maxHeight - (dragY - pageXOffset));
const offSetToolbarX = maxWidth - this.scrollbarWidth - (dragX - pageXOffset) - contStyleRight;
// Calculate limits with sizes of window, modal and mouse position.
const minPointerY = maxHeight - this.container.offsetHeight + offSetToolbarY;
const maxPointerY = this.title.offsetHeight - (this.title.offsetHeight - offSetToolbarY);
const minPointerX = maxWidth - offSetToolbarX - this.scrollbarWidth;
const maxPointerX = this.container.offsetWidth - offSetToolbarX;
const minPointer = { x: minPointerX, y: minPointerY };
const maxPointer = { x: maxPointerX, y: maxPointerY };
return { minPointer, maxPointer };
}
/**
* Returns the scrollbar width size of browser
* @returns {Number} The scrollbar width.
*/
// eslint-disable-next-line class-methods-use-this
getScrollBarWidth() {
// Create a paragraph with full width of page.
const inner = document.createElement("p");
inner.style.width = "100%";
inner.style.height = "200px";
// Create a hidden div to compare sizes.
const outer = document.createElement("div");
outer.style.position = "absolute";
outer.style.top = "0px";
outer.style.left = "0px";
outer.style.visibility = "hidden";
outer.style.width = "200px";
outer.style.height = "150px";
outer.style.overflow = "hidden";
outer.appendChild(inner);
document.body.appendChild(outer);
const widthOuter = inner.offsetWidth;
// Change type overflow of paragraph for measure scrollbar.
outer.style.overflow = "scroll";
let widthInner = inner.offsetWidth;
// If measure is the same, we compare with internal div.
if (widthOuter === widthInner) {
widthInner = outer.clientWidth;
}
document.body.removeChild(outer);
return widthOuter - widthInner;
}
/**
* Set the dragDataObject to null.
*/
stopDrag() {
// Due to we have multiple events that call this function, we need only to execute
// the next modifiers one time,
// when the user stops to drag and dragDataObject is not null (the object to drag is attached).
if (this.dragDataObject || this.resizeDataObject) {
// If modal doesn't change, it's not necessary to set position with interpolation.
this.container.style.transform = "";
if (this.dragDataObject) {
this.container.style.right = `${parseInt(this.container.style.right, 10) - parseInt(this.lastDrag.x, 10)}px`;
this.container.style.bottom =