collabora-online/browser/src/control/Control.JSDialog.js

861 lines
29 KiB
JavaScript

/*
* Copyright the Collabora Online contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* L.Control.JSDialog - class which creates and updates dialogs, popups, snackbar
*/
/* global JSDialog Hammer app _ */
L.Control.JSDialog = L.Control.extend({
options: {
snackbarTimeout: 10000
},
dialogs: {},
draggingObject: null,
onAdd: function (map) {
this.map = map;
this.map.on('jsdialog', this.onJSDialog, this);
this.map.on('jsdialogupdate', this.onJSUpdate, this);
this.map.on('jsdialogaction', this.onJSAction, this);
this.map.on('zoomend', this.onZoomEnd, this);
this.map.on('closealldialogs', this.onCloseAll, this);
this.map.on('closeAutoFilterDialog', this.closeAutoFilterDialogsOnTabChange, this);
},
onRemove: function() {
this.map.off('jsdialog', this.onJSDialog, this);
this.map.off('jsdialogupdate', this.onJSUpdate, this);
this.map.off('jsdialogaction', this.onJSAction, this);
this.map.off('zoomend', this.onZoomEnd, this);
this.map.off('closealldialogs', this.onCloseAll, this);
this.map.off('closeAutoFilterDialog', this.closeAutoFilterDialogsOnTabChange, this);
},
hasDialogOpened: function() {
var dialogs = this.dialogs;
return Object.keys(dialogs)
.filter(function (key) {
return key != 'snackbar' && dialogs[key].isDropdown !== true;
})
.length > 0;
},
hasDropdownOpened: function() {
return Object.values(this.dialogs)
.filter(function (dialog) { return dialog.isDropdown === true; })
.length > 0;
},
hasSnackbarOpened: function() {
return Object.keys(this.dialogs)
.filter(function (key) { return key == 'snackbar'; })
.length > 0;
},
clearDialog: function(id) {
var builder = this.dialogs[id].builder;
L.DomUtil.remove(this.dialogs[id].container);
if (this.dialogs[id].overlay && !this.dialogs[id].isSubmenu)
L.DomUtil.remove(this.dialogs[id].overlay);
delete this.dialogs[id];
return builder;
},
close: function(id, sendCloseEvent) {
if (id && this.dialogs[id]) {
if (!sendCloseEvent && this.dialogs[id].overlay && !this.dialogs[id].isSubmenu)
L.DomUtil.remove(this.dialogs[id].overlay);
if (this.dialogs[id].timeoutId)
clearTimeout(this.dialogs[id].timeoutId);
if (this.dialogs[id].isPopup)
this.closePopover(id, sendCloseEvent);
else
this.closeDialog(id, sendCloseEvent);
}
},
closeAll: function(leaveSnackbar) {
var dialogs = Object.keys(this.dialogs);
for (var i = 0; i < dialogs.length; i++) {
if (leaveSnackbar && dialogs[i] && dialogs[i] === 'snackbar')
continue;
this.close(dialogs[i], app.idleHandler._active);
}
},
closeAllDropdowns: function() {
var dialogs = Object.values(this.dialogs);
for (var i = 0; i < dialogs.length; i++) {
if (dialogs[i] && !dialogs[i].isDropdown)
continue;
this.close(dialogs[i].id, false);
}
},
closeDialog: function(id, sendCloseEvent) {
if (!id || !this.dialogs[id]) {
console.warn('missing dialog data');
return;
}
this.focusToLastElement(id);
var builder = this.clearDialog(id);
if (sendCloseEvent !== false && builder)
builder.callback('dialog', 'close', {id: '__DIALOG__'}, null, builder);
},
// sendCloseEvent means that we only send a command to the server
// we want to kill HTML popup when we receive feedback from the server
closePopover: function(id, sendCloseEvent) {
if (!id || !this.dialogs[id]) {
console.warn('missing popover data');
return;
}
var clickToClose = this.dialogs[id].clickToClose;
var builder = this.dialogs[id].builder;
if (sendCloseEvent) {
// first try to close the dropdown if exists
if (clickToClose && typeof clickToClose.closeDropdown === 'function')
clickToClose.closeDropdown();
if (clickToClose && L.DomUtil.hasClass(clickToClose, 'menubutton'))
clickToClose.click();
else if (builder)
builder.callback('popover', 'close', {id: '__POPOVER__'}, null, builder);
else
console.warn('closePopover: no builder');
}
else {
this.clearDialog(id);
}
this.focusToLastElement(id);
},
onCloseAll: function() {
this.closeAll(/*leaveSnackbar*/ true);
// should also close all dropdowns on close all dialogs
this.closeAllDropdowns();
},
focusToLastElement: function(id) {
try {
this.dialogs[id].lastFocusedElement.focus();
}
catch (error) {
this.map.focus();
}
},
setTabs: function() {
console.error('setTabs: not implemented in dialogs.');
},
selectedTab: function() {
// nothing to do here
},
_getDefaultButtonId: function(widgets) {
for (var i in widgets) {
if (widgets[i].type === 'pushbutton' || widgets[i].type === 'okbutton') {
if (widgets[i].has_default === true)
return widgets[i].id;
}
if (widgets[i].children) {
var found = this._getDefaultButtonId(widgets[i].children);
if (found)
return found;
}
}
return null;
},
fadeOutDialog: function(instance) {
if (instance.id && this.dialogs[instance.id]) {
var container = this.dialogs[instance.id].container;
L.DomUtil.addClass(container, 'fadeout');
container.onanimationend = function() { instance.that.close(instance.id, false); };
// be sure it will be removed
setTimeout(function() { instance.that.close(instance.id, false); }, 700);
}
},
getOrCreateOverlay: function(instance) {
// Submenu is created inside the same overlay as parent dropdown
if (instance.isDropdown && instance.isSubmenu) {
instance.overlay = document.body.querySelector('.jsdialog-overlay');
return;
}
// Dialogue overlay which will allow automatic positioning and cancellation of the dialogue if cancellable.
var overlay = L.DomUtil.get(instance.id + '-overlay');
if (!overlay) {
overlay = L.DomUtil.create('div', 'jsdialog-overlay ' + (instance.cancellable && !instance.hasOverlay ? 'cancellable' : ''), instance.containerParent);
overlay.id = instance.id + '-overlay';
if (instance.cancellable) {
// dropdowns are online-only components, don't exist in core
var hasToNotifyServer = !instance.isDropdown;
overlay.onclick = function () { this.close(instance.id, hasToNotifyServer); }.bind(this);
}
}
instance.overlay = overlay;
},
createContainer: function(instance, parentContainer) {
// it has to be form to handle default button
instance.container = L.DomUtil.create('div', 'jsdialog-window', parentContainer);
instance.container.setAttribute('role', 'dialog');
instance.container.id = instance.id;
instance.form = L.DomUtil.create('form', 'jsdialog-container ui-dialog ui-widget-content lokdialog_container', instance.container);
// Prevent overlay from getting the click, except if we want click to dismiss
// Like in the case of the inactivity message.
// https://github.com/CollaboraOnline/online/issues/7403
if (!instance.clickToDismiss) {
instance.container.onclick = function(e) { e.stopPropagation(); };
}
if (instance.collapsed && (instance.collapsed === 'true' || instance.collapsed === true))
L.DomUtil.addClass(instance.container, 'collapsed');
// prevent from reloading
instance.form.addEventListener('submit', function (event) { event.preventDefault(); });
instance.defaultButtonId = this._getDefaultButtonId(instance.children);
if (instance.children && instance.children.length &&
instance.children[0].children && instance.children[0].children.length === 1)
instance.isOnlyChild = true;
// it has to be first button in the form
var defaultButton = L.DomUtil.createWithId('button', 'default-button', instance.form);
defaultButton.style.display = 'none';
defaultButton.onclick = function() {
if (instance.defaultButtonId) {
var button = instance.form.querySelector('#' + instance.defaultButtonId);
if (button)
button.click();
}
};
if (instance.haveTitlebar) {
instance.titlebar = L.DomUtil.create('div', 'ui-dialog-titlebar ui-corner-all ui-widget-header ui-helper-clearfix', instance.form);
var title = L.DomUtil.create('h2', 'ui-dialog-title', instance.titlebar);
title.innerText = instance.title;
instance.titleCloseButton = L.DomUtil.create('button', 'ui-button ui-corner-all ui-widget ui-button-icon-only ui-dialog-titlebar-close', instance.titlebar);
instance.titleCloseButton.setAttribute('aria-label', _('Close dialog'));
instance.titleCloseButton.tabIndex = '-1';
L.DomUtil.create('span', 'ui-button-icon ui-icon ui-icon-closethick', instance.titleCloseButton);
}
if (instance.isModalPopUp || instance.isDocumentAreaPopup || instance.isSnackbar)
L.DomUtil.addClass(instance.container, 'modalpopup');
if (instance.isModalPopUp && !instance.popupParent) // Special case for menu popups (they are also modal dialogues).
instance.overlay.classList.add('dimmed');
if (instance.isSnackbar) {
L.DomUtil.addClass(instance.container, 'snackbar');
L.DomUtil.addClass(instance.form, 'snackbar');
}
instance.content = L.DomUtil.create('div', 'lokdialog ui-dialog-content ui-widget-content', instance.form);
this.dialogs[instance.id] = {};
},
createDialog: function(instance) {
instance.builder = new L.control.jsDialogBuilder(
{
windowId: instance.id,
mobileWizard: this,
map: this.map,
cssClass: 'jsdialog' + (instance.isAutofilter ? ' autofilter' : '') + (instance.isOnlyChild ? ' one-child-popup' : ''),
callback: instance.callback
});
instance.builder.build(instance.content, [instance]);
instance.builder.setContainer(instance.content);
var primaryBtn = instance.content.querySelector('#' + instance.defaultButtonId);
if (primaryBtn)
L.DomUtil.addClass(primaryBtn, 'button-primary');
},
addFocusHandler: function(instance) {
var failedToFindFocus = function() {
if (document.getElementById(instance.init_focus_id))
document.getElementById(instance.init_focus_id).focus();
else {
app.console.error('There is no focusable element in the modal. Either focusId should be given or modal should have a response button.');
instance.that.close(instance.id, true);
instance.that.map.focus();
}
};
JSDialog.MakeFocusCycle(instance.container, failedToFindFocus);
},
addHandlers: function(instance) {
var onInput = function(ev) {
if (ev.isFirst)
instance.that.draggingObject = instance.that.dialogs[instance.id];
if (ev.isFinal && instance.that.draggingObject
&& instance.that.draggingObject.translateX
&& instance.that.draggingObject.translateY) {
instance.that.draggingObject.startX = instance.that.draggingObject.translateX;
instance.that.draggingObject.startY = instance.that.draggingObject.translateY;
instance.that.draggingObject.translateX = 0;
instance.that.draggingObject.translateY = 0;
instance.that.draggingObject = null;
}
};
if (instance.haveTitlebar) {
instance.titleCloseButton.onclick = function() {
instance.that.close(instance.id, true);
};
}
if (instance.nonModal) {
instance.titleCloseButton.onclick = function() {
var newestDialog = Math.max.apply(null,
Object.keys(instance.that.dialogs).map(function(i) { return parseInt(i);}));
if (newestDialog > parseInt(instance.id))
return;
instance.that.closeDialog(instance.id, true);
};
var hammerTitlebar = new Hammer(instance.titlebar);
hammerTitlebar.add(new Hammer.Pan({ threshold: 20, pointers: 0 }));
hammerTitlebar.on('panstart', this.onPan.bind(this));
hammerTitlebar.on('panmove', this.onPan.bind(this));
hammerTitlebar.on('hammer.input', onInput);
}
var popupParent = instance.popupParent ? L.DomUtil.get(instance.popupParent) : null;
this.addFocusHandler(instance); // Loop focus for all dialogues.
var clickToCloseId = instance.clickToClose;
if (clickToCloseId && clickToCloseId.indexOf('.uno:') === 0)
clickToCloseId = clickToCloseId.substr('.uno:'.length);
var clickToCloseElement = null;
if (clickToCloseId && popupParent) {
clickToCloseElement = popupParent.querySelector('[id=\'' + clickToCloseId + '\']');
// we avoid duplicated ids in unotoolbuttons - try with class
if (!clickToCloseElement)
clickToCloseElement = popupParent.querySelector('.uno' + clickToCloseId);
} else if (clickToCloseId) {
// fallback
clickToCloseElement = L.DomUtil.get(clickToCloseId);
}
instance.clickToClose = clickToCloseElement;
// setup initial focus and helper elements for closing popup
var initialFocusElement = JSDialog.GetFocusableElements(instance.container);
if (instance.canHaveFocus && initialFocusElement && initialFocusElement.length)
initialFocusElement[0].focus();
var focusWidget = instance.init_focus_id ? instance.container.querySelector('[id=\'' + instance.init_focus_id + '\']') : null;
if (focusWidget)
focusWidget.focus();
if (focusWidget && document.activeElement !== focusWidget) {
var firstFocusable = JSDialog.GetFocusableElements(focusWidget);
if (firstFocusable && firstFocusable.length)
firstFocusable[0].focus();
else
console.error('cannot get focus for widget: "' + instance.init_focus_id + '"');
}
if (instance.isDropdown && instance.isSubmenu) {
instance.container.addEventListener('mouseleave', function () {
instance.builder.callback('combobox', 'hidedropdown', {id: instance.id}, null, instance.builder);
});
}
},
/// if you use updatePos - instance param is binded automatically
setPosition: function(instance, updatedPos) {
var calculated = false;
var isRTL = document.documentElement.dir === 'rtl';
if (instance.isSnackbar) {
calculated = true;
instance.posx = window.innerWidth/2 - instance.form.offsetWidth/2;
instance.posy = window.innerHeight - instance.form.offsetHeight - 40;
} else if (instance.nonModal || instance.popupParent) {
// in case of toolbox we want to create popup positioned by toolitem not toolbox
if (updatedPos) {
calculated = true;
instance.posx = updatedPos.x;
instance.posy = updatedPos.y;
}
var parent = L.DomUtil.get(instance.popupParent);
if (instance.clickToCloseId && parent) {
var childButton = parent.querySelector('[id=\'' + instance.clickToCloseId + '\']');
if (childButton)
parent = childButton;
}
if (!parent && instance.popupParent === '_POPOVER_') {
// popup was trigerred not by toolbar or menu button, probably on tile area
if (instance.isAutofilter) {
// we are already done
return;
}
else {
console.warn('other popup than autofilter in the document area');
}
}
if (parent) {
calculated = true;
instance.posx = parent.getBoundingClientRect().left;
instance.posy = parent.getBoundingClientRect().bottom;
if (instance.popupAnchor && instance.popupAnchor.indexOf('top') >= 0)
instance.posy = parent.getBoundingClientRect().top;
instance.container.style.minWidth = parent.getBoundingClientRect().width + 'px';
if (isRTL)
instance.posx = window.innerWidth - instance.posx;
if (instance.popupAnchor && instance.popupAnchor.indexOf('end') >= 0)
instance.posx += (isRTL ? 0 : 1) * (parent.clientWidth) - 15;
if (instance.content.clientWidth > window.innerWidth)
instance.container.style.maxWidth = (window.innerWidth - instance.posx - 20) + 'px';
else if (instance.posx + instance.content.clientWidth > window.innerWidth)
instance.posx -= instance.posx + instance.content.clientWidth + 10 - window.innerWidth;
if (instance.content.clientHeight > window.innerHeight)
instance.container.style.maxHeight = (window.innerHeight - instance.posy - 20) + 'px';
else if (instance.posy + instance.content.clientHeight > window.innerHeight)
instance.posy -= instance.posy + instance.content.clientHeight + 10 - window.innerHeight;
}
else {
var height = instance.form.getBoundingClientRect().height;
if (instance.posy + height > instance.containerParent.getBoundingClientRect().height) {
calculated = true;
var newTopPosition = instance.posy - height;
if (newTopPosition < 0)
newTopPosition = 0;
instance.posy = newTopPosition;
}
var width = instance.form.getBoundingClientRect().width;
if (isRTL)
width = width * -1;
if (instance.posx + width > instance.containerParent.getBoundingClientRect().width) {
calculated = true;
var newLeftPosition = instance.posx - width;
if (newLeftPosition < 0)
newLeftPosition = 0;
instance.posx = newLeftPosition;
}
}
}
var positionNotSet = !instance.container.style || !instance.container.style.marginInlineStart;
if (calculated || positionNotSet)
this.updatePosition(instance.container, instance.posx, instance.posy);
},
centerDialogPosition: function (instance) {
var isRTL = document.documentElement.dir === 'rtl';
var height = instance.form.getBoundingClientRect().height;
var width = instance.form.getBoundingClientRect().width;
instance.startX = instance.posx = (window.innerWidth - (isRTL ? (-1 * width) : width)) / 2;
instance.startY = instance.posy = (window.innerHeight - height) / 2;
instance.updatePos({x: instance.posx, y: instance.posy});
},
parentAutofilter : null,
calculateAutoFilterPosition: function(instance) {
// this is autofilter popup
// RTL mode: only difference is when file is RTL not UI
// var isViewRTL = document.documentElement.dir === 'rtl';
var isSpreadsheetRTL = this.map._docLayer.isCalcRTL();
var scale = this.map.zoomToFactor(this.map.getZoom());
var origin = this.map.getPixelOrigin();
var panePos = this.map._getMapPanePos();
var offsetX = isSpreadsheetRTL ? 0 : app.sectionContainer.getSectionWithName(L.CSections.RowHeader.name).size[0];
var offsetY = app.sectionContainer.getSectionWithName(L.CSections.ColumnHeader.name).size[1];
if (this.isChildAutoFilter(instance)) {
this.calculateSubmenuAutoFilterPosition(instance, this.parentAutofilter);
return;
}
this.parentAutofilter = instance.form;
var left = parseInt(instance.posx) * scale;
var top = parseInt(instance.posy) * scale;
var splitPanesContext = this.map.getSplitPanesContext();
var splitPos = new L.Point(0, 0);
if (splitPanesContext)
splitPos = splitPanesContext.getSplitPos();
var newLeft = left + panePos.x - origin.x;
if (left >= splitPos.x && newLeft >= 0)
left = newLeft;
var newTop = top + panePos.y - origin.y;
if (top >= splitPos.y && newTop >= 0)
top = newTop;
if (isSpreadsheetRTL)
left = this.map._size.x - left;
instance.posx = left + offsetX;
instance.posy = top + offsetY;
var width = instance.form.getBoundingClientRect().width;
var canvasEl = this.map._docLayer._canvas.getBoundingClientRect();
var autoFilterBottom = instance.posy + canvasEl.top + instance.form.getBoundingClientRect().height;
var canvasBottom = canvasEl.bottom;
if (instance.posx + width > window.innerWidth)
instance.posx = window.innerWidth - width;
// at this point we have un updated potion of autofilter instance.
// so to handle overlapping case of autofiler and toolbar we need some complex calculation
if (autoFilterBottom > canvasBottom)
instance.posy = instance.posy - (autoFilterBottom - canvasBottom);
this.updatePosition(instance.container, instance.posx, instance.posy);
},
isChildAutoFilter: function(instance) {
// JSON structure suggest that if children array's first element has id='menu' and widgetType = 'treelistbox' then it will definatly a child autofilter popup
var rootChild = instance.children[0];
if (rootChild) {
var firstWidget = rootChild.children[0];
return firstWidget ? (firstWidget.id === 'menu' && firstWidget.type === 'treelistbox') : false;
}
return false;
},
calculateSubmenuAutoFilterPosition: function(instance, parentAutofilter) {
var parentAutofilter = parentAutofilter.getBoundingClientRect();
instance.posx = parentAutofilter.right;
instance.posy = parentAutofilter.top - this.map._docLayer._canvas.getBoundingClientRect().top;
// set marding start for child popup in rtl mode
var isSpreadsheetRTL = this.map._docLayer.isCalcRTL();
if (isSpreadsheetRTL) {
var rtlPosx = parentAutofilter.left - instance.form.getBoundingClientRect().width;
instance.posx = rtlPosx < 0 ? 0 : rtlPosx;
}
// set posx of instance (submenufilter) based on window width
var width = instance.content.clientWidth;
if (instance.posx + width > window.innerWidth)
instance.posx -= instance.posx + width - window.innerWidth;
// submenu filter popup should not go below toolbar element. Adjust height according to window height and bottom toolbar element so it will not overlap with each other
var height = instance.form.getBoundingClientRect().height;
if (instance.posy + height > window.innerHeight)
instance.posy = window.innerHeight - height;
this.updatePosition(instance.container, instance.posx, instance.posy);
},
closeAutoFilterDialogsOnTabChange: function() {
//this.dialogs is an object
var dialogKeys = Object.keys(this.dialogs);
for (var i = 0; i < dialogKeys.length; i++) {
var autoFilterDialogId = dialogKeys[i];
var dialog = this.dialogs[autoFilterDialogId];
// Check if the current dialog has the isAutofilter property set to true
if (dialog.isAutofilter) {
// Call this.close(key, true) for the current dialog
this.close(autoFilterDialogId, true);
}
}
},
onJSDialog: function(e) {
/*
Dialog types:
* Modal (isModalPopUp = true): non-movable + overlay + dimmed background.
* Nonmodal: movable + no dim + no overlay (user can interact with the document).
* Popup (Non-dialog) (isDocumentAreaPopup = true): overlay + no dim.
*/
// We will pass this here and there, so we can split the code into smaller functions.
// Then we will save this into this.dialogs[].
var instance = e.data;
// Save last focused element, we will set the focus back to this element after this popup is closed.
if (!this.dialogs[instance.id] || !this.dialogs[instance.id].lastFocusedElement) // Avoid to reset while updates.
instance.lastFocusedElement = document.activeElement;
instance.callback = e.callback;
instance.isSnackbar = e.data.type === 'snackbar';
instance.isDropdown = e.data.type === 'dropdown';
instance.isModalPopUp = e.data.type === 'modalpopup' || instance.isDropdown;
instance.snackbarTimeout = e.data.timeout || this.options.snackbarTimeout;
instance.isOnlyChild = false;
instance.that = this;
instance.startX = e.data.posx;
instance.startY = e.data.posy;
instance.updatePos = null;
instance.canHaveFocus = !instance.isSnackbar && instance.id !== 'busypopup' && !instance.isAutoCompletePopup;
instance.isDocumentAreaPopup = instance.popupParent === '_POPOVER_' && instance.posx !== undefined && instance.posy !== undefined;
instance.isPopup = instance.isModalPopUp || instance.isDocumentAreaPopup || instance.isSnackbar;
instance.containerParent = instance.isDocumentAreaPopup ? document.getElementById('document-container'): document.body;
instance.isAutofilter = instance.isDocumentAreaPopup && this.map._docLayer.isCalc();
instance.haveTitlebar = (!instance.isModalPopUp && !instance.isSnackbar) || (instance.hasClose && instance.title && instance.title !== '');
instance.nonModal = !instance.isModalPopUp && !instance.isDocumentAreaPopup && !instance.isSnackbar;
// Make a better seperation between popups and modals.
if (instance.isDocumentAreaPopup)
instance.isModalPopUp = false;
// Check.
if (instance.popupParent === '_POPOVER_' && (instance.posx === undefined || instance.posy === undefined))
console.error('There is a POPOVER dialogue without position information.');
if (instance.action === 'fadeout')
{
this.fadeOutDialog(instance);
}
else if (instance.action === 'close')
{
this.close(instance.id, false);
// Manage focus
var dialogs = Object.keys(this.dialogs);
if (dialogs.length) {
var lastKey = dialogs[dialogs.length - 1];
var container = this.dialogs[lastKey].container;
if (container)
container.focus();
var initialFocusElement = JSDialog.GetFocusableElements(container);
if (initialFocusElement && initialFocusElement.length)
initialFocusElement[0].focus();
} else {
this.map.focus();
}
}
else {
// There is no action, so we create a new dialogue.
if (instance.isModalPopUp || instance.isDocumentAreaPopup)
this.getOrCreateOverlay(instance);
if (this.dialogs[instance.id]) {
instance.posx = this.dialogs[instance.id].startX;
instance.posy = this.dialogs[instance.id].startY;
var toRemove = this.dialogs[instance.id].container;
L.DomUtil.remove(toRemove);
}
// We show some dialogs such as Macro Security Warning Dialog and Text Import Dialog (csv)
// They are displayed before the document is loaded
// Spinning should be happening until the 1st interaction with the user
// which is the dialog opening in this case
if (this.map)
this.map._progressBar.end();
this.createContainer(instance, instance.overlay ? instance.overlay: instance.containerParent);
this.createDialog(instance);
this.addHandlers(instance);
// FIXME: remove this auto-binded instance so it will be clear what is passed
instance.updatePos = this.setPosition.bind(this, instance);
// Special case for nonModal dialogues. Core side doesn't send their initial coordinates. We need to center them.
if (instance.nonModal) {
this.centerDialogPosition(instance);
} else {
instance.updatePos();
}
if (instance.isAutofilter)
this.calculateAutoFilterPosition(instance);
this.dialogs[instance.id] = instance;
if (instance.isSnackbar && instance.snackbarTimeout > 0) {
instance.timeoutId = setTimeout(function () { instance.that.closePopover(instance.id, false); }, instance.snackbarTimeout);
}
}
},
onJSUpdate: function (e) {
var data = e.data;
if (data.jsontype !== 'dialog' && data.jsontype !== 'popup')
return;
var dialog = this.dialogs[data.id] ? this.dialogs[data.id].container : null;
if (!dialog)
return;
var builder = new L.control.jsDialogBuilder({windowId: data.id,
mobileWizard: this,
map: this.map,
cssClass: 'jsdialog',
callback: e.callback
});
builder.updateWidget(dialog, data.control);
var dialogInfo = this.dialogs[data.id];
if (dialogInfo.isDocumentAreaPopup) {
// FIXME: suspicious false to remove, leftover from: 629b25b, updatePos rework: a2d666d
// FIXME: data here doesn't seem to have posx, posy in any case (only with full updates)
dialogInfo.updatePos(false, new L.Point(data.posx, data.posy));
}
// FIXME: remove 100 ms magic timing, drawing areas should request dialog position update
// when they receive payload with bigger content
setTimeout(function () { dialogInfo.updatePos(); }, 100);
},
onJSAction: function (e) {
var data = e.data;
var innerData = data.data;
if (data.jsontype !== 'dialog' && data.jsontype !== 'popup')
return;
var dialog = this.dialogs[data.id];
if (!dialog)
return;
var builder = dialog.builder;
if (!builder)
return;
var dialogContainer = dialog.container;
if (!dialogContainer)
return;
// focus on element outside view will move viewarea leaving blank space on the bottom
if (innerData.action_type === 'grab_focus') {
var control = dialogContainer.querySelector('[id=\'' + innerData.control_id + '\']');
var controlPosition = control.getBoundingClientRect();
if (controlPosition.bottom > window.innerHeight ||
controlPosition.right > window.innerWidth) {
this.centerDialogPosition(dialog); // will center it
}
}
builder.executeAction(dialogContainer, innerData);
},
_clamp: function(value, min, max) {
return Math.min(Math.max(value, min), max);
},
onPan: function (ev) {
const target = this.draggingObject;
if (target) {
const isRTL = document.documentElement.dir === 'rtl';
const dialogBounds = target.form.getBoundingClientRect();
const width = dialogBounds.width;
const height = dialogBounds.height;
const startX = target.startX ? target.startX : 0;
const startY = target.startY ? target.startY : 0;
let newX = startX + ev.deltaX * (isRTL ? -1 : 1);
let newY = startY + ev.deltaY;
// We want to allow dragging the dialog offscreen iff it also drags another part of the dialog onscreen
if (newX < 0 && newX + width < window.innerWidth) {
newX = Math.min(0, window.innerWidth - width);
}
if (newX + width > window.innerWidth && newX > 0) {
newX = Math.max(window.innerWidth - width, 0);
}
// Happily for us, jsdialogs scroll in the y direction if the screen is too small anyway which lets us not worry about the title bar going fully offscreen
newY = this._clamp(newY, 0, window.innerHeight - height);
target.translateX = newX;
target.translateY = newY;
this.updatePosition(target.container, newX, newY);
}
},
updatePosition: function (target, newX, newY) {
target.style.marginInlineStart = newX + 'px';
target.style.marginTop = newY + 'px';
},
handleKeyEvent: function (event) {
var keyCode = event.keyCode;
switch (keyCode) {
case 27:
// ESC
var dialogKeys = Object.keys(this.dialogs);
if (dialogKeys.length) {
var lastKey = dialogKeys[dialogKeys.length - 1];
var sendCloseToServer = this.dialogs[lastKey].isDropdown !== true;
this.close(lastKey, sendCloseToServer);
return true;
}
}
return false;
},
onZoomEnd: function () {
var dialogs = Object.keys(this.dialogs);
if (dialogs.length) {
var lastKey = dialogs[dialogs.length - 1];
var dialogInfo = this.dialogs[lastKey];
if (dialogInfo.isPopup) {
this.close(lastKey, true);
this.map.focus();
}
}
}
});
L.control.jsDialog = function (options) {
return new L.Control.JSDialog(options);
};