UNPKG

node-webodf

Version:

WebODF - JavaScript Document Engine http://webodf.org/

950 lines (838 loc) 34.5 kB
/** * Copyright (C) 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 core, ops, odf, gui, runtime*/ /** * @constructor * @implements {core.Destroyable} * @param {!ops.Session} session * @param {!gui.SessionConstraints} sessionConstraints * @param {!gui.SessionContext} sessionContext * @param {!string} inputMemberId * @param {!odf.ObjectNameGenerator} objectNameGenerator * @param {!boolean} directTextStylingEnabled * @param {!boolean} directParagraphStylingEnabled */ gui.DirectFormattingController = function DirectFormattingController( session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator, directTextStylingEnabled, directParagraphStylingEnabled ) { "use strict"; var self = this, odtDocument = session.getOdtDocument(), utils = new core.Utils(), odfUtils = odf.OdfUtils, eventNotifier = new core.EventNotifier([ gui.DirectFormattingController.enabledChanged, gui.DirectFormattingController.textStylingChanged, gui.DirectFormattingController.paragraphStylingChanged ]), /**@const*/ textns = odf.Namespaces.textns, /**@const*/ NEXT = core.StepDirection.NEXT, /**@type{?odf.Formatting.StyleData}*/ directCursorStyleProperties = null, // cached text settings /**@type{!gui.DirectFormattingController.SelectionInfo}*/ lastSignalledSelectionInfo, /**@type {!core.LazyProperty.<!gui.DirectFormattingController.SelectionInfo>} */ selectionInfoCache; /** * Gets the current selection information style summary * @return {!gui.StyleSummary} */ function getCachedStyleSummary() { return selectionInfoCache.value().styleSummary; } /** * @return {!{directTextStyling: !boolean, directParagraphStyling: !boolean}} */ function getCachedEnabledFeatures() { return selectionInfoCache.value().enabledFeatures; } this.enabledFeatures = getCachedEnabledFeatures; /** * Fetch all the character elements and text nodes in the specified range, or if the range is collapsed, the node just to * the left of the cursor. * @param {!Range} range * @return {!Array.<!Node>} */ function getNodes(range) { var container, nodes; if (range.collapsed) { container = range.startContainer; // Attempt to find the node at the specified startOffset within the startContainer. // In the case where a range starts at (parent, 1), this will mean the // style information is retrieved for the child node at index 1. // Also, need to check the length is less than the number of child nodes, as a range is // legally able to start at (parent, parent.childNodes.length). if (container.hasChildNodes() && range.startOffset < container.childNodes.length) { container = container.childNodes.item(range.startOffset); } nodes = [container]; } else { nodes = odfUtils.getTextElements(range, true, false); } return nodes; } /** * Get all styles currently applied to the selected range. If the range is collapsed, * this will return the style the next inserted character will have * @return {!gui.DirectFormattingController.SelectionInfo} */ function getSelectionInfo() { var cursor = odtDocument.getCursor(inputMemberId), range = cursor && cursor.getSelectedRange(), nodes = [], /**@type{!Array.<!odf.Formatting.AppliedStyle>}*/ selectionStyles = [], selectionContainsText = true, enabledFeatures = { directTextStyling: true, directParagraphStyling: true }; if (range) { nodes = getNodes(range); if (nodes.length === 0) { nodes = [range.startContainer, range.endContainer]; selectionContainsText = false; } selectionStyles = odtDocument.getFormatting().getAppliedStyles(nodes); } if (selectionStyles[0] !== undefined && directCursorStyleProperties) { // direct cursor styles add to the style of the existing range, overriding where defined selectionStyles[0].styleProperties = utils.mergeObjects(selectionStyles[0].styleProperties, directCursorStyleProperties); } if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) { enabledFeatures.directTextStyling = enabledFeatures.directParagraphStyling = /**@type{!boolean}*/(sessionContext.isLocalCursorWithinOwnAnnotation()); } if (enabledFeatures.directTextStyling) { enabledFeatures.directTextStyling = selectionContainsText && cursor !== undefined // cursor might not be registered && cursor.getSelectionType() === ops.OdtCursor.RangeSelection; } return /**@type{!gui.DirectFormattingController.SelectionInfo}*/({ enabledFeatures: enabledFeatures, appliedStyles: selectionStyles, styleSummary: new gui.StyleSummary(selectionStyles) }); } /** * Create a map containing all the keys that have a different value * in the new summary object. * @param {!Object.<string,function():*>} oldSummary * @param {!Object.<string,function():*>} newSummary * @return {!Object.<!string, *>} */ function createDiff(oldSummary, newSummary) { var diffMap = {}; Object.keys(oldSummary).forEach(function (funcName) { var oldValue = oldSummary[funcName](), newValue = newSummary[funcName](); if (oldValue !== newValue) { diffMap[funcName] = newValue; } }); return diffMap; } /** * @return {undefined} */ function emitSelectionChanges() { var textStyleDiff, paragraphStyleDiff, lastStyleSummary = lastSignalledSelectionInfo.styleSummary, newSelectionInfo = selectionInfoCache.value(), newSelectionStylesSummary = newSelectionInfo.styleSummary, lastEnabledFeatures = lastSignalledSelectionInfo.enabledFeatures, newEnabledFeatures = newSelectionInfo.enabledFeatures, enabledFeaturesChanged; textStyleDiff = createDiff(lastStyleSummary.text, newSelectionStylesSummary.text); paragraphStyleDiff = createDiff(lastStyleSummary.paragraph, newSelectionStylesSummary.paragraph); enabledFeaturesChanged = !(newEnabledFeatures.directTextStyling === lastEnabledFeatures.directTextStyling && newEnabledFeatures.directParagraphStyling === lastEnabledFeatures.directParagraphStyling); lastSignalledSelectionInfo = newSelectionInfo; if (enabledFeaturesChanged) { eventNotifier.emit(gui.DirectFormattingController.enabledChanged, newEnabledFeatures); } if (Object.keys(textStyleDiff).length > 0) { eventNotifier.emit(gui.DirectFormattingController.textStylingChanged, textStyleDiff); } if (Object.keys(paragraphStyleDiff).length > 0) { eventNotifier.emit(gui.DirectFormattingController.paragraphStylingChanged, paragraphStyleDiff); } } /** * Resets and immediately updates the current selection info. VERY SLOW... please use sparingly. * @return {undefined} */ function forceSelectionInfoRefresh() { selectionInfoCache.reset(); emitSelectionChanges(); } /** * @param {!ops.OdtCursor|!string} cursorOrId * @return {undefined} */ function onCursorEvent(cursorOrId) { var cursorMemberId = (typeof cursorOrId === "string") ? cursorOrId : cursorOrId.getMemberId(); if (cursorMemberId === inputMemberId) { selectionInfoCache.reset(); } } /** * @return {undefined} */ function onParagraphStyleModified() { // TODO: check if the cursor (selection) is actually affected selectionInfoCache.reset(); } /** * @param {!{paragraphElement:Element}} args * @return {undefined} */ function onParagraphChanged(args) { var cursor = odtDocument.getCursor(inputMemberId), p = args.paragraphElement; if (cursor && odfUtils.getParagraphElement(cursor.getNode()) === p) { selectionInfoCache.reset(); } } /** * @param {!function():boolean} predicate * @param {!function(!boolean):undefined} toggleMethod * @return {!boolean} */ function toggle(predicate, toggleMethod) { toggleMethod(!predicate()); return true; } /** * Apply the supplied text properties to the current range. If no range is selected, * this styling will be applied to the next character entered. * @param {!odf.Formatting.StyleData} textProperties * @return {undefined} */ function formatTextSelection(textProperties) { if (!getCachedEnabledFeatures().directTextStyling) { return; } var selection = odtDocument.getCursorSelection(inputMemberId), op, properties = {'style:text-properties' : textProperties}; if (selection.length !== 0) { op = new ops.OpApplyDirectStyling(); op.init({ memberid: inputMemberId, position: selection.position, length: selection.length, setProperties: properties }); session.enqueue([op]); } else { // Direct styling is additive. E.g., if the user selects bold and then italic, the intent is to produce // bold & italic text directCursorStyleProperties = utils.mergeObjects(directCursorStyleProperties || {}, properties); selectionInfoCache.reset(); } } this.formatTextSelection = formatTextSelection; /** * @param {!string} propertyName * @param {!string} propertyValue * @return {undefined} */ function applyTextPropertyToSelection(propertyName, propertyValue) { var textProperties = {}; textProperties[propertyName] = propertyValue; formatTextSelection(textProperties); } /** * Generate an operation that would apply the current direct cursor styling to the specified * position and length * @param {!number} position * @param {!number} length * @param {!boolean} useCachedStyle * @return {ops.Operation} */ this.createCursorStyleOp = function (position, length, useCachedStyle) { var styleOp = null, /**@type{!odf.Formatting.AppliedStyle|undefined}*/ appliedStyles, /**@type{?odf.Formatting.StyleData|undefined}*/ properties = directCursorStyleProperties; if (useCachedStyle) { appliedStyles = selectionInfoCache.value().appliedStyles[0]; properties = appliedStyles && appliedStyles.styleProperties; } if (properties && properties['style:text-properties']) { styleOp = new ops.OpApplyDirectStyling(); styleOp.init({ memberid: inputMemberId, position: position, length: length, setProperties: {'style:text-properties': properties['style:text-properties']} }); directCursorStyleProperties = null; selectionInfoCache.reset(); } return styleOp; }; /** * Listen for local operations and clear the local cursor styling if necessary * @param {!ops.Operation} op */ function clearCursorStyle(op) { var spec = op.spec(); if (directCursorStyleProperties && spec.memberid === inputMemberId) { if (spec.optype !== "SplitParagraph") { // Most operations by the local user should clear the current cursor style // SplitParagraph is an exception because at the time the split occurs, there has been no element // added to apply the style to. Even after a split, the cursor should still style the next inserted // character directCursorStyleProperties = null; selectionInfoCache.reset(); } } } /** * @param {!boolean} checked * @return {undefined} */ function setBold(checked) { var value = checked ? 'bold' : 'normal'; applyTextPropertyToSelection('fo:font-weight', value); } this.setBold = setBold; /** * @param {!boolean} checked * @return {undefined} */ function setItalic(checked) { var value = checked ? 'italic' : 'normal'; applyTextPropertyToSelection('fo:font-style', value); } this.setItalic = setItalic; /** * @param {!boolean} checked * @return {undefined} */ function setHasUnderline(checked) { var value = checked ? 'solid' : 'none'; applyTextPropertyToSelection('style:text-underline-style', value); } this.setHasUnderline = setHasUnderline; /** * @param {!boolean} checked * @return {undefined} */ function setHasStrikethrough(checked) { var value = checked ? 'solid' : 'none'; applyTextPropertyToSelection('style:text-line-through-style', value); } this.setHasStrikethrough = setHasStrikethrough; /** * @param {!number} value * @return {undefined} */ function setFontSize(value) { applyTextPropertyToSelection('fo:font-size', value + "pt"); } this.setFontSize = setFontSize; /** * @param {!string} value * @return {undefined} */ function setFontName(value) { applyTextPropertyToSelection('style:font-name', value); } this.setFontName = setFontName; /** * Get all styles currently applied to the selected range. If the range is collapsed, * this will return the style the next inserted character will have. * (Note, this is not used internally by WebODF, but is provided as a convenience method * for external consumers) * @return {!Array.<!odf.Formatting.AppliedStyle>} */ this.getAppliedStyles = function () { return selectionInfoCache.value().appliedStyles; }; /** * @return {!boolean} */ this.toggleBold = toggle.bind(self, function () { return getCachedStyleSummary().isBold(); }, setBold); /** * @return {!boolean} */ this.toggleItalic = toggle.bind(self, function () { return getCachedStyleSummary().isItalic(); }, setItalic); /** * @return {!boolean} */ this.toggleUnderline = toggle.bind(self, function () { return getCachedStyleSummary().hasUnderline(); }, setHasUnderline); /** * @return {!boolean} */ this.toggleStrikethrough = toggle.bind(self, function () { return getCachedStyleSummary().hasStrikeThrough(); }, setHasStrikethrough); /** * @return {!boolean} */ this.isBold = function () { return getCachedStyleSummary().isBold(); }; /** * @return {!boolean} */ this.isItalic = function () { return getCachedStyleSummary().isItalic(); }; /** * @return {!boolean} */ this.hasUnderline = function () { return getCachedStyleSummary().hasUnderline(); }; /** * @return {!boolean} */ this.hasStrikeThrough = function () { return getCachedStyleSummary().hasStrikeThrough(); }; /** * @return {number|undefined} */ this.fontSize = function () { return getCachedStyleSummary().fontSize(); }; /** * @return {string|undefined} */ this.fontName = function () { return getCachedStyleSummary().fontName(); }; /** * @return {!boolean} */ this.isAlignedLeft = function () { return getCachedStyleSummary().isAlignedLeft(); }; /** * @return {!boolean} */ this.isAlignedCenter = function () { return getCachedStyleSummary().isAlignedCenter(); }; /** * @return {!boolean} */ this.isAlignedRight = function () { return getCachedStyleSummary().isAlignedRight(); }; /** * @return {!boolean} */ this.isAlignedJustified = function () { return getCachedStyleSummary().isAlignedJustified(); }; /** * @param {!Object.<string,string>} obj * @param {string} key * @return {string|undefined} */ function getOwnProperty(obj, key) { return obj.hasOwnProperty(key) ? obj[key] : undefined; } /** * @param {!function(!odf.Formatting.StyleData) : !odf.Formatting.StyleData} applyDirectStyling * @return {undefined} */ function applyParagraphDirectStyling(applyDirectStyling) { if (!getCachedEnabledFeatures().directParagraphStyling) { return; } var range = odtDocument.getCursor(inputMemberId).getSelectedRange(), paragraphs = odfUtils.getParagraphElements(range), formatting = odtDocument.getFormatting(), operations = [], derivedStyleNames = {}, /**@type{string|undefined}*/ defaultStyleName; paragraphs.forEach(function (paragraph) { var paragraphStartPoint = odtDocument.convertDomPointToCursorStep(paragraph, 0, NEXT), paragraphStyleName = paragraph.getAttributeNS(odf.Namespaces.textns, "style-name"), /**@type{string|undefined}*/ newParagraphStyleName, opAddStyle, opSetParagraphStyle, paragraphProperties; // Try and reuse an existing paragraph style if possible if (paragraphStyleName) { newParagraphStyleName = getOwnProperty(derivedStyleNames, paragraphStyleName); } else { newParagraphStyleName = defaultStyleName; } if (!newParagraphStyleName) { newParagraphStyleName = objectNameGenerator.generateStyleName(); if (paragraphStyleName) { derivedStyleNames[paragraphStyleName] = newParagraphStyleName; paragraphProperties = formatting.createDerivedStyleObject(paragraphStyleName, "paragraph", {}); } else { defaultStyleName = newParagraphStyleName; paragraphProperties = {}; } // The assumption is that applyDirectStyling will return the same transform given the same // paragraph properties (e.g., there is nothing dependent on whether this is the 10th paragraph) paragraphProperties = applyDirectStyling(paragraphProperties); opAddStyle = new ops.OpAddStyle(); opAddStyle.init({ memberid: inputMemberId, styleName: newParagraphStyleName.toString(), styleFamily: 'paragraph', isAutomaticStyle: true, setProperties: paragraphProperties }); operations.push(opAddStyle); } opSetParagraphStyle = new ops.OpSetParagraphStyle(); opSetParagraphStyle.init({ memberid: inputMemberId, styleName: newParagraphStyleName.toString(), position: paragraphStartPoint }); operations.push(opSetParagraphStyle); }); session.enqueue(operations); } /** * @param {!odf.Formatting.StyleData} styleOverrides * @return {undefined} */ function applySimpleParagraphDirectStyling(styleOverrides) { applyParagraphDirectStyling(function (paragraphStyle) { return /**@type {!odf.Formatting.StyleData}*/(utils.mergeObjects(paragraphStyle, styleOverrides)); }); } /** * @param {!string} alignment * @return {undefined} */ function alignParagraph(alignment) { applySimpleParagraphDirectStyling({"style:paragraph-properties" : {"fo:text-align" : alignment}}); } /** * @return {!boolean} */ this.alignParagraphLeft = function () { alignParagraph('left'); return true; }; /** * @return {!boolean} */ this.alignParagraphCenter = function () { alignParagraph('center'); return true; }; /** * @return {!boolean} */ this.alignParagraphRight = function () { alignParagraph('right'); return true; }; /** * @return {!boolean} */ this.alignParagraphJustified = function () { alignParagraph('justify'); return true; }; /** * @param {!number} direction * @param {!odf.Formatting.StyleData} paragraphStyle * @return {!odf.Formatting.StyleData} */ function modifyParagraphIndent(direction, paragraphStyle) { var tabStopDistance = odtDocument.getFormatting().getDefaultTabStopDistance(), paragraphProperties = paragraphStyle["style:paragraph-properties"], indentValue, indent, newIndent; if (paragraphProperties) { indentValue = /**@type {!string}*/(paragraphProperties["fo:margin-left"]); indent = odfUtils.parseLength(indentValue); } if (indent && indent.unit === tabStopDistance.unit) { newIndent = (indent.value + (direction * tabStopDistance.value)) + indent.unit; } else { // TODO unit-conversion would allow indent to work irrespective of the paragraph's indent type newIndent = (direction * tabStopDistance.value) + tabStopDistance.unit; } return /**@type {!odf.Formatting.StyleData}*/(utils.mergeObjects(paragraphStyle, {"style:paragraph-properties" : {"fo:margin-left" : newIndent}})); } /** * @return {!boolean} */ this.indent = function () { applyParagraphDirectStyling(modifyParagraphIndent.bind(null, 1)); return true; }; /** * @return {!boolean} */ this.outdent = function () { applyParagraphDirectStyling(modifyParagraphIndent.bind(null, -1)); return true; }; /** * Check if the selection ends at the last step of a paragraph. * @param {!Range} range * @param {!Node} paragraphNode * @return {boolean} */ function isSelectionAtTheEndOfLastParagraph(range, paragraphNode) { var stepIterator, filters = [odtDocument.getPositionFilter(), odtDocument.createRootFilter(inputMemberId)]; stepIterator = odtDocument.createStepIterator(/**@type{!Node}*/(range.endContainer), range.endOffset, filters, paragraphNode); return stepIterator.nextStep() === false; } /** * Returns true if the first text node in the selection has different text style from the first paragraph; otherwise false. * @param {!Range} range * @param {!Node} paragraphNode * @return {!boolean} */ function isTextStyleDifferentFromFirstParagraph(range, paragraphNode) { var textNodes = getNodes(range), // If the selection has no text nodes, fetch the text style at the start of the range instead selectedNodes = textNodes.length === 0 ? [range.startContainer] : textNodes, appliedTextStyles = odtDocument.getFormatting().getAppliedStyles(selectedNodes), textStyle = appliedTextStyles.length > 0 ? appliedTextStyles[0].styleProperties : undefined, paragraphStyle = odtDocument.getFormatting().getAppliedStylesForElement(paragraphNode).styleProperties; if (!textStyle || textStyle['style:family'] !== 'text' || !textStyle['style:text-properties']) { return false; } if (!paragraphStyle || !paragraphStyle['style:text-properties']) { return true; } textStyle = /**@type {!odf.Formatting.StyleData}*/(textStyle['style:text-properties']); paragraphStyle = /**@type {!odf.Formatting.StyleData}*/(paragraphStyle['style:text-properties']); // <style:text-properties> only has attributes, so no need to look for child elements (as in: objects) return !Object.keys(textStyle).every(function (key) { return textStyle[key] === paragraphStyle[key]; }); } /** * TODO: HACK, REMOVE * Generates operations that would create and apply the current direct cursor * styling to the paragraph at given position. * @param {number} position * @return {!Array.<!ops.Operation>} */ this.createParagraphStyleOps = function (position) { if (!getCachedEnabledFeatures().directParagraphStyling) { return []; } var cursor = odtDocument.getCursor(inputMemberId), range = cursor.getSelectedRange(), operations = [], op, startNode, endNode, paragraphNode, appliedStyles, properties, parentStyleName, styleName; if (cursor.hasForwardSelection()) { startNode = cursor.getAnchorNode(); endNode = cursor.getNode(); } else { startNode = cursor.getNode(); endNode = cursor.getAnchorNode(); } paragraphNode = /**@type{!Element}*/(odfUtils.getParagraphElement(endNode)); runtime.assert(Boolean(paragraphNode), "DirectFormattingController: Cursor outside paragraph"); if (!isSelectionAtTheEndOfLastParagraph(range, paragraphNode)) { return operations; } if (endNode !== startNode) { paragraphNode = /**@type{!Element}*/(odfUtils.getParagraphElement(startNode)); } if (!directCursorStyleProperties && !isTextStyleDifferentFromFirstParagraph(range, paragraphNode)) { return operations; } appliedStyles = selectionInfoCache.value().appliedStyles[0]; properties = appliedStyles && appliedStyles.styleProperties; if (!properties) { return operations; } parentStyleName = paragraphNode.getAttributeNS(textns, 'style-name'); if (parentStyleName) { properties = { 'style:text-properties': properties['style:text-properties'] }; properties = odtDocument.getFormatting().createDerivedStyleObject(parentStyleName, 'paragraph', properties); } styleName = objectNameGenerator.generateStyleName(); op = new ops.OpAddStyle(); op.init({ memberid: inputMemberId, styleName: styleName, styleFamily: 'paragraph', isAutomaticStyle: true, setProperties: properties }); operations.push(op); op = new ops.OpSetParagraphStyle(); op.init({ memberid: inputMemberId, styleName: styleName, position: position }); operations.push(op); return operations; }; /** * @param {!string} eventid * @param {!Function} cb * @return {undefined} */ this.subscribe = function (eventid, cb) { eventNotifier.subscribe(eventid, cb); }; /** * @param {!string} eventid * @param {!Function} cb * @return {undefined} */ this.unsubscribe = function (eventid, cb) { eventNotifier.unsubscribe(eventid, cb); }; /** * @param {!function(!Error=)} callback passing an error object in case of error * @return {undefined} */ this.destroy = function (callback) { odtDocument.unsubscribe(ops.Document.signalCursorAdded, onCursorEvent); odtDocument.unsubscribe(ops.Document.signalCursorRemoved, onCursorEvent); odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorEvent); odtDocument.unsubscribe(ops.OdtDocument.signalParagraphStyleModified, onParagraphStyleModified); odtDocument.unsubscribe(ops.OdtDocument.signalParagraphChanged, onParagraphChanged); odtDocument.unsubscribe(ops.OdtDocument.signalOperationEnd, clearCursorStyle); odtDocument.unsubscribe(ops.OdtDocument.signalProcessingBatchEnd, emitSelectionChanges); sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, forceSelectionInfoRefresh); callback(); }; /** * @return {undefined} */ /*jslint emptyblock: true*/ function emptyFunction() { } /*jslint emptyblock: false*/ /** * @return {!boolean} */ function emptyBoolFunction () { return false; } /** * @return {!boolean} */ function emptyFalseReturningFunction() { return false; } /** * @return {!gui.DirectFormattingController.SelectionInfo} */ function getCachedSelectionInfo() { return selectionInfoCache.value(); } function init() { odtDocument.subscribe(ops.Document.signalCursorAdded, onCursorEvent); odtDocument.subscribe(ops.Document.signalCursorRemoved, onCursorEvent); odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent); odtDocument.subscribe(ops.OdtDocument.signalParagraphStyleModified, onParagraphStyleModified); odtDocument.subscribe(ops.OdtDocument.signalParagraphChanged, onParagraphChanged); odtDocument.subscribe(ops.OdtDocument.signalOperationEnd, clearCursorStyle); odtDocument.subscribe(ops.OdtDocument.signalProcessingBatchEnd, emitSelectionChanges); sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, forceSelectionInfoRefresh); selectionInfoCache = new core.LazyProperty(getSelectionInfo); // Using a function rather than calling selectionInfoCache.value() directly because Closure Compiler's generics // inference is quite limited and does not seem to recognise the core.LazyProperty type interface correctly // within the function that creates the instance. Everywhere else in this file has no issues with it though... lastSignalledSelectionInfo = getCachedSelectionInfo(); if (!directTextStylingEnabled) { self.formatTextSelection = emptyFunction; self.setBold = emptyFunction; self.setItalic = emptyFunction; self.setHasUnderline = emptyFunction; self.setHasStrikethrough = emptyFunction; self.setFontSize = emptyFunction; self.setFontName = emptyFunction; self.toggleBold = emptyFalseReturningFunction; self.toggleItalic = emptyFalseReturningFunction; self.toggleUnderline = emptyFalseReturningFunction; self.toggleStrikethrough = emptyFalseReturningFunction; } if (!directParagraphStylingEnabled) { self.alignParagraphCenter = emptyBoolFunction; self.alignParagraphJustified = emptyBoolFunction; self.alignParagraphLeft = emptyBoolFunction; self.alignParagraphRight = emptyBoolFunction; self.createParagraphStyleOps = function () { return []; }; self.indent = emptyBoolFunction; self.outdent = emptyBoolFunction; } } init(); }; /**@const*/gui.DirectFormattingController.enabledChanged = "enabled/changed"; /**@const*/gui.DirectFormattingController.textStylingChanged = "textStyling/changed"; /**@const*/gui.DirectFormattingController.paragraphStylingChanged = "paragraphStyling/changed"; /** * @constructor * @struct */ gui.DirectFormattingController.SelectionInfo = function() { "use strict"; /** * Specifies which forms of styling options are enabled for the current selection. * @type {!{directTextStyling: !boolean, directParagraphStyling: !boolean}} */ this.enabledFeatures; /** * Applied styles in the selection * @type {!Array.<!odf.Formatting.AppliedStyle>} */ this.appliedStyles; /** * Style summary for the selection * @type {!gui.StyleSummary} */ this.styleSummary; };