node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
1,098 lines (996 loc) • 58.5 kB
JavaScript
/**
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* This file is part of WebODF.
*
* WebODF is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* WebODF is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: http://www.webodf.org/
* @source: https://github.com/kogmbh/WebODF/
*/
/*global runtime, core, gui, Node, ops, odf */
/**
* @constructor
* @struct
*/
gui.SessionControllerOptions = function () {
"use strict";
/**
* Sets whether direct paragraph styling should be enabled.
* @type {!boolean}
*/
this.directTextStylingEnabled = false;
/**
* Sets whether direct paragraph styling should be enabled.
* @type {!boolean}
*/
this.directParagraphStylingEnabled = false;
/**
* Sets whether annotation creation/deletion should be enabled.
* @type {!boolean}
*/
this.annotationsEnabled = false;
};
(function () {
"use strict";
var /**@const*/FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT;
/**
* @constructor
* @implements {core.Destroyable}
* @param {!ops.Session} session
* @param {!string} inputMemberId
* @param {!ops.OdtCursor} shadowCursor
* @param {!gui.SessionControllerOptions} args
*/
gui.SessionController = function SessionController(session, inputMemberId, shadowCursor, args) {
var /**@type{!Window}*/window = /**@type{!Window}*/(runtime.getWindow()),
odtDocument = session.getOdtDocument(),
sessionConstraints = new gui.SessionConstraints(),
sessionContext = new gui.SessionContext(session, inputMemberId),
domUtils = core.DomUtils,
odfUtils = odf.OdfUtils,
mimeDataExporter = new gui.MimeDataExporter(),
clipboard = new gui.Clipboard(mimeDataExporter),
keyDownHandler = new gui.KeyboardHandler(),
keyPressHandler = new gui.KeyboardHandler(),
keyUpHandler = new gui.KeyboardHandler(),
/**@type{boolean}*/
clickStartedWithinCanvas = false,
objectNameGenerator = new odf.ObjectNameGenerator(odtDocument.getOdfCanvas().odfContainer(), inputMemberId),
isMouseMoved = false,
/**@type{core.PositionFilter}*/
mouseDownRootFilter = null,
handleMouseClickTimeoutId,
undoManager = null,
eventManager = new gui.EventManager(odtDocument),
annotationsEnabled = args.annotationsEnabled,
annotationController = new gui.AnnotationController(session, sessionConstraints, inputMemberId),
directFormattingController = new gui.DirectFormattingController(session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator,
args.directTextStylingEnabled, args.directParagraphStylingEnabled),
createCursorStyleOp = /**@type {function (!number, !number, !boolean):ops.Operation}*/ (directFormattingController.createCursorStyleOp),
createParagraphStyleOps = /**@type {function (!number):!Array.<!ops.Operation>}*/ (directFormattingController.createParagraphStyleOps),
textController = new gui.TextController(session, sessionConstraints, sessionContext, inputMemberId, createCursorStyleOp, createParagraphStyleOps),
imageController = new gui.ImageController(session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator),
imageSelector = new gui.ImageSelector(odtDocument.getOdfCanvas()),
shadowCursorIterator = odtDocument.createPositionIterator(odtDocument.getRootNode()),
/**@type{!core.ScheduledTask}*/
drawShadowCursorTask,
/**@type{!core.ScheduledTask}*/
redrawRegionSelectionTask,
pasteController = new gui.PasteController(session, sessionConstraints, sessionContext, inputMemberId),
inputMethodEditor = new gui.InputMethodEditor(inputMemberId, eventManager),
/**@type{number}*/
clickCount = 0,
hyperlinkClickHandler = new gui.HyperlinkClickHandler(odtDocument.getOdfCanvas().getElement,
keyDownHandler, keyUpHandler),
hyperlinkController = new gui.HyperlinkController(session, sessionConstraints, sessionContext, inputMemberId),
selectionController = new gui.SelectionController(session, inputMemberId),
metadataController = new gui.MetadataController(session, inputMemberId),
modifier = gui.KeyboardHandler.Modifier,
keyCode = gui.KeyboardHandler.KeyCode,
isMacOS = window.navigator.appVersion.toLowerCase().indexOf("mac") !== -1,
isIOS = ["iPad", "iPod", "iPhone"].indexOf(window.navigator.platform) !== -1,
/**@type{?gui.IOSSafariSupport}*/
iOSSafariSupport;
runtime.assert(window !== null,
"Expected to be run in an environment which has a global window, like a browser.");
/**
* @param {!Event} e
* @return {Node}
*/
function getTarget(e) {
// e.srcElement because IE10 likes to be different...
return /**@type{Node}*/(e.target) || e.srcElement || null;
}
/**
* @param {!Event} event
* @return {undefined}
*/
function cancelEvent(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
/**
* @param {!number} x
* @param {!number} y
* @return {?{container:!Node, offset:!number}}
*/
function caretPositionFromPoint(x, y) {
var doc = odtDocument.getDOMDocument(),
c,
result = null;
if (doc.caretRangeFromPoint) {
c = doc.caretRangeFromPoint(x, y);
result = {
container: /**@type{!Node}*/(c.startContainer),
offset: c.startOffset
};
} else if (doc.caretPositionFromPoint) {
c = doc.caretPositionFromPoint(x, y);
if (c && c.offsetNode) {
result = {
container: c.offsetNode,
offset: c.offset
};
}
}
return result;
}
/**
* If the user's current selection is region selection (e.g., an image), any executed operations
* could cause the picture to shift relative to the selection rectangle.
* @return {undefined}
*/
function redrawRegionSelection() {
var cursor = odtDocument.getCursor(inputMemberId),
imageElement;
if (cursor && cursor.getSelectionType() === ops.OdtCursor.RegionSelection) {
imageElement = odfUtils.getImageElements(cursor.getSelectedRange())[0];
if (imageElement) {
imageSelector.select(/**@type{!Element}*/(imageElement.parentNode));
return;
}
}
// May have just processed our own remove cursor operation...
// In this case, clear any image selection chrome to prevent user confusion
imageSelector.clearSelection();
}
/**
* @param {!Event} event
* @return {?string}
*/
function stringFromKeyPress(event) {
if (event.which === null || event.which === undefined) {
return String.fromCharCode(event.keyCode); // IE
}
if (event.which !== 0 && event.charCode !== 0) {
return String.fromCharCode(event.which); // the rest
}
return null; // special key
}
/**
* Handle the cut operation request
* @param {!Event} e
* @return {undefined}
*/
function handleCut(e) {
var cursor = odtDocument.getCursor(inputMemberId),
selectedRange = cursor.getSelectedRange();
if (selectedRange.collapsed) {
// Modifying the clipboard data will clear any existing data,
// so cut shouldn't touch the clipboard if there is nothing selected
e.preventDefault();
return;
}
// The document is readonly, so the data will never get placed on
// the clipboard in most browsers unless we do it ourselves.
if (clipboard.setDataFromRange(e, selectedRange)) {
textController.removeCurrentSelection();
} else {
// TODO What should we do if cut isn't supported?
runtime.log("Cut operation failed");
}
}
/**
* Tell the browser that it's ok to perform a cut action on our read-only body
* @return {!boolean}
*/
function handleBeforeCut() {
var cursor = odtDocument.getCursor(inputMemberId),
selectedRange = cursor.getSelectedRange();
return selectedRange.collapsed !== false; // return false to enable cut menu... straightforward right?!
}
/**
* Handle the copy operation request
* @param {!Event} e
* @return {undefined}
*/
function handleCopy(e) {
var cursor = odtDocument.getCursor(inputMemberId),
selectedRange = cursor.getSelectedRange();
if (selectedRange.collapsed) {
// Modifying the clipboard data will clear any existing data,
// so copy shouldn't touch the clipboard if there is nothing
// selected
e.preventDefault();
return;
}
// Place the data on the clipboard ourselves to ensure consistency
// with cut behaviours
if (!clipboard.setDataFromRange(e, selectedRange)) {
// TODO What should we do if copy isn't supported?
runtime.log("Copy operation failed");
}
}
/**
* @param {!Event} e
* @return {undefined}
*/
function handlePaste(e) {
var plainText;
if (window.clipboardData && window.clipboardData.getData) { // IE
plainText = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) { // the rest
plainText = e.clipboardData.getData('text/plain');
}
if (plainText) {
textController.removeCurrentSelection();
pasteController.paste(plainText);
}
cancelEvent(e);
}
/**
* Tell the browser that it's ok to perform a paste action on our read-only body
* @return {!boolean}
*/
function handleBeforePaste() {
return false;
}
/**
* @param {!ops.Operation} op
* @return {undefined}
*/
function updateUndoStack(op) {
if (undoManager) {
undoManager.onOperationExecuted(op);
}
}
/**
* @param {?Event} e
* @return {undefined}
*/
function forwardUndoStackChange(e) {
odtDocument.emit(ops.OdtDocument.signalUndoStackChanged, e);
}
/**
* @return {!boolean}
*/
function undo() {
var hadFocusBefore;
if (undoManager) {
hadFocusBefore = eventManager.hasFocus();
undoManager.moveBackward(1);
if (hadFocusBefore) {
eventManager.focus();
}
return true;
}
return false;
}
// TODO it will soon be time to grow an UndoController
this.undo = undo;
/**
* @return {!boolean}
*/
function redo() {
var hadFocusBefore;
if (undoManager) {
hadFocusBefore = eventManager.hasFocus();
undoManager.moveForward(1);
if (hadFocusBefore) {
eventManager.focus();
}
return true;
}
return false;
}
// TODO it will soon be time to grow an UndoController
this.redo = redo;
/**
* This processes our custom drag events and if they are on
* a selection handle (with the attribute 'end' denoting the left
* or right handle), updates the shadow cursor's selection to
* be on those endpoints.
* @param {!Event} event
* @return {undefined}
*/
function extendSelectionByDrag(event) {
var position,
cursor = odtDocument.getCursor(inputMemberId),
selectedRange = cursor.getSelectedRange(),
newSelectionRange,
/**@type{!string}*/
handleEnd = /**@type{!Element}*/(getTarget(event)).getAttribute('end');
if (selectedRange && handleEnd) {
position = caretPositionFromPoint(event.clientX, event.clientY);
if (position) {
shadowCursorIterator.setUnfilteredPosition(position.container, position.offset);
if (mouseDownRootFilter.acceptPosition(shadowCursorIterator) === FILTER_ACCEPT) {
newSelectionRange = /**@type{!Range}*/(selectedRange.cloneRange());
if (handleEnd === 'left') {
newSelectionRange.setStart(shadowCursorIterator.container(), shadowCursorIterator.unfilteredDomOffset());
} else {
newSelectionRange.setEnd(shadowCursorIterator.container(), shadowCursorIterator.unfilteredDomOffset());
}
shadowCursor.setSelectedRange(newSelectionRange, handleEnd === 'right');
odtDocument.emit(ops.Document.signalCursorMoved, shadowCursor);
}
}
}
}
function updateCursorSelection() {
selectionController.selectRange(shadowCursor.getSelectedRange(), shadowCursor.hasForwardSelection(), 1);
}
function updateShadowCursor() {
var selection = window.getSelection(),
selectionRange = selection.rangeCount > 0 && selectionController.selectionToRange(selection);
if (clickStartedWithinCanvas && selectionRange) {
isMouseMoved = true;
imageSelector.clearSelection();
shadowCursorIterator.setUnfilteredPosition(/**@type {!Node}*/(selection.focusNode), selection.focusOffset);
if (mouseDownRootFilter.acceptPosition(shadowCursorIterator) === FILTER_ACCEPT) {
if (clickCount === 2) {
selectionController.expandToWordBoundaries(selectionRange.range);
} else if (clickCount >= 3) {
selectionController.expandToParagraphBoundaries(selectionRange.range);
}
shadowCursor.setSelectedRange(selectionRange.range, selectionRange.hasForwardSelection);
odtDocument.emit(ops.Document.signalCursorMoved, shadowCursor);
}
}
}
/**
* In order for drag operations to work, the browser needs to have it's current
* selection set. This is called on mouse down to synchronize the user's last selection
* to the browser selection
* @param {ops.OdtCursor} cursor
* @return {undefined}
*/
function synchronizeWindowSelection(cursor) {
var selection = window.getSelection(),
range = cursor.getSelectedRange();
if (selection.extend) {
if (cursor.hasForwardSelection()) {
selection.collapse(range.startContainer, range.startOffset);
selection.extend(range.endContainer, range.endOffset);
} else {
selection.collapse(range.endContainer, range.endOffset);
selection.extend(range.startContainer, range.startOffset);
}
} else {
// Internet explorer does provide any method for
// preserving the range direction
// See http://msdn.microsoft.com/en-us/library/ie/ff974359%28v=vs.85%29.aspx
// Unfortunately, clearing the range will also blur the current focus.
selection.removeAllRanges();
selection.addRange(range.cloneRange());
}
}
/**
* Return the number of mouse clicks if the mouse event is for the primary button. Otherwise return 0.
* @param {!UIEvent} event
* @return {!number}
*/
function computeClickCount(event) {
// According to the spec, button === 0 indicates the primary button (the left button by default, or the
// right button if the user has switched their mouse buttons around).
return event.button === 0 ? event.detail : 0;
}
/**
* Updates a flag indicating whether the mouse down event occurred within the OdfCanvas element.
* This is necessary because the mouse-up binding needs to be global in order to handle mouse-up
* events that occur when the user releases the mouse button outside the canvas.
* This filter limits selection changes to mouse down events that start inside the canvas
* @param {!UIEvent} e
*/
function handleMouseDown(e) {
var target = getTarget(e),
cursor = odtDocument.getCursor(inputMemberId),
rootNode;
clickStartedWithinCanvas = target !== null && domUtils.containsNode(odtDocument.getOdfCanvas().getElement(), target);
if (clickStartedWithinCanvas) {
isMouseMoved = false;
rootNode = odtDocument.getRootElement(/**@type{!Node}*/(target)) || odtDocument.getRootNode();
mouseDownRootFilter = odtDocument.createRootFilter(rootNode);
clickCount = computeClickCount(e);
if (cursor && e.shiftKey) {
// Firefox seems to get rather confused about the window selection when shift+extending it.
// Help this poor browser by resetting the window selection back to the anchor node if the user
// is holding shift.
window.getSelection().collapse(cursor.getAnchorNode(), 0);
} else {
synchronizeWindowSelection(cursor);
}
if (clickCount > 1) {
updateShadowCursor();
}
}
}
/**
* Return a mutable version of a selection-type object.
* @param {?Selection} selection
* @return {?{anchorNode: ?Node, anchorOffset: !number, focusNode: ?Node, focusOffset: !number}}
*/
function mutableSelection(selection) {
if (selection) {
return {
anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode,
focusOffset: selection.focusOffset
};
}
return null;
}
/**
* Gets the next walkable position after the given node.
* @param {!Node} node
* @return {?{container:!Node, offset:!number}}
*/
function getNextWalkablePosition(node) {
var root = odtDocument.getRootElement(node),
rootFilter = odtDocument.createRootFilter(root),
stepIterator = odtDocument.createStepIterator(node, 0, [rootFilter, odtDocument.getPositionFilter()], root);
stepIterator.setPosition(node, node.childNodes.length);
if (!stepIterator.roundToNextStep()) {
return null;
}
return {
container: stepIterator.container(),
offset: stepIterator.offset()
};
}
/**
* Causes a cursor movement to the position hinted by a mouse click
* event.
* @param {!UIEvent} event
* @return {undefined}
*/
function moveByMouseClickEvent(event) {
var selection = mutableSelection(window.getSelection()),
isCollapsed = window.getSelection().isCollapsed,
position,
selectionRange,
rect,
frameNode;
if (!selection.anchorNode && !selection.focusNode) {
// chrome & safari will report null for focus and anchor nodes after a right-click in text selection
position = caretPositionFromPoint(event.clientX, event.clientY);
if (position) {
selection.anchorNode = /**@type{!Node}*/(position.container);
selection.anchorOffset = position.offset;
selection.focusNode = selection.anchorNode;
selection.focusOffset = selection.anchorOffset;
}
}
if (odfUtils.isImage(selection.focusNode) && selection.focusOffset === 0
&& odfUtils.isCharacterFrame(selection.focusNode.parentNode)) {
// In FireFox if an image has no text around it, click on either side of the
// image resulting the same selection get returned. focusNode: image, focusOffset: 0
// Move the cursor to the next walkable position when clicking on the right side of an image
frameNode = /**@type{!Element}*/(selection.focusNode.parentNode);
rect = frameNode.getBoundingClientRect();
if (event.clientX > rect.left) {
// On OSX, right-clicking on an image at the end of a range selection will hit
// this particular branch. The image should remain selected if the right-click occurs on top
// of it as technically it's the same behaviour as right clicking on an existing text selection.
position = getNextWalkablePosition(frameNode);
if (position) {
selection.focusNode = position.container;
selection.focusOffset = position.offset;
if (isCollapsed) {
// See above comment for the circumstances when the range might not be collapsed
selection.anchorNode = selection.focusNode;
selection.anchorOffset = selection.focusOffset;
}
}
}
} else if (odfUtils.isImage(selection.focusNode.firstChild) && selection.focusOffset === 1
&& odfUtils.isCharacterFrame(selection.focusNode)) {
// When click on the right side of an image that has no text elements, non-FireFox browsers
// will return focusNode: frame, focusOffset: 1 as the selection. Since this is not a valid cursor
// position, move the cursor to the next walkable position after the frame node.
// To activate this branch (only applicable on OSX + Linux WebKit-derived browsers AFAIK):
// 1. With a paragraph containing some text followed by an inline image and no trailing text,
// select from the start of paragraph to the end.
// 2. Now click once to the right hand side of the image. The cursor *should* jump to the right side
position = getNextWalkablePosition(selection.focusNode);
if (position) {
// This should only ever be hit when the selection is intended to become collapsed
selection.anchorNode = selection.focusNode = position.container;
selection.anchorOffset = selection.focusOffset = position.offset;
}
}
// Need to check the selection again in case the caret position didn't return any result
if (selection.anchorNode && selection.focusNode) {
selectionRange = selectionController.selectionToRange(selection);
selectionController.selectRange(selectionRange.range,
selectionRange.hasForwardSelection, computeClickCount(event));
}
eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
}
/**
* @param {!Event} event
* @return {undefined}
*/
function selectWordByLongPress(event) {
var /**@type{?{anchorNode: ?Node, anchorOffset: !number, focusNode: ?Node, focusOffset: !number}}*/
selection,
position,
selectionRange,
container, offset;
position = caretPositionFromPoint(event.clientX, event.clientY);
if (position) {
container = /**@type{!Node}*/(position.container);
offset = position.offset;
selection = {
anchorNode: container,
anchorOffset: offset,
focusNode: container,
focusOffset: offset
};
selectionRange = selectionController.selectionToRange(selection);
selectionController.selectRange(selectionRange.range,
selectionRange.hasForwardSelection, 2);
eventManager.focus();
}
}
/**
* @param {!UIEvent} event
* @return {undefined}
*/
function handleMouseClickEvent(event) {
var target = getTarget(event),
clickEvent,
range,
wasCollapsed,
frameNode,
pos;
drawShadowCursorTask.processRequests(); // Resynchronise the shadow cursor before processing anything else
if (clickStartedWithinCanvas) {
// Each mouse down event should only ever result in a single mouse click being processed.
// This is to cope with there being no hard rules about whether a contextmenu
// should be followed by a mouseup as well according to the HTML5 specs.
// See http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
// We don't want to just select the image if it is a range selection hence ensure the selection is collapsed.
if (odfUtils.isImage(target) && odfUtils.isCharacterFrame(target.parentNode) && window.getSelection().isCollapsed) {
selectionController.selectImage(/**@type{!Node}*/(target.parentNode));
eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
} else if (imageSelector.isSelectorElement(target)) {
eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
} else if (isMouseMoved) {
range = shadowCursor.getSelectedRange();
wasCollapsed = range.collapsed;
// Resets the endContainer and endOffset when a forward selection end up on an image;
// Otherwise the image will not be selected because endContainer: image, endOffset 0 is not a valid
// cursor position.
if (odfUtils.isImage(range.endContainer) && range.endOffset === 0
&& odfUtils.isCharacterFrame(range.endContainer.parentNode)) {
frameNode = /**@type{!Element}*/(range.endContainer.parentNode);
pos = getNextWalkablePosition(frameNode);
if (pos) {
range.setEnd(pos.container, pos.offset);
if (wasCollapsed) {
range.collapse(false); // collapses the range to its end
}
}
}
selectionController.selectRange(range, shadowCursor.hasForwardSelection(), computeClickCount(event));
eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
} else {
// Clicking in already selected text won't update window.getSelection() until just after
// the click is processed. Set 0 timeout here so the newly clicked position can be updated
// by the browser. Unfortunately this is only working in Firefox. For other browsers, we have to work
// out the caret position from two coordinates.
// In iOS, however, it is not possible to assign focus within a timeout. But in that case
// we do not even need a timeout, because we do not use native selections at all there,
// therefore for that platform, just directly move by the mouse click and give focus.
if (isIOS) {
moveByMouseClickEvent(event);
} else {
// IE10 destructs event objects once the event handler is done, so create a copy of the data.
// "The event object is only available during an event; that is, you can use it in event handlers but not in other code"
// (from http://msdn.microsoft.com/en-us/library/ie/aa703876(v=vs.85).aspx)
// TODO: IE10 on a test machine does not have the "detail" property set on "mouseup" events here,
// even if the docs claim it should exist, cmp. http://msdn.microsoft.com/en-au/library/ie/ff974344(v=vs.85).aspx
// So doubleclicks will not be detected on (some?) IE currently.
clickEvent = /**@type{!UIEvent}*/(domUtils.cloneEvent(event));
handleMouseClickTimeoutId = runtime.setTimeout(function () {
moveByMouseClickEvent(clickEvent);
}, 0);
}
}
// TODO assumes the mouseup/contextmenu is the same button as the mousedown that initialized the clickCount
clickCount = 0;
clickStartedWithinCanvas = false;
isMouseMoved = false;
}
}
/**
* @param {!MouseEvent} e
* @return {undefined}
*/
function handleDragStart(e) {
var cursor = odtDocument.getCursor(inputMemberId),
selectedRange = cursor.getSelectedRange();
if (selectedRange.collapsed) {
return;
}
mimeDataExporter.exportRangeToDataTransfer(/**@type{!DataTransfer}*/(e.dataTransfer), selectedRange);
}
function handleDragEnd() {
// Drag operations consume the corresponding mouse up event.
// If this happens, the selection should still be reset.
if (clickStartedWithinCanvas) {
eventManager.focus();
}
clickCount = 0;
clickStartedWithinCanvas = false;
isMouseMoved = false;
}
/**
* @param {!UIEvent} e
*/
function handleContextMenu(e) {
// TODO Various browsers have different default behaviours on right click
// We can detect this at runtime without doing any kind of platform sniffing
// simply by observing what the browser has tried to do on right-click.
// - OSX: Safari/Chrome - Expand to word boundary
// - OSX: Firefox - No expansion
// - Windows: Safari/Chrome/Firefox - No expansion
handleMouseClickEvent(e);
}
/**
* @param {!UIEvent} event
*/
function handleMouseUp(event) {
var target = /**@type{!Element}*/(getTarget(event)),
annotationNode = null;
if (target.className === "annotationRemoveButton") {
runtime.assert(annotationsEnabled, "Remove buttons are displayed on annotations while annotation editing is disabled in the controller.");
annotationNode = /**@type{!Element}*/(target.parentNode).getElementsByTagNameNS(odf.Namespaces.officens, 'annotation').item(0);
annotationController.removeAnnotation(/**@type{!Element}*/(annotationNode));
eventManager.focus();
} else {
if (target.getAttribute('class') !== 'webodf-draggable') {
handleMouseClickEvent(event);
}
}
}
/**
* Handle composition end event. If there is data specified, treat this as text
* to be inserted into the document.
* @param {!CompositionEvent} e
*/
function insertNonEmptyData(e) {
// https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html#event-type-compositionend
var input = e.data;
if (input) {
if (input.indexOf("\n") === -1) {
textController.insertText(input);
} else {
// Multi-line input should be handled as if it was pasted, rather than inserted as one giant
// single string.
pasteController.paste(input);
}
}
}
/**
* Executes the provided function and returns true
* Used to swallow events regardless of whether an operation was created
* @param {!Function} fn
* @return {!Function}
*/
function returnTrue(fn) {
return function () {
fn();
return true;
};
}
/**
* Executes the given function on range selection only
* @param {function(T):(boolean|undefined)} fn
* @return {function(T):(boolean|undefined)}
* @template T
*/
function rangeSelectionOnly(fn) {
/**
* @param {*} e
* @return {!boolean|undefined}
*/
function f(e) {
var selectionType = odtDocument.getCursor(inputMemberId).getSelectionType();
if (selectionType === ops.OdtCursor.RangeSelection) {
return fn(e);
}
return true;
}
return f;
}
/**
* Inserts the local cursor.
* @return {undefined}
*/
function insertLocalCursor() {
runtime.assert(session.getOdtDocument().getCursor(inputMemberId) === undefined, "Inserting local cursor a second time.");
var op = new ops.OpAddCursor();
op.init({memberid: inputMemberId});
session.enqueue([op]);
// Immediately capture focus when the local cursor is inserted
eventManager.focus();
}
this.insertLocalCursor = insertLocalCursor;
/**
* Removes the local cursor.
* @return {undefined}
*/
function removeLocalCursor() {
runtime.assert(session.getOdtDocument().getCursor(inputMemberId) !== undefined, "Removing local cursor without inserting before.");
var op = new ops.OpRemoveCursor();
op.init({memberid: inputMemberId});
session.enqueue([op]);
}
this.removeLocalCursor = removeLocalCursor;
/**
* @return {undefined}
*/
this.startEditing = function () {
inputMethodEditor.subscribe(gui.InputMethodEditor.signalCompositionStart, textController.removeCurrentSelection);
inputMethodEditor.subscribe(gui.InputMethodEditor.signalCompositionEnd, insertNonEmptyData);
eventManager.subscribe("beforecut", handleBeforeCut);
eventManager.subscribe("cut", handleCut);
eventManager.subscribe("beforepaste", handleBeforePaste);
eventManager.subscribe("paste", handlePaste);
if (undoManager) {
// For most undo managers, the initial state is a clean document *with* a cursor present
undoManager.initialize();
}
eventManager.setEditing(true);
hyperlinkClickHandler.setModifier(isMacOS ? modifier.Meta : modifier.Ctrl);
// Most browsers will go back one page when given an unhandled backspace press
// To prevent this, the event handler for this key should always return true
keyDownHandler.bind(keyCode.Backspace, modifier.None, returnTrue(textController.removeTextByBackspaceKey), true);
keyDownHandler.bind(keyCode.Delete, modifier.None, textController.removeTextByDeleteKey);
// TODO: deselect the currently selected image when press Esc
// TODO: move the image selection box to next image/frame when press tab on selected image
keyDownHandler.bind(keyCode.Tab, modifier.None, rangeSelectionOnly(function () {
textController.insertText("\t");
return true;
}));
if (isMacOS) {
keyDownHandler.bind(keyCode.Clear, modifier.None, textController.removeCurrentSelection);
keyDownHandler.bind(keyCode.B, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleBold));
keyDownHandler.bind(keyCode.I, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleItalic));
keyDownHandler.bind(keyCode.U, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleUnderline));
keyDownHandler.bind(keyCode.L, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphLeft));
keyDownHandler.bind(keyCode.E, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphCenter));
keyDownHandler.bind(keyCode.R, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphRight));
keyDownHandler.bind(keyCode.J, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphJustified));
if (annotationsEnabled) {
keyDownHandler.bind(keyCode.C, modifier.MetaShift, annotationController.addAnnotation);
}
keyDownHandler.bind(keyCode.Z, modifier.Meta, undo);
keyDownHandler.bind(keyCode.Z, modifier.MetaShift, redo);
} else {
keyDownHandler.bind(keyCode.B, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleBold));
keyDownHandler.bind(keyCode.I, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleItalic));
keyDownHandler.bind(keyCode.U, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleUnderline));
keyDownHandler.bind(keyCode.L, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphLeft));
keyDownHandler.bind(keyCode.E, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphCenter));
keyDownHandler.bind(keyCode.R, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphRight));
keyDownHandler.bind(keyCode.J, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphJustified));
if (annotationsEnabled) {
keyDownHandler.bind(keyCode.C, modifier.CtrlAlt, annotationController.addAnnotation);
}
keyDownHandler.bind(keyCode.Z, modifier.Ctrl, undo);
keyDownHandler.bind(keyCode.Z, modifier.CtrlShift, redo);
}
// the default action is to insert text into the document
/**
* @param {!KeyboardEvent} e
* @return {boolean|undefined}
*/
function handler(e) {
var text = stringFromKeyPress(e);
if (text && !(e.altKey || e.ctrlKey || e.metaKey)) {
textController.insertText(text);
return true;
}
return false;
}
keyPressHandler.setDefault(rangeSelectionOnly(handler));
keyPressHandler.bind(keyCode.Enter, modifier.None, rangeSelectionOnly(textController.enqueueParagraphSplittingOps));
};
/**
* @return {undefined}
*/
this.endEditing = function () {
inputMethodEditor.unsubscribe(gui.InputMethodEditor.signalCompositionStart, textController.removeCurrentSelection);
inputMethodEditor.unsubscribe(gui.InputMethodEditor.signalCompositionEnd, insertNonEmptyData);
eventManager.unsubscribe("cut", handleCut);
eventManager.unsubscribe("beforecut", handleBeforeCut);
eventManager.unsubscribe("paste", handlePaste);
eventManager.unsubscribe("beforepaste", handleBeforePaste);
eventManager.setEditing(false);
hyperlinkClickHandler.setModifier(modifier.None);
keyDownHandler.bind(keyCode.Backspace, modifier.None, function () { return true; }, true);
keyDownHandler.unbind(keyCode.Delete, modifier.None);
keyDownHandler.unbind(keyCode.Tab, modifier.None);
if (isMacOS) {
keyDownHandler.unbind(keyCode.Clear, modifier.None);
keyDownHandler.unbind(keyCode.B, modifier.Meta);
keyDownHandler.unbind(keyCode.I, modifier.Meta);
keyDownHandler.unbind(keyCode.U, modifier.Meta);
keyDownHandler.unbind(keyCode.L, modifier.MetaShift);
keyDownHandler.unbind(keyCode.E, modifier.MetaShift);
keyDownHandler.unbind(keyCode.R, modifier.MetaShift);
keyDownHandler.unbind(keyCode.J, modifier.MetaShift);
if (annotationsEnabled) {
keyDownHandler.unbind(keyCode.C, modifier.MetaShift);
}
keyDownHandler.unbind(keyCode.Z, modifier.Meta);
keyDownHandler.unbind(keyCode.Z, modifier.MetaShift);
} else {
keyDownHandler.unbind(keyCode.B, modifier.Ctrl);
keyDownHandler.unbind(keyCode.I, modifier.Ctrl);
keyDownHandler.unbind(keyCode.U, modifier.Ctrl);
keyDownHandler.unbind(keyCode.L, modifier.CtrlShift);
keyDownHandler.unbind(keyCode.E, modifier.CtrlShift);
keyDownHandler.unbind(keyCode.R, modifier.CtrlShift);
keyDownHandler.unbind(keyCode.J, modifier.CtrlShift);
if (annotationsEnabled) {
keyDownHandler.unbind(keyCode.C, modifier.CtrlAlt);
}
keyDownHandler.unbind(keyCode.Z, modifier.Ctrl);
keyDownHandler.unbind(keyCode.Z, modifier.CtrlShift);
}
keyPressHandler.setDefault(null);
keyPressHandler.unbind(keyCode.Enter, modifier.None);
};
/**
* @return {!string}
*/
this.getInputMemberId = function () {
return inputMemberId;
};
/**
* @return {!ops.Session}
*/
this.getSession = function () {
return session;
};
/**
* @return {!gui.SessionConstraints}
*/
this.getSessionConstraints = function () {
return sessionConstraints;
};
/**
* @param {?gui.UndoManager} manager
* @return {undefined}
*/
this.setUndoManager = function (manager) {
if (undoManager) {
undoManager.unsubscribe(gui.UndoManager.signalUndoStackChanged, forwardUndoStackChange);
}
undoManager = manager;
if (undoManager) {
undoManager.setDocument(odtDocument);
// As per gui.UndoManager, this should NOT fire any signals or report
// events being executed back to the undo manager.
undoManager.setPlaybackFunction(session.enqueue);
undoManager.subscribe(gui.UndoManager.signalUndoStackChanged, forwardUndoStackChange);
}
};
/**
* @return {?gui.UndoManager}
*/
this.getUndoManager = function () {
return undoManager;
};
/**
* @return {!gui.MetadataController}
*/
this.getMetadataController = function () {
return metadataController;
};
/**
* @return {?gui.AnnotationController}
*/
this.getAnnotationController = function () {
return annotationController;
};
/**
* @return {!gui.DirectFormattingController}
*/
this.getDirectFormattingController = function () {
return directFormattingController;
};
/**
* @return {!gui.HyperlinkClickHandler}
*/
this.getHyperlinkClickHandler = function () {
return hyperlinkClickHandler;
};
/**
* @return {!gui.HyperlinkController}
*/
this.getHyperlinkController = function () {
return hyperlinkController;
};
/**
* @return {!gui.ImageController}
*/
this.getImageController = function () {
return imageController;
};
/**
* @return {!gui.SelectionController}
*/
this.getSelectionController = function () {
return selectionController;
};
/**
* @return {!gui.TextController}
*/
this.getTextController = function () {
return textController;
};
/**
* @return {!gui.EventManager}
*/
this.getEventManager = function() {
return eventManager;
};
/**
* Return the keyboard event handlers
* @return {{keydown: gui.KeyboardHandler, keypress: gui.KeyboardHandler}}
*/
this.getKeyboardHandlers = function () {
return {
keydown: keyDownHandler,
keypress: keyPressHandler
};
};
/**
* @param {!function(!Object=)} callback passing an error object in case of error
* @return {undefined}
*/
function destroy(callback) {
eventManager.unsubscribe("keydown", keyDownHandler.handleEvent);
eventManager.unsubscribe("keypress", keyPressHandler.handleEvent);
eventManager.unsubscribe("keyup", keyUpHandler.handleEvent);
eventManager.unsubscribe("copy", handleCopy);
eventManager.unsubscribe("mousedown", handleMouseDown);
eventManager.unsubscribe("mousemove", drawShadowCursorTask.trigger);
eventManager.unsubscribe("mouseup", handleMouseUp);
eventManager.unsubscribe("contextmenu", handleContextMenu);
eventManager.unsubscribe("dragstart", handleDragStart);
eventManager.unsubscribe("dragend", handleDragEnd);
eventManager.unsubscribe("click", hyperlinkClickHandler.handleClick