1122 lines
36 KiB
JavaScript
1122 lines
36 KiB
JavaScript
/* -*- js-indent-level: 8 -*- */
|
|
/*
|
|
* 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.LokDialog used for displaying LOK dialogs
|
|
*/
|
|
|
|
/* global app $ L Hammer brandProductName UNOModifier */
|
|
|
|
L.WinUtil = {
|
|
|
|
};
|
|
|
|
var firstTouchPositionX = null;
|
|
var firstTouchPositionY = null;
|
|
var previousTouchType = null;
|
|
|
|
function updateTransformation(target) {
|
|
if (target !== null && target !== undefined) {
|
|
var value = [
|
|
'translate3d(' + target.transformation.translate.x + 'px, ' + target.transformation.translate.y + 'px, 0)',
|
|
'scale(' + target.transformation.scale + ', ' + target.transformation.scale + ')'
|
|
];
|
|
|
|
value = value.join(' ');
|
|
target.value.style.webkitTransform = value;
|
|
target.value.style.mozTransform = value;
|
|
target.value.style.transform = value;
|
|
|
|
if (target.transformation.origin) {
|
|
target.value.style[L.DomUtil.TRANSFORM_ORIGIN] = target.transformation.origin;
|
|
}
|
|
}
|
|
}
|
|
|
|
var draggedObject = null;
|
|
|
|
var zoomTargets = [];
|
|
|
|
function findZoomTarget(id) {
|
|
for (var item in zoomTargets) {
|
|
if (zoomTargets[item].key === id || zoomTargets[item].titlebar.id === id) {
|
|
return zoomTargets[item];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function removeZoomTarget(id) {
|
|
for (var item in zoomTargets) {
|
|
if (zoomTargets[item].key === id || zoomTargets[item].titlebar.id === id) {
|
|
delete zoomTargets[item];
|
|
}
|
|
}
|
|
}
|
|
|
|
function toZoomTargetId(id) {
|
|
return id.replace('-canvas', '');
|
|
}
|
|
|
|
L.Control.LokDialog = L.Control.extend({
|
|
|
|
dialogIdPrefix: 'lokdialog-',
|
|
|
|
hasDialogInMobilePanelOpened: function() {
|
|
return window.mobileDialogId !== undefined;
|
|
},
|
|
|
|
onPan: function (ev) {
|
|
if (!draggedObject)
|
|
return;
|
|
|
|
var id = toZoomTargetId(draggedObject.id);
|
|
var target = findZoomTarget(id);
|
|
|
|
if (target) {
|
|
var newX = target.initialState.startX + ev.deltaX;
|
|
var newY = target.initialState.startY + ev.deltaY;
|
|
|
|
// Don't allow to put dialog outside the view
|
|
if (window.mode.isDesktop() &&
|
|
(newX < -target.width/2 || newY < -target.height/2
|
|
|| newX > window.innerWidth - target.width/2
|
|
|| newY > window.innerHeight - target.height/2)) {
|
|
var dialog = $('.lokdialog_container');
|
|
var left = parseFloat(dialog.css('left'));
|
|
var top = parseFloat(dialog.css('top'));
|
|
newX = Math.max(newX, -left);
|
|
newY = Math.max(newY, -top);
|
|
}
|
|
|
|
target.transformation.translate = {
|
|
x: newX,
|
|
y: newY
|
|
};
|
|
|
|
target.transformation.translate = {
|
|
x: newX,
|
|
y: newY
|
|
};
|
|
|
|
updateTransformation(target);
|
|
}
|
|
},
|
|
|
|
onPinch: function (ev) {
|
|
var id = toZoomTargetId(ev.target.id);
|
|
var target = findZoomTarget(id);
|
|
|
|
if (target) {
|
|
if (ev.type == 'pinchstart') {
|
|
target.initialState.initScale = target.transformation.scale || 1;
|
|
}
|
|
|
|
if (target.initialState.initScale * ev.scale > 0.4) {
|
|
target.transformation.scale = target.initialState.initScale * ev.scale;
|
|
}
|
|
|
|
updateTransformation(target);
|
|
}
|
|
},
|
|
|
|
onAdd: function (map) {
|
|
map.on('window', this._onDialogMsg, this);
|
|
map.on('windowpaint', this._onDialogPaint, this);
|
|
map.on('docloaded', this._docLoaded, this);
|
|
map.on('closepopup', this.onCloseCurrentPopUp, this);
|
|
map.on('closepopups', this._onClosePopups, this);
|
|
map.on('editorgotfocus', this._onEditorGotFocus, this);
|
|
// Fired to signal that the input focus is being changed.
|
|
map.on('changefocuswidget', this._changeFocusWidget, this);
|
|
L.DomEvent.on(document, 'mouseup', this.onCloseCurrentPopUp, this);
|
|
},
|
|
|
|
_dialogs: {},
|
|
|
|
hasOpenedDialog: function() {
|
|
return Object.keys(this._dialogs).length > 0;
|
|
},
|
|
|
|
getCurrentDialogContainer: function() {
|
|
if (this._currentId)
|
|
return document.getElementById(this._dialogs[this._currentId].strId);
|
|
else
|
|
return null;
|
|
},
|
|
|
|
// method used to warn user about dialog modality
|
|
blinkOpenDialog: function() {
|
|
$('.lokdialog_container').addClass('lokblink');
|
|
setTimeout(function () {
|
|
$('.lokdialog_container').removeClass('lokblink');
|
|
}, 600);
|
|
},
|
|
|
|
_docLoaded: function(e) {
|
|
if (!e.status) {
|
|
$('.lokdialog_container:not(.jsdialog-container)').remove();
|
|
$('.lokdialogchild-canvas').remove();
|
|
}
|
|
},
|
|
|
|
_getParentId: function(id) {
|
|
id = parseInt(id);
|
|
for (var winId in this._dialogs) {
|
|
if (this._dialogs[winId].childid && this._dialogs[winId].childid === id) {
|
|
return winId;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
_isOpen: function(id) {
|
|
return (id in this._dialogs) && this._dialogs[id] &&
|
|
$('#' + this._toStrId(id)).length > 0;
|
|
},
|
|
|
|
isCursorVisible: function(id) {
|
|
return (id in this._dialogs) && this._dialogs[id].cursorVisible;
|
|
},
|
|
|
|
_isSelectionHandle: function(el) {
|
|
return L.DomUtil.hasClass(el, 'text-selection-handle-start') ||
|
|
L.DomUtil.hasClass(el, 'text-selection-handle-end');
|
|
},
|
|
|
|
// Given a prefixed dialog id like 'lokdialog-323', gives a raw id, 323.
|
|
_toIntId: function(id) {
|
|
if (typeof(id) === 'string')
|
|
return parseInt(id.replace(this.dialogIdPrefix, ''));
|
|
return id;
|
|
},
|
|
|
|
// Converts a raw dialog id like 432, to 'lokdialog-432'.
|
|
_toStrId: function(id) {
|
|
return this.dialogIdPrefix + id;
|
|
},
|
|
|
|
// Create a rectangle string of form "x,y,width,height"
|
|
// if params are missing, assumes 0,0,dialog width, dialog height
|
|
_createRectStr: function(id, x, y, width, height) {
|
|
if (!width && id !== null)
|
|
width = this._dialogs[parseInt(id)].width;
|
|
if (!width || width <= 0)
|
|
return null;
|
|
if (!height && id !== null)
|
|
height = this._dialogs[parseInt(id)].height;
|
|
if (!height || height <= 0)
|
|
return null;
|
|
if (!x)
|
|
x = 0;
|
|
if (!y)
|
|
y = 0;
|
|
|
|
// pre-multiplied by the scale factor
|
|
return [x * app.roundedDpiScale, y * app.roundedDpiScale, width * app.roundedDpiScale, height * app.roundedDpiScale].join(',');
|
|
},
|
|
|
|
_sendPaintWindowRect: function(id, x, y, width, height) {
|
|
this._sendPaintWindow(id, this._createRectStr(id, x, y, width, height));
|
|
},
|
|
|
|
// TODO: Extract to tool in Debug.js
|
|
_debugPaintWindow: function(id, rectangle) {
|
|
var strId = this._toStrId(id);
|
|
var canvas = document.getElementById(strId + '-canvas');
|
|
if (!canvas)
|
|
return; // no window to paint to
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.beginPath();
|
|
var rect = rectangle.split(',');
|
|
ctx.rect(rect[0], rect[1], rect[2], rect[3]);
|
|
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
|
|
ctx.fill();
|
|
},
|
|
|
|
_sendPaintWindow: function(id, rectangle) {
|
|
if (!rectangle)
|
|
return; // Don't request rendering an empty area.
|
|
|
|
rectangle = rectangle.replace(/ /g, '');
|
|
if (!rectangle)
|
|
return; // Don't request rendering an empty area.
|
|
|
|
//window.app.console.log('_sendPaintWindow: rectangle: ' + rectangle + ', dpiscale: ' + dpiscale);
|
|
app.socket.sendMessage('paintwindow ' + id + ' rectangle=' + rectangle + ' dpiscale=' + app.roundedDpiScale);
|
|
|
|
if (this._map._debug.debugOn)
|
|
this._debugPaintWindow(id, rectangle);
|
|
},
|
|
|
|
_sendCloseWindow: function(id) {
|
|
app.socket.sendMessage('windowcommand ' + id + ' close');
|
|
// CSV and Macro Security Warning Dialogs are shown before the document load
|
|
// In that state the document is not really loaded and closing or cancelling it
|
|
// returns docnotloaded error. Instead of this we can return to the integration
|
|
if (!this._map._docLoaded && !window._firstDialogHandled) {
|
|
window.onClose();
|
|
}
|
|
},
|
|
|
|
_isRectangleValid: function(rect) {
|
|
rect = rect.split(',');
|
|
return (!isNaN(parseInt(rect[0])) && !isNaN(parseInt(rect[1])) &&
|
|
parseInt(rect[2]) >= 0 && parseInt(rect[3]) >= 0);
|
|
},
|
|
|
|
_onDialogMsg: function(e) {
|
|
// window.app.console.log('onDialogMsg: id: ' + e.id + ', winType: ' + e.winType + ', action: ' + e.action + ', size: ' + e.size + ', rectangle: ' + e.rectangle);
|
|
if (e.winType != undefined &&
|
|
e.winType !== 'dialog' &&
|
|
e.winType !== 'calc-input-win' &&
|
|
e.winType !== 'child' &&
|
|
e.winType !== 'deck' &&
|
|
e.winType !== 'tooltip' &&
|
|
e.winType !== 'dropdown') {
|
|
return;
|
|
}
|
|
|
|
e.id = parseInt(e.id);
|
|
var strId = this._toStrId(e.id);
|
|
|
|
var width = 0;
|
|
var height = 0;
|
|
if (e.size) {
|
|
width = parseInt(e.size.split(',')[0]);
|
|
height = parseInt(e.size.split(',')[1]);
|
|
}
|
|
|
|
var left;
|
|
var top;
|
|
if (e.position) {
|
|
left = parseInt(e.position.split(',')[0]);
|
|
top = parseInt(e.position.split(',')[1]);
|
|
}
|
|
|
|
if (e.title && typeof brandProductName !== 'undefined') {
|
|
e.title = e.title.replace('Collabora Office', brandProductName);
|
|
}
|
|
|
|
if (e.action === 'created') {
|
|
if ((e.winType === 'dialog' || e.winType === 'dropdown') && !window.mode.isMobile()) {
|
|
// When left/top are invalid, the dialog shows in the center.
|
|
this._launchDialog(e.id, left, top, width, height, e.title, null, e.unique_id);
|
|
} else if (e.winType === 'child' || e.winType === 'tooltip') {
|
|
var parentId = parseInt(e.parentId);
|
|
if (!this._isOpen(parentId))
|
|
return;
|
|
|
|
// In case of tooltips, do not remove the previous popup
|
|
// only if that's also a tooltip.
|
|
if (e.winType === 'tooltip' &&
|
|
this._dialogs[parentId].childid !== undefined &&
|
|
this._dialogs[parentId].childistooltip !== true)
|
|
return;
|
|
|
|
if (!left)
|
|
left = 0;
|
|
if (!top)
|
|
top = 0;
|
|
this._removeDialogChild(parentId);
|
|
|
|
this._dialogs[parentId].childid = e.id;
|
|
this._dialogs[parentId].childwidth = width;
|
|
this._dialogs[parentId].childheight = height;
|
|
this._dialogs[parentId].childx = left;
|
|
this._dialogs[parentId].childy = top;
|
|
|
|
if (e.winType === 'tooltip')
|
|
this._dialogs[parentId].childistooltip = true;
|
|
else
|
|
this._dialogs[parentId].childistooltip = false;
|
|
this._createDialogChild(e.id, parentId, top, left);
|
|
this._sendPaintWindow(e.id, this._createRectStr(null, 0, 0, width, height));
|
|
}
|
|
}
|
|
|
|
// All other callbacks don't make sense without an active dialog.
|
|
if (!(this._isOpen(e.id) || this._getParentId(e.id))) {
|
|
if (e.action == 'close' && window.mobileDialogId == e.id) {
|
|
window.mobileDialogId = undefined;
|
|
//this._map.fire('closemobilewizard');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// We don't want dialogs on smartphones, only calc input window is allowed
|
|
if (window.mode.isMobile())
|
|
return;
|
|
|
|
if (e.action === 'invalidate') {
|
|
this.wasInvalidated = true;
|
|
var parent = this._getParentId(e.id);
|
|
var rectangle = e.rectangle;
|
|
if (parent) { // this is a floating window
|
|
if (e.rectangle && this._dialogs[parent].childistooltip === true) {
|
|
// resize tooltips on invalidation
|
|
this._removeDialogChild(parent);
|
|
left = this._dialogs[parent].childx;
|
|
top = this._dialogs[parent].childy;
|
|
width = parseInt(e.rectangle.split(',')[2]);
|
|
height = parseInt(e.rectangle.split(',')[3]);
|
|
this._dialogs[parent].childwidth = width;
|
|
this._dialogs[parent].childheight = height;
|
|
this._createDialogChild(e.id, parent, top, left);
|
|
}
|
|
|
|
rectangle = this._createRectStr(null, 0, 0, this._dialogs[parent].childwidth, this._dialogs[parent].childheight);
|
|
} else if (rectangle) { // this is the actual dialog
|
|
if (this._isRectangleValid(rectangle)) {
|
|
rectangle = e.rectangle.split(',');
|
|
x = parseInt(rectangle[0]);
|
|
y = parseInt(rectangle[1]);
|
|
width = parseInt(rectangle[2]);
|
|
height = parseInt(rectangle[3]);
|
|
|
|
rectangle = this._createRectStr(null, x, y, width, height);
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
rectangle = this._createRectStr(e.id);
|
|
}
|
|
this._sendPaintWindow(e.id, rectangle);
|
|
} else if (e.action === 'size_changed') {
|
|
// FIXME: we don't really have to destroy and launch the dialog again but do it for
|
|
// now because the size sent to us previously in 'created' cb is not correct
|
|
$('#' + strId).remove();
|
|
this._launchDialog(e.id, null, null, width, height, this._dialogs[parseInt(e.id)].title, null, e.unique_id);
|
|
if (this._map._docLayer && this._map._docLayer._docType === 'spreadsheet') {
|
|
if (app.sectionContainer.doesSectionExist(L.CSections.RowHeader.name)) {
|
|
app.sectionContainer.getSectionWithName(L.CSections.RowHeader.name)._updateCanvas();
|
|
app.sectionContainer.getSectionWithName(L.CSections.ColumnHeader.name)._updateCanvas();
|
|
}
|
|
}
|
|
} else if (e.action === 'cursor_invalidate') {
|
|
if (this._isOpen(e.id) && !!e.rectangle) {
|
|
rectangle = e.rectangle.split(',');
|
|
var x = parseInt(rectangle[0]);
|
|
var y = parseInt(rectangle[1]);
|
|
height = parseInt(rectangle[3]);
|
|
|
|
this._updateDialogCursor(e.id, x, y, height);
|
|
}
|
|
} else if (e.action === 'text_selection') {
|
|
if (this._isOpen(e.id)) {
|
|
var rectangles = [];
|
|
var startHandleVisible, endHandleVisible;
|
|
if (e.rectangles) {
|
|
var dataList = e.rectangles.match(/\d+/g);
|
|
if (dataList != null) {
|
|
for (var i = 0; i < dataList.length; i += 4) {
|
|
var rect = {};
|
|
rect.x = parseInt(dataList[i]);
|
|
rect.y = parseInt(dataList[i + 1]);
|
|
rect.width = parseInt(dataList[i + 2]);
|
|
rect.height = parseInt(dataList[i + 3]);
|
|
rectangles.push(rect);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (e.startHandleVisible) {
|
|
startHandleVisible = e.startHandleVisible === 'true';
|
|
}
|
|
if (e.endHandleVisible) {
|
|
endHandleVisible = e.endHandleVisible === 'true';
|
|
}
|
|
this._updateTextSelection(e.id, rectangles, startHandleVisible, endHandleVisible);
|
|
}
|
|
} else if (e.action === 'title_changed') {
|
|
if (e.title && this._dialogs[parseInt(e.id)]) {
|
|
this._dialogs[parseInt(e.id)].title = e.title;
|
|
$('#' + strId).dialog('option', 'title', e.title);
|
|
}
|
|
} else if (e.action === 'cursor_visible') {
|
|
// cursor_visible implies focus has changed, but can
|
|
// be misleading when it flips back on forth on typing!
|
|
var visible = (e.visible === 'true');
|
|
this._dialogs[e.id].cursorVisible = visible;
|
|
if (visible) {
|
|
$('#' + strId + '-cursor').css({display: 'block'});
|
|
this._map.fire('changefocuswidget', {winId: e.id, dialog: this, acceptInput: true}); // Us.
|
|
}
|
|
else {
|
|
$('#' + strId + '-cursor').css({display: 'none'});
|
|
}
|
|
} else if (e.action === 'close') {
|
|
parent = this._getParentId(e.id);
|
|
if (parent)
|
|
this._onDialogChildClose(parent);
|
|
else
|
|
this._onDialogClose(e.id, false);
|
|
} else if (e.action === 'hide') {
|
|
$('#' + strId).parent().css({display: 'none'});
|
|
} else if (e.action === 'show') {
|
|
$('#' + strId).parent().css({display: 'block'});
|
|
}
|
|
},
|
|
|
|
_updateDialogCursor: function(dlgId, x, y, height) {
|
|
var strId = this._toStrId(dlgId);
|
|
var dialogCursor = L.DomUtil.get(strId + '-cursor');
|
|
var cursorVisible = this.isCursorVisible(dlgId);
|
|
L.DomUtil.setStyle(dialogCursor, 'height', height + 'px');
|
|
L.DomUtil.setStyle(dialogCursor, 'display', cursorVisible ? 'block' : 'none');
|
|
// set the position of the cursor container element
|
|
L.DomUtil.setStyle(this._dialogs[dlgId].cursor, 'left', x + 'px');
|
|
L.DomUtil.setStyle(this._dialogs[dlgId].cursor, 'top', y + 'px');
|
|
|
|
// Make sure the keyboard is visible if there is a cursor.
|
|
// But don't hide the keyboard otherwise.
|
|
// At least the formula-input hides the cursor after each key input.
|
|
if (cursorVisible)
|
|
this._map.focus(true);
|
|
},
|
|
|
|
_createDialogCursor: function(dialogId) {
|
|
var id = this._toIntId(dialogId);
|
|
this._dialogs[id].cursor = L.DomUtil.create('div', 'leaflet-cursor-container', L.DomUtil.get(dialogId));
|
|
var cursor = L.DomUtil.create('div', 'leaflet-cursor lokdialog-cursor', this._dialogs[id].cursor);
|
|
cursor.id = dialogId + '-cursor';
|
|
L.DomUtil.addClass(cursor, 'blinking-cursor');
|
|
},
|
|
|
|
_updateTextSelection: function(dlgId, rectangles, startHandleVisible, endHandleVisible) {
|
|
var strId = this._toIntId(dlgId);
|
|
var selections = this._dialogs[strId].textSelection.rectangles;
|
|
L.DomUtil.empty(selections);
|
|
var handles = this._dialogs[strId].textSelection.handles;
|
|
var startHandle, endHandle;
|
|
if (startHandleVisible) {
|
|
startHandle = this._dialogs[strId].textSelection.startHandle;
|
|
} else if (handles.start) {
|
|
L.DomUtil.remove(handles.start);
|
|
handles.start = null;
|
|
}
|
|
if (endHandleVisible) {
|
|
endHandle = this._dialogs[strId].textSelection.endHandle;
|
|
} else if (handles.end) {
|
|
L.DomUtil.remove(handles.end);
|
|
handles.end = null;
|
|
}
|
|
|
|
if (!handles.start && !handles.end)
|
|
handles.draggingStopped = true;
|
|
|
|
if (!rectangles || rectangles.length < 1) {
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < rectangles.length; ++i) {
|
|
var container = L.DomUtil.create('div', 'leaflet-text-selection-container', selections);
|
|
var selection = L.DomUtil.create('div', 'leaflet-text-selection', container);
|
|
var rect = rectangles[i];
|
|
L.DomUtil.setStyle(selection, 'width', rect.width + 'px');
|
|
L.DomUtil.setStyle(selection, 'height', rect.height + 'px');
|
|
L.DomUtil.setStyle(container, 'left', rect.x + 'px');
|
|
L.DomUtil.setStyle(container, 'top', rect.y + 'px');
|
|
}
|
|
|
|
var startPos;
|
|
if (startHandle) {
|
|
var startRect = rectangles[0];
|
|
if (startRect.width < 1)
|
|
return;
|
|
startRect = {x: startRect.x, y: startRect.y, width: 1, height: startRect.height};
|
|
startPos = L.point(startRect.x, startRect.y + startRect.height);
|
|
startPos = startPos.subtract(L.point(0, 2));
|
|
startHandle.lastPos = startPos;
|
|
startHandle.rowHeight = startRect.height;
|
|
}
|
|
|
|
var endPos;
|
|
if (endHandle) {
|
|
var endRect = rectangles[rectangles.length - 1];
|
|
if (endRect.width < 1)
|
|
return;
|
|
endRect = {x: endRect.x + endRect.width - 1, y: endRect.y, width: 1, height: endRect.height};
|
|
endPos = L.point(endRect.x, endRect.y + endRect.height);
|
|
endPos = endPos.subtract(L.point(0, 2));
|
|
endHandle.lastPos = endPos;
|
|
endHandle.rowHeight = endRect.height;
|
|
}
|
|
|
|
if (startHandle && handles.draggingStopped) {
|
|
if (!handles.start)
|
|
handles.start = handles.appendChild(startHandle);
|
|
// window.app.console.log('lokdialog: _updateTextSelection: startPos: x: ' + startPos.x + ', y: ' + startPos.y);
|
|
startHandle.pos = startPos;
|
|
L.DomUtil.setStyle(startHandle, 'left', startPos.x + 'px');
|
|
L.DomUtil.setStyle(startHandle, 'top', startPos.y + 'px');
|
|
}
|
|
|
|
if (endHandle && handles.draggingStopped) {
|
|
if (!handles.end)
|
|
handles.end = handles.appendChild(endHandle);
|
|
// window.app.console.log('lokdialog: _updateTextSelection: endPos: x: ' + endPos.x + ', y: ' + endPos.y);
|
|
endHandle.pos = endPos;
|
|
L.DomUtil.setStyle(endHandle, 'left', endPos.x + 'px');
|
|
L.DomUtil.setStyle(endHandle, 'top', endPos.y + 'px');
|
|
}
|
|
},
|
|
|
|
focus: function(dlgId, acceptInput) {
|
|
this._map.setWinId(dlgId);
|
|
if (dlgId in this._dialogs) {
|
|
this._map.focus(acceptInput);
|
|
}
|
|
},
|
|
|
|
_setCanvasWidthHeight: function(canvas, width, height) {
|
|
var newWidth = width * app.roundedDpiScale;
|
|
var changed = false;
|
|
if (canvas.width != newWidth) {
|
|
L.DomUtil.setStyle(canvas, 'width', width + 'px');
|
|
canvas.width = newWidth;
|
|
changed = true;
|
|
}
|
|
|
|
var newHeight = height * app.roundedDpiScale;
|
|
if (canvas.height != newHeight) {
|
|
L.DomUtil.setStyle(canvas, 'height', height + 'px');
|
|
canvas.height = newHeight;
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
},
|
|
|
|
_launchDialog: function(id, leftTwips, topTwips, width, height, title, type, uniqueId) {
|
|
if (window.ThisIsTheiOSApp && app.map.mobileTopBar)
|
|
app.map.mobileTopBar.enableItem('closemobile', false);
|
|
this.onCloseCurrentPopUp();
|
|
var dialogContainer = L.DomUtil.create('div', 'lokdialog', document.body);
|
|
L.DomUtil.setStyle(dialogContainer, 'padding', '0px');
|
|
L.DomUtil.setStyle(dialogContainer, 'margin', '0px');
|
|
L.DomUtil.setStyle(dialogContainer, 'touch-action', 'manipulate');
|
|
|
|
var strId = this._toStrId(id);
|
|
dialogContainer.id = strId;
|
|
if (uniqueId)
|
|
dialogContainer.dataset.uniqueId = uniqueId;
|
|
|
|
var dialogCanvas = L.DomUtil.create('canvas', 'lokdialog_canvas', dialogContainer);
|
|
this._setCanvasWidthHeight(dialogCanvas, width, height);
|
|
dialogCanvas.id = strId + '-canvas';
|
|
|
|
var dialogClass = 'lokdialog_container';
|
|
if (!title)
|
|
dialogClass += ' lokdialog_notitle';
|
|
|
|
var that = this;
|
|
var size = $(window).width();
|
|
$(dialogContainer).dialog({
|
|
minWidth: Math.min(width, size.x),
|
|
width: Math.min(width, size.x),
|
|
maxHeight: $(window).height(),
|
|
height: 'auto',
|
|
title: title ? title : '',
|
|
modal: false,
|
|
closeOnEscape: true,
|
|
draggable: false,
|
|
resizable: false,
|
|
dialogClass: dialogClass,
|
|
close: function() {
|
|
that._onDialogClose(id, true);
|
|
}
|
|
});
|
|
|
|
if (leftTwips != null && topTwips != null) {
|
|
// magic to re-calculate the position in twips to absolute pixel
|
|
// position inside the #document-container
|
|
var pixels = this._map._docLayer._twipsToPixels(new L.Point(leftTwips, topTwips));
|
|
var origin = this._map.getPixelOrigin();
|
|
var panePos = this._map._getMapPanePos();
|
|
|
|
var left = pixels.x + panePos.x - origin.x;
|
|
var top = pixels.y + panePos.y - origin.y;
|
|
|
|
if (left >= 0 && top >= 0) {
|
|
$(dialogContainer).dialog('option', 'position',
|
|
{ my: 'left top',
|
|
at: 'left+' + left + ' top+' + top,
|
|
of: type === 'dropdown' ? '#map' :
|
|
'#document-container' });
|
|
}
|
|
}
|
|
|
|
// don't show the dialog surround until we have the dialog content
|
|
$(dialogContainer).parent().hide();
|
|
|
|
// Override default minHeight, which can be too large for thin dialogs.
|
|
L.DomUtil.setStyle(dialogContainer, 'minHeight', height + 'px');
|
|
|
|
// Title bar may overflow due to range name. So we should have max width
|
|
var titleBar = dialogContainer.previousSibling;
|
|
var leftPadding = window.getComputedStyle(titleBar).getPropertyValue('padding-left').slice(0, -2);
|
|
var rightPadding = window.getComputedStyle(titleBar).getPropertyValue('padding-right').slice(0, -2);
|
|
L.DomUtil.setStyle(titleBar, 'maxWidth', width - (+leftPadding + +rightPadding) + 'px');
|
|
|
|
this._dialogs[id] = {
|
|
id: id,
|
|
strId: strId,
|
|
width: width,
|
|
height: height,
|
|
cursor: null,
|
|
title: title
|
|
};
|
|
|
|
// don't make 'TAB' focus on this button; we want to cycle focus in the lok dialog with each TAB
|
|
$('.lokdialog_container button.ui-dialog-titlebar-close').attr('tabindex', '-1').blur();
|
|
|
|
this._createDialogCursor(strId);
|
|
this._setupWindowEvents(id, dialogCanvas/*, dlgInput*/);
|
|
this._setupGestures(dialogContainer, id, dialogCanvas);
|
|
|
|
this._currentId = id;
|
|
this._sendPaintWindow(id, this._createRectStr(id));
|
|
},
|
|
|
|
_postLaunch: function(id, panelContainer, panelCanvas) {
|
|
if (window.mode.isDesktop()) {
|
|
this._setupWindowEvents(id, panelCanvas/*, dlgInput*/);
|
|
}
|
|
|
|
// Render window.
|
|
this._sendPaintWindowRect(id);
|
|
},
|
|
|
|
_setupWindowEvents: function(id, canvas/*, dlgInput*/) {
|
|
L.DomEvent.on(canvas, 'contextmenu', L.DomEvent.preventDefault);
|
|
L.DomEvent.on(canvas, 'mousemove', function(e) {
|
|
var pos = this._isSelectionHandle(e.target) ? L.DomEvent.getMousePosition(e, canvas) : {x: e.offsetX, y: e.offsetY};
|
|
this._postWindowMouseEvent('move', id, pos.x, pos.y, 1, 0, 0);
|
|
}, this);
|
|
|
|
L.DomEvent.on(canvas, 'mousedown mouseup', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
|
|
if (this._map.uiManager.isUIBlocked())
|
|
return;
|
|
|
|
if (canvas.lastDraggedHandle)
|
|
canvas.lastDraggedHandle = null;
|
|
|
|
var buttons = 0;
|
|
if (this._map['mouse']) {
|
|
buttons |= e.button === this._map['mouse'].JSButtons.left ? this._map['mouse'].LOButtons.left : 0;
|
|
buttons |= e.button === this._map['mouse'].JSButtons.middle ? this._map['mouse'].LOButtons.middle : 0;
|
|
buttons |= e.button === this._map['mouse'].JSButtons.right ? this._map['mouse'].LOButtons.right : 0;
|
|
} else {
|
|
buttons = 1;
|
|
}
|
|
|
|
var modifier = 0;
|
|
var shift = e.shiftKey ? UNOModifier.SHIFT : 0;
|
|
var ctrl = e.ctrlKey ? UNOModifier.CTRL : 0;
|
|
var alt = e.altKey ? UNOModifier.ALT : 0;
|
|
var cmd = e.metaKey ? UNOModifier.CTRLMAC : 0;
|
|
modifier = shift | ctrl | alt | cmd;
|
|
|
|
// 'mousedown' -> 'buttondown'
|
|
var lokEventType = e.type.replace('mouse', 'button');
|
|
var pos = this._isSelectionHandle(e.target) ? L.DomEvent.getMousePosition(e, canvas) : {x: e.offsetX, y: e.offsetY};
|
|
this._postWindowMouseEvent(lokEventType, id, pos.x, pos.y, 1, buttons, modifier);
|
|
this._map.setWinId(id);
|
|
//dlgInput.focus();
|
|
}, this);
|
|
|
|
L.DomEvent.on(canvas, 'click', function(ev) {
|
|
// Clicking on the dialog's canvas shall not trigger any
|
|
// focus change - therefore the event is stopped and preventDefault()ed.
|
|
L.DomEvent.stop(ev);
|
|
});
|
|
},
|
|
|
|
_setupGestures: function(dialogContainer, id, canvas) {
|
|
var targetId = toZoomTargetId(canvas.id);
|
|
var zoomTarget = $('#' + targetId).parent().get(0);
|
|
var titlebar = $('#' + targetId).prev().children().get(0);
|
|
|
|
var ratio = 1.0;
|
|
var width = this._dialogs[id].width;
|
|
var height = this._dialogs[id].height;
|
|
var offsetX = 0;
|
|
var offsetY = 0;
|
|
|
|
if ((window.mode.isMobile() || window.mode.isTablet()) && width > window.screen.width) {
|
|
ratio = window.screen.width / (width + 40);
|
|
offsetX = -(width - window.screen.width) / 2;
|
|
offsetY = -(height - window.screen.height) / 2;
|
|
}
|
|
|
|
var state = {
|
|
startX: offsetX,
|
|
startY: offsetY,
|
|
initScale: ratio
|
|
};
|
|
var transformation = {
|
|
translate: { x: offsetX, y: offsetY },
|
|
scale: ratio,
|
|
angle: 0,
|
|
rx: 0,
|
|
ry: 0,
|
|
rz: 0
|
|
};
|
|
|
|
// on mobile, force the positioning to the top, so that it is not
|
|
// covered by the virtual keyboard
|
|
if (window.mode.isMobile()) {
|
|
$(dialogContainer).dialog('option', 'position', { my: 'center top', at: 'center top', of: '#document-container' });
|
|
transformation.origin = 'center top';
|
|
transformation.translate.y = 0;
|
|
}
|
|
|
|
if (findZoomTarget(targetId) != null) {
|
|
removeZoomTarget(targetId);
|
|
}
|
|
zoomTargets.push({key: targetId, value: zoomTarget, titlebar: titlebar, transformation: transformation, initialState: state, width:width, height: height});
|
|
|
|
var that = this;
|
|
var hammerTitlebar = new Hammer(titlebar);
|
|
hammerTitlebar.add(new Hammer.Pan({ threshold: 20, pointers: 0 }));
|
|
hammerTitlebar.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([hammerTitlebar.get('pan')]);
|
|
|
|
hammerTitlebar.on('panstart', this.onPan);
|
|
hammerTitlebar.on('panmove', this.onPan);
|
|
hammerTitlebar.on('pinchstart pinchmove', this.onPinch);
|
|
hammerTitlebar.on('hammer.input', function(ev) {
|
|
if (ev.isFirst) {
|
|
draggedObject = ev.target;
|
|
}
|
|
|
|
if (ev.isFinal && draggedObject) {
|
|
var id = toZoomTargetId(draggedObject.id);
|
|
var target = findZoomTarget(id);
|
|
if (target) {
|
|
target.initialState.startX = target.transformation.translate.x;
|
|
target.initialState.startY = target.transformation.translate.y;
|
|
}
|
|
draggedObject = null;
|
|
}
|
|
});
|
|
|
|
var hammerContent = new Hammer(canvas);
|
|
hammerContent.add(new Hammer.Pan({ threshold: 20, pointers: 0 }));
|
|
hammerContent.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([hammerContent.get('pan')]);
|
|
|
|
hammerContent.on('panstart', this.onPan);
|
|
hammerContent.on('panmove', this.onPan);
|
|
hammerContent.on('pinchstart pinchmove', this.onPinch);
|
|
hammerContent.on('hammer.input', function(ev) {
|
|
if (ev.isFirst) {
|
|
that.wasInvalidated = false;
|
|
draggedObject = ev.target;
|
|
}
|
|
else if (that.wasInvalidated) {
|
|
draggedObject = null;
|
|
that.wasInvalidated = false;
|
|
return;
|
|
}
|
|
|
|
if (ev.isFinal && draggedObject) {
|
|
var id = toZoomTargetId(draggedObject.id);
|
|
var target = findZoomTarget(id);
|
|
if (target) {
|
|
target.initialState.startX = target.transformation.translate.x;
|
|
target.initialState.startY = target.transformation.translate.y;
|
|
}
|
|
draggedObject = null;
|
|
}
|
|
});
|
|
|
|
updateTransformation(findZoomTarget(targetId));
|
|
},
|
|
|
|
_postWindowMouseEvent: function(type, winid, x, y, count, buttons, modifier) {
|
|
app.socket.sendMessage('windowmouse id=' + winid + ' type=' + type +
|
|
' x=' + x + ' y=' + y + ' count=' + count +
|
|
' buttons=' + buttons + ' modifier=' + modifier);
|
|
},
|
|
|
|
_postWindowGestureEvent: function(winid, type, x, y, offset) {
|
|
// window.app.console.log('x ' + x + ' y ' + y + ' o ' + offset);
|
|
app.socket.sendMessage('windowgesture id=' + winid + ' type=' + type +
|
|
' x=' + x + ' y=' + y + ' offset=' + offset);
|
|
},
|
|
|
|
_closeChildWindows: function(dialogId) {
|
|
// child windows - with greater id number
|
|
var that = this;
|
|
var foundCurrent = false;
|
|
|
|
Object.keys(this._dialogs).forEach(function(id) {
|
|
if (foundCurrent)
|
|
that._onDialogClose(id, true);
|
|
|
|
if (id == dialogId)
|
|
foundCurrent = true;
|
|
});
|
|
},
|
|
|
|
_onDialogClose: function(dialogId, notifyBackend) {
|
|
this._closeChildWindows(dialogId);
|
|
|
|
if (window.ThisIsTheiOSApp && app.map.mobileTopBar)
|
|
app.map.mobileTopBar.enableItem('closemobile', true);
|
|
|
|
if (notifyBackend)
|
|
this._sendCloseWindow(dialogId);
|
|
$('#' + this._toStrId(dialogId)).remove();
|
|
|
|
// focus the main document
|
|
this._map.fire('editorgotfocus');
|
|
this._map.focus();
|
|
|
|
delete this._dialogs[dialogId];
|
|
this._currentId = null;
|
|
|
|
removeZoomTarget(this._toStrId(dialogId));
|
|
},
|
|
|
|
_onClosePopups: function() {
|
|
for (var dialogId in this._dialogs) {
|
|
this._onDialogClose(dialogId, true);
|
|
}
|
|
if (this.hasDialogInMobilePanelOpened()) {
|
|
this._onDialogClose(window.mobileDialogId, true);
|
|
}
|
|
},
|
|
|
|
onCloseCurrentPopUp: function() {
|
|
// for title-less dialog only (context menu, pop-up)
|
|
if (this._currentId && this._isOpen(this._currentId) &&
|
|
!this._dialogs[this._currentId].title)
|
|
this._onDialogClose(this._currentId, true);
|
|
},
|
|
|
|
_onEditorGotFocus: function() {
|
|
// We need to lose focus on any dialogs currently with focus.
|
|
for (var winId in this._dialogs) {
|
|
$('#' + this._dialogs[winId].strId + '-cursor').css({display: 'none'});
|
|
}
|
|
},
|
|
|
|
// Focus is being changed, update states.
|
|
_changeFocusWidget: function (e) {
|
|
if (e.winId === 0) {
|
|
// We lost the focus.
|
|
this._onEditorGotFocus();
|
|
} else {
|
|
this.focus(e.winId, e.acceptInput);
|
|
if (this._map.formulabar)
|
|
this._map.onFormulaBarFocus();
|
|
}
|
|
},
|
|
|
|
_paintDialog: function(parentId, rectangle, img) {
|
|
var strId = this._toStrId(parentId);
|
|
var canvas = document.getElementById(strId + '-canvas');
|
|
if (!canvas)
|
|
return; // no window to paint to
|
|
|
|
this._dialogs[parentId].isPainting = true;
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
var that = this;
|
|
var x = 0;
|
|
var y = 0;
|
|
if (rectangle) {
|
|
rectangle = rectangle.split(',');
|
|
x = parseInt(rectangle[0]);
|
|
y = parseInt(rectangle[1]);
|
|
}
|
|
|
|
var container = L.DomUtil.get(strId);
|
|
|
|
ctx.drawImage(img, x, y);
|
|
|
|
// if dialog is hidden, show it
|
|
if (container)
|
|
$(container).parent().show();
|
|
|
|
if (parentId in that._dialogs) {
|
|
// We might have closed the dialog by the time we render.
|
|
that._dialogs[parentId].isPainting = false;
|
|
that._map.fire('changefocuswidget', {winId: parentId, dialog: that});
|
|
}
|
|
},
|
|
|
|
// Binary dialog msg recvd from core
|
|
_onDialogPaint: function (e) {
|
|
var id = parseInt(e.id);
|
|
var parentId = this._getParentId(id);
|
|
if (parentId) {
|
|
this._paintDialogChild(parentId, e.img);
|
|
} else {
|
|
this._paintDialog(id, e.rectangle, e.img);
|
|
}
|
|
},
|
|
|
|
// Dialog Child Methods
|
|
|
|
_paintDialogChild: function(parentId, img) {
|
|
var strId = this._toStrId(parentId);
|
|
var canvas = L.DomUtil.get(strId + '-floating');
|
|
if (!canvas)
|
|
return; // no floating window to paint to
|
|
|
|
// Make sure the child is not trimmed on the right.
|
|
var width = this._dialogs[parentId].childwidth;
|
|
var left = parseInt(canvas.style.left);
|
|
var leftPos = left + width;
|
|
if (leftPos > window.innerWidth) {
|
|
var newLeft = window.innerWidth - width - 20;
|
|
L.DomUtil.setStyle(canvas, 'left', newLeft + 'px');
|
|
}
|
|
// Also, make sure child is not trimmed on bottom.
|
|
var top = parseInt(canvas.style.top);
|
|
var height = this._dialogs[parentId].childheight;
|
|
var bottomPos = top + height;
|
|
if (bottomPos > window.innerHeight) {
|
|
var newTop = top - height - 20;
|
|
L.DomUtil.setStyle(canvas, 'top', newTop + 'px');
|
|
}
|
|
|
|
// The image is rendered per the HiDPI scale we used
|
|
// while requesting rendering the image. Here we
|
|
// set the canvas to have the actual size, while
|
|
// the image is rendered with the HiDPI scale.
|
|
this._setCanvasWidthHeight(canvas, this._dialogs[parentId].childwidth,
|
|
this._dialogs[parentId].childheight);
|
|
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.drawImage(img, 0, 0);
|
|
$(canvas).show();
|
|
},
|
|
|
|
_onDialogChildClose: function(dialogId) {
|
|
$('#' + this._toStrId(dialogId) + '-floating').remove();
|
|
|
|
// Remove any extra height allocated for the parent container (only for floating dialogs).
|
|
var canvas = document.getElementById(dialogId + '-canvas');
|
|
if (!canvas) {
|
|
canvas = document.getElementById(this._toStrId(dialogId) + '-canvas');
|
|
if (!canvas)
|
|
return;
|
|
}
|
|
var canvasHeight = canvas.height;
|
|
$('#' + dialogId).height(canvasHeight + 'px');
|
|
|
|
this._dialogs[dialogId].childid = undefined;
|
|
this._dialogs[dialogId].childx = undefined;
|
|
this._dialogs[dialogId].childy = undefined;
|
|
},
|
|
|
|
_removeDialogChild: function(id) {
|
|
$('#' + this._toStrId(id) + '-floating').remove();
|
|
this._dialogs[id].childid = undefined;
|
|
this._dialogs[id].childx = undefined;
|
|
this._dialogs[id].childy = undefined;
|
|
},
|
|
|
|
_createDialogChild: function(childId, parentId, top, left) {
|
|
var strId = this._toStrId(parentId);
|
|
var dialogContainer = L.DomUtil.get(strId);
|
|
var floatingCanvas = L.DomUtil.create('canvas', 'lokdialogchild-canvas', dialogContainer);
|
|
$(floatingCanvas).hide(); // Hide to avoid flickering while we set the dimensions.
|
|
|
|
floatingCanvas.id = strId + '-floating';
|
|
L.DomUtil.setStyle(floatingCanvas, 'position', 'fixed');
|
|
L.DomUtil.setStyle(floatingCanvas, 'z-index', '11');
|
|
L.DomUtil.setStyle(floatingCanvas, 'width', '0px');
|
|
L.DomUtil.setStyle(floatingCanvas, 'height', '0px');
|
|
|
|
/*
|
|
Some notes:
|
|
* Modal windows' child positions are relative to page borders.
|
|
* So this code adapts to it.
|
|
*/
|
|
|
|
// Add header height..
|
|
var addition = 40;
|
|
L.DomUtil.setStyle(floatingCanvas, 'margin-inline-start', left + 'px');
|
|
L.DomUtil.setStyle(floatingCanvas, 'top', (top + addition) + 'px');
|
|
|
|
// attach events
|
|
this._setupChildEvents(childId, floatingCanvas);
|
|
},
|
|
|
|
_setupChildEvents: function(childId, canvas) {
|
|
L.DomEvent.on(canvas, 'contextmenu', L.DomEvent.preventDefault);
|
|
|
|
L.DomEvent.on(canvas, 'touchstart touchmove touchend', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
var rect = canvas.getBoundingClientRect();
|
|
var touchX = (e.type === 'touchend') ? e.changedTouches[0].clientX : e.targetTouches[0].clientX;
|
|
var touchY = (e.type === 'touchend') ? e.changedTouches[0].clientY : e.targetTouches[0].clientY;
|
|
touchX = touchX - rect.x;
|
|
touchY = touchY - rect.y;
|
|
if (e.type === 'touchstart')
|
|
{
|
|
firstTouchPositionX = touchX;
|
|
firstTouchPositionY = touchY;
|
|
this._postWindowGestureEvent(childId, 'panBegin', firstTouchPositionX, firstTouchPositionY, 0);
|
|
}
|
|
else if (e.type === 'touchend')
|
|
{
|
|
this._postWindowGestureEvent(childId, 'panEnd', firstTouchPositionX, firstTouchPositionY, firstTouchPositionY - touchY);
|
|
if (previousTouchType === 'touchstart') {
|
|
// Simulate mouse click
|
|
if (this._map['mouse']) {
|
|
this._postWindowMouseEvent('buttondown', childId, firstTouchPositionX, firstTouchPositionY, 1, this._map['mouse'].LOButtons.left, 0);
|
|
this._postWindowMouseEvent('buttonup', childId, firstTouchPositionX, firstTouchPositionY, 1, this._map['mouse'].LOButtons.left, 0);
|
|
} else {
|
|
this._postWindowMouseEvent('buttondown', childId, firstTouchPositionX, firstTouchPositionY, 1, 1, 0);
|
|
this._postWindowMouseEvent('buttonup', childId, firstTouchPositionX, firstTouchPositionY, 1, 1, 0);
|
|
}
|
|
}
|
|
firstTouchPositionX = null;
|
|
firstTouchPositionY = null;
|
|
}
|
|
else if (e.type === 'touchmove')
|
|
{
|
|
this._postWindowGestureEvent(childId, 'panUpdate', firstTouchPositionX, firstTouchPositionY, firstTouchPositionY - touchY);
|
|
}
|
|
previousTouchType = e.type;
|
|
}, this);
|
|
|
|
L.DomEvent.on(canvas, 'mousedown mouseup', function(e) {
|
|
var buttons = 0;
|
|
if (this._map['mouse']) {
|
|
buttons |= e.button === this._map['mouse'].JSButtons.left ? this._map['mouse'].LOButtons.left : 0;
|
|
buttons |= e.button === this._map['mouse'].JSButtons.middle ? this._map['mouse'].LOButtons.middle : 0;
|
|
buttons |= e.button === this._map['mouse'].JSButtons.right ? this._map['mouse'].LOButtons.right : 0;
|
|
} else {
|
|
buttons = 1;
|
|
}
|
|
var lokEventType = e.type.replace('mouse', 'button');
|
|
this._postWindowMouseEvent(lokEventType, childId, e.offsetX, e.offsetY, 1, buttons, 0);
|
|
}, this);
|
|
L.DomEvent.on(canvas, 'mousemove', function(e) {
|
|
this._postWindowMouseEvent('move', childId, e.offsetX, e.offsetY, 1, 0, 0);
|
|
}, this);
|
|
L.DomEvent.on(canvas, 'contextmenu', function() {
|
|
return false;
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
L.control.lokDialog = function (options) {
|
|
return new L.Control.LokDialog(options);
|
|
};
|