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 TelemetryService from './telemetry';
import IntegrationModel from './integrationmodel';
import closeIcon from '!!raw-loader!../styles/icons/general/close_icon.svg'; //eslint-disable-line
import closeHoverIcon from '!!raw-loader!../styles/icons/hover/close_icon_h.svg'; //eslint-disable-line
import fullsIcon from '!!raw-loader!../styles/icons/general/fulls_icon.svg'; //eslint-disable-line
import fullsHoverIcon from '!!raw-loader!../styles/icons/hover/fulls_icon_h.svg'; //eslint-disable-line
import minIcon from '!!raw-loader!../styles/icons/general/min_icon.svg'; //eslint-disable-line
import minHoverIcon from '!!raw-loader!../styles/icons/hover/min_icon_h.svg'; //eslint-disable-line
import minsIcon from '!!raw-loader!../styles/icons/general/mins_icon.svg'; //eslint-disable-line
import minsHoverIcon from '!!raw-loader!../styles/icons/hover/mins_icon_h.svg'; //eslint-disable-line
import maxIcon from '!!raw-loader!../styles/icons/general/max_icon.svg'; //eslint-disable-line
import maxHoverIcon from '!!raw-loader!../styles/icons/hover/max_icon_h.svg'; //eslint-disable-line
/**
* @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 = `100%${this.iosMeasureUnit}`;
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 = isLandscape || isPortrait;
// Obtain number of current instance.
this.instanceId = document.getElementsByClassName('wrs_modal_dialogContainer').length;
// Device object properties.
/**
* @type {DeviceProperties}
*/
this.deviceProperties = {
orientation: landscape ? 'landscape' : 'portait',
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 postion.
* @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 = '';
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');
// 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}";`);
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');
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}";`);
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');
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}";`);
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');
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}";`);
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'),
},
this.submitAction.bind(this),
);
this.cancelButton = this.createSubmitButton(
{
id: this.getElementId('wrs_modal_button_cancel'),
class: 'wrs_modal_button_cancel',
innerHTML: StringManager.get('cancel'),
},
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(); },
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.
*/
submitAction() {
if (typeof this.contentManager.submitAction !== 'undefined') {
this.contentManager.submitAction();
}
this.close();
}
/**
* This method is called when the modal object has been cancelled. If
* contentElement has implemented hasChanges method, a confirm popup
* will be shown if hasChanges returns true.
*/
cancelAction() {
if (typeof this.contentManager.hasChanges === 'undefined') {
this.close();
} else if (!this.contentManager.hasChanges()) {
this.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;
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 = '◢';
// This is a definition of Resize Button Top-Left.
this.resizerTL = document.createElement('div');
this.resizerTL.className = 'wrs_bottom_left_resizer';
// 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() {
// TODO Change the call to telemetry to use events.
try {
TelemetryService.send([{
timestamp: new Date().toJSON(),
topic: '0',
level: 'info',
message: 'HELO telemetry.wiris.net',
}])
.then((response) => {
// TODO: manage retries for codes
// DEBUG
// console.log('modal.open TelemetryService.send - response:', response);
});
} catch (error) {
// DEBUG
// console.log('modal.open TelemetryService.send - error:', error);
}
// 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;
this.setContainerHeight(`${100 + this.iosMeasureUnit}`);
}
}
if (!ContentManager.isEditorLoaded()) {
const listener = Listeners.newListener('onLoad', () => {
this.contentManager.onOpen(this);
});
this.contentManager.addListener(listener);
} else {
this.contentManager.onOpen(this);
}
}
/**
* Closes modal window and restores viewport header.
*/
close() {
// Set temporal image to null to avoid
// opening a existing formula editor when trying to open a new one
IntegrationModel.setTemporalImageToNull();
this.removeClass('wrs_maximized');
this.removeClass('wrs_minimized');
this.removeClass('wrs_stack');
this.addClass('wrs_closed');
this.saveModalProperties();
this.unlockWebsiteScroll();
this.properties.open = 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 openning.
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.wrapper.style.width = `${this.container.clientWidth - 12}px`;
this.wrapper.style.height = `${this.container.clientHeight - 38}px`;
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.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(this.container, '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(this.container, '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
// iexplorer need a dinamic 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);
// Substract 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 hadware 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 = `${parseInt(this.container.style.bottom, 10) - parseInt(this.lastDrag.y, 10)}px`;
}
// We make focus on editor after drag modal windows to prevent lose focus.
this.focus();
// Restore mouse events on iframe.
// this.iframe.style['pointer-events'] = 'auto';
document.body.style['user-select'] = '';
// Restore static state of iframe if we use Internet Explorer.
if (this.isIE11()) {
// this.iframe.style['position'] = null;
}
// Active text select event.
Util.removeClass(document.body, 'wrs_noselect');
Util.removeClass(this.overlay, 'wrs_overlay_active');
}
this.dragDataObject = null;
this.resizeDataObject = null;
this.initialWidth = null;
this.leftScale = null;
}
/**
* Recalculates scale for modal when resize browser window.
*/
onWindowResize() {
this.recalculateScrollBar();
this.recalculatePosition();
this.recalculateScale();
}
/**
* Triggers keyboard events:
* - Tab key tab to go to submit button.
* - Esc key to close the modal dialog.
* @param {KeyboardEvent} keyboardEvent - The keyboard event.
*/
onKeyDown(keyboardEvent) {
if (keyboardEvent.key !== undefined) {
// Popupmessage is not oppened.
if (this.popup.overlayWrapper.style.display !== 'block') {
// Code to detect Esc event
if (keyboardEvent.key === 'Escape' || keyboardEvent.key === 'Esc') {
if (this.properties.open) {
this.contentManager.onKeyDown(keyboardEvent);
}
} else if (keyboardEvent.shiftKey && keyboardEvent.key === 'Tab') { // Code to detect shift Tab event.
if (document.activeElement === this.cancelButton) {
this.submitButton.focus();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
} else {
this.contentManager.onKeyDown(keyboardEvent);
}
} else if (keyboardEvent.key === 'Tab') { // Code to detect Tab event.
if (document.activeElement === this.submitButton) {
this.cancelButton.focus();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
} else {
this.contentManager.onKeyDown(keyboardEvent);
}
}
} else { // Popupmessage oppened.
this.popup.onKeyDown(keyboardEvent);
}
}
}
/**
* Recalculating position for modal dialog when the browser is resized.
*/
recalculatePosition() {
this.container.style.right = `${Math.min(parseInt(this.container.style.right, 10), window.innerWidth - this.scrollbarWidth - this.container.offsetWidth)}px`;
if (parseInt(this.container.style.right, 10) < 0) {
this.container.style.right = '0px';
}
this.container.style.bottom = `${Math.min(parseInt(this.container.style.bottom, 10), window.innerHeight - this.container.offsetHeight)}px`;
if (parseInt(this.container.style.bottom, 10) < 0) {
this.container.style.bottom = '0px';
}
}
/**
* Recalculating scale for modal when the browser is resized.
*/
recalculateScale() {
let sizeModificated = false;
if (parseInt(this.container.style.width, 10) > 580) {
this.container.style.width = `${Math.min(parseInt(this.container.style.width, 10), window.innerWidth - this.scrollbarWidth)}px`;
sizeModificated = true;
} else {
this.container.style.width = '580px';
sizeModificated = true;
}
if (parseInt(this.container.style.height, 10) > 338) {
this.container.style.height = `${Math.min(parseInt(this.container.style.height, 10), window.innerHeight)}px`;
sizeModificated = true;
} else {
this.container.style.height = '338px';
sizeModificated = true;
}
if (sizeModificated) {
this.recalculateSize();
}
}
/**
* Recalculating width of browser scroll bar.
*/
recalculateScrollBar() {
this.hasScrollBar = window.innerWidth > document.documentElement.clientWidth;
if (this.hasScrollBar) {
this.scrollbarWidth = this.getScrollBarWidth();
} else {
this.scrollbarWidth = 0;
}
}
/**
* Hide soft keyboards on iOS devices.
*/
// eslint-disable-next-line class-methods-use-this
hideKeyboard() {
// iOS keyboard can't be detected or hide directly from JavaScript.
// So, this method simulates that user focus a text input and blur
// the selection.
const inputField = document.createElement('input');
this.container.appendChild(inputField);
inputField.focus();
inputField.blur();
// Is removed to not see it.
inputField.remove();
}
/**
* Focus to contentManager object.
*/
focus() {
if (this.contentManager != null && typeof this.contentManager.onFocus !== 'undefined') {
this.contentManager.onFocus();
}
}
/**
* Returns true when the device is on portrait mode.
*/
// eslint-disable-next-line class-methods-use-this
portraitMode() {
return window.innerHeight > window.innerWidth;
}
/**
* Event handler that change container size when IOS softkeyboard is opened.
*/
handleOpenedIosSoftkeyboard() {
if (!this.iosSoftkeyboardOpened && this.iosDivHeight != null && this.iosDivHeight === `100${this.iosMeasureUnit}`) {
if (this.portraitMode()) {
this.setContainerHeight(`63${this.iosMeasureUnit}`);
} else {
this.setContainerHeight(`40${this.iosMeasureUnit}`);
}
}
this.iosSoftkeyboardOpened = true;
}
/**
* Event handler that change container size when IOS softkeyboard is closed.
*/
handleClosedIosSoftkeyboard() {
this.iosSoftkeyboardOpened = false;
this.setContainerHeight(`100${this.iosMeasureUnit}`);
}
/**
* Change container sizes when orientation is changed on iOS.
*/
orientationChangeIosSoftkeyboard() {
if (this.iosSoftkeyboardOpened) {
if (this.portraitMode()) {
this.setContainerHeight(`63${this.iosMeasureUnit}`);
} else {
this.setContainerHeight(`40${this.iosMeasureUnit}`);
}
} else {
this.setContainerHeight(`100${this.iosMeasureUnit}`);
}
}
/**
* Change container sizes when orientation is changed on Android.
*/
orientationChangeAndroidSoftkeyboard() {
this.setContainerHeight('100%');
}
/**
* Set iframe container height.
* @param {Number} height - New height.
*/
setContainerHeight(height) {
this.iosDivHeight = height;
this.wrapper.style.height = height;
}
/**
* Check content of editor before close action.
*/
showPopUpMessage() {
if (this.properties.state === 'minimized') {
this.stack();
}
this.popup.show();
}
/**
* Sets the tithle of the modal dialog.
* @param {String} title - Modal dialog title.
*/
setTitle(title) {
this.title.innerHTML = title;
}
/**
* Returns the id of an element, adding the instance number to
* the element class name:
* className --> className[idNumber]
* @param {String} className - The element class name.
* @returns {String} A string appending the instance id to the className.
*/
getElementId(className) {
return `${className}[${this.instanceId}]`;
}
}