diff --git a/programs/editor/Tools.js b/programs/editor/Tools.js index b58f0ebd3..1a8607f1e 100644 --- a/programs/editor/Tools.js +++ b/programs/editor/Tools.js @@ -32,6 +32,7 @@ define("webodf/editor/Tools", [ "dijit/form/DropDownButton", "dijit/Toolbar", "webodf/editor/widgets/paragraphAlignment", + "webodf/editor/widgets/toggleLists", "webodf/editor/widgets/simpleStyles", "webodf/editor/widgets/undoRedoMenu", "webodf/editor/widgets/toolbarWidgets/currentStyle", @@ -42,7 +43,7 @@ define("webodf/editor/Tools", [ "webodf/editor/widgets/zoomSlider", "webodf/editor/widgets/aboutDialog", "webodf/editor/EditorSession"], - function (ready, MenuItem, DropDownMenu, Button, DropDownButton, Toolbar, ParagraphAlignment, SimpleStyles, UndoRedoMenu, CurrentStyle, AnnotationControl, EditHyperlinks, ImageInserter, ParagraphStylesDialog, ZoomSlider, AboutDialog, EditorSession) { + function (ready, MenuItem, DropDownMenu, Button, DropDownButton, Toolbar, ParagraphAlignment, ToggleLists, SimpleStyles, UndoRedoMenu, CurrentStyle, AnnotationControl, EditHyperlinks, ImageInserter, ParagraphStylesDialog, ZoomSlider, AboutDialog, EditorSession) { "use strict"; return function Tools(toolbarElementId, args) { @@ -59,6 +60,7 @@ define("webodf/editor/Tools", [ undoRedoMenu, editorSession, paragraphAlignment, + toggleLists, imageInserter, annotationControl, editHyperlinks, @@ -171,6 +173,9 @@ define("webodf/editor/Tools", [ // Paragraph direct alignment buttons paragraphAlignment = createTool(ParagraphAlignment, args.directParagraphStylingEnabled); + // Numbered and bulleted list toggle buttons + toggleLists = createTool(ToggleLists, args.listEditingEnabled); + // Paragraph Style Selector currentStyle = createTool(CurrentStyle, args.paragraphStyleSelectingEnabled); diff --git a/programs/editor/widgets/toggleLists.js b/programs/editor/widgets/toggleLists.js new file mode 100644 index 000000000..f3685e32d --- /dev/null +++ b/programs/editor/widgets/toggleLists.js @@ -0,0 +1,127 @@ +/** + * Copyright (C) 2010-2014 KO GmbH + * + * @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 . + * @licend + * + * @source: http://www.webodf.org/ + * @source: https://github.com/kogmbh/WebODF/ + */ + +/*global define,require */ + +define("webodf/editor/widgets/toggleLists", [ + "dijit/form/ToggleButton", + "webodf/editor/EditorSession"], + + function (ToggleButton, EditorSession) { + "use strict"; + + var ToggleLists = function (callback) { + var self = this, + editorSession, + listController, + widget = {}, + numberedList, + bulletedList; + + numberedList = new ToggleButton({ + label: runtime.tr('Numbering'), + disabled: true, + showLabel: false, + checked: false, + iconClass: "dijitEditorIcon dijitEditorIconInsertOrderedList", + onChange: function (checked) { + var success = listController.setNumberedList(checked); + //TODO: remove this when the list controller supports all use cases triggered by this button + if(!success) { + numberedList.set("checked", !checked, false); + } + self.onToolDone(); + } + }); + + bulletedList = new ToggleButton({ + label: runtime.tr('Bullets'), + disabled: true, + showLabel: false, + checked: false, + iconClass: "dijitEditorIcon dijitEditorIconInsertUnorderedList", + onChange: function (checked) { + var success = listController.setBulletedList(checked); + //TODO: remove this when the list controller supports all use cases triggered by this button + if(!success) { + bulletedList.set("checked", !checked, false); + } + self.onToolDone(); + } + }); + + widget.children = [numberedList, bulletedList]; + + widget.startup = function () { + widget.children.forEach(function (element) { + element.startup(); + }); + }; + + widget.placeAt = function (container) { + widget.children.forEach(function (element) { + element.placeAt(container); + }); + return widget; + }; + + function enableToggleButtons(isEnabled) { + widget.children.forEach(function (element) { + element.setAttribute('disabled', !isEnabled); + }); + } + + function updateToggleButtons(styleSummary) { + bulletedList.set("checked", styleSummary.isBulletedList, false); + numberedList.set("checked", styleSummary.isNumberedList, false); + + } + + this.onToolDone = function () { + }; + + this.setEditorSession = function (session) { + if (editorSession) { + listController.unsubscribe(gui.ListController.listStylingChanged, updateToggleButtons); + listController.unsubscribe(gui.ListController.enabledChanged, enableToggleButtons); + } + + editorSession = session; + + if (editorSession) { + listController = editorSession.sessionController.getListController(); + listController.subscribe(gui.ListController.listStylingChanged, updateToggleButtons); + listController.subscribe(gui.ListController.enabledChanged, enableToggleButtons); + enableToggleButtons(listController.isEnabled()); + } else { + enableToggleButtons(false); + } + }; + + callback(widget); + }; + + return ToggleLists; + } +); \ No newline at end of file diff --git a/programs/editor/wodocollabtexteditor.js b/programs/editor/wodocollabtexteditor.js index befead563..b8dd16073 100644 --- a/programs/editor/wodocollabtexteditor.js +++ b/programs/editor/wodocollabtexteditor.js @@ -177,6 +177,7 @@ var Wodo = Wodo || (function () { paragraphStyleEditingEnabled = isEnabled(editorOptions.paragraphStyleEditingEnabled), imageEditingEnabled = isEnabled(editorOptions.imageEditingEnabled, true), hyperlinkEditingEnabled = isEnabled(editorOptions.hyperlinkEditingEnabled, true), + listEditingEnabled = isEnabled(editorOptions.listEditingEnabled, true), reviewModeEnabled = isEnabled(editorOptions.reviewModeEnabled, true), annotationsEnabled = reviewModeEnabled || isEnabled(editorOptions.annotationsEnabled, true), undoRedoEnabled = false, // no proper mechanism yet for collab @@ -224,6 +225,7 @@ var Wodo = Wodo || (function () { paragraphStyleEditingEnabled: paragraphStyleEditingEnabled, imageEditingEnabled: imageEditingEnabled, hyperlinkEditingEnabled: hyperlinkEditingEnabled, + listEditingEnabled: listEditingEnabled, annotationsEnabled: annotationsEnabled, zoomingEnabled: zoomingEnabled, reviewModeEnabled: reviewModeEnabled @@ -567,6 +569,7 @@ var Wodo = Wodo || (function () { paragraphStyleEditingEnabled: paragraphStyleEditingEnabled, imageInsertingEnabled: imageEditingEnabled, hyperlinkEditingEnabled: hyperlinkEditingEnabled, + listEditingEnabled: listEditingEnabled, annotationsEnabled: annotationsEnabled, undoRedoEnabled: undoRedoEnabled, zoomingEnabled: zoomingEnabled diff --git a/programs/editor/wodotexteditor.js b/programs/editor/wodotexteditor.js index d5ac88715..dc9ec78f2 100644 --- a/programs/editor/wodotexteditor.js +++ b/programs/editor/wodotexteditor.js @@ -288,6 +288,7 @@ var Wodo = Wodo || (function () { paragraphStyleEditingEnabled = isEnabled(editorOptions.paragraphStyleEditingEnabled), imageEditingEnabled = isEnabled(editorOptions.imageEditingEnabled), hyperlinkEditingEnabled = isEnabled(editorOptions.hyperlinkEditingEnabled), + listEditingEnabled = isEnabled(editorOptions.listEditingEnabled), reviewModeEnabled = Boolean(editorOptions.reviewModeEnabled), // needs to be explicitly enabled annotationsEnabled = reviewModeEnabled || isEnabled(editorOptions.annotationsEnabled), undoRedoEnabled = isEnabled(editorOptions.undoRedoEnabled), @@ -331,6 +332,7 @@ var Wodo = Wodo || (function () { paragraphStyleEditingEnabled: paragraphStyleEditingEnabled, imageEditingEnabled: imageEditingEnabled, hyperlinkEditingEnabled: hyperlinkEditingEnabled, + listEditingEnabled: listEditingEnabled, annotationsEnabled: annotationsEnabled, zoomingEnabled: zoomingEnabled, reviewModeEnabled: reviewModeEnabled @@ -661,6 +663,7 @@ var Wodo = Wodo || (function () { paragraphStyleEditingEnabled: paragraphStyleEditingEnabled, imageInsertingEnabled: imageEditingEnabled, hyperlinkEditingEnabled: hyperlinkEditingEnabled, + listEditingEnabled: listEditingEnabled, annotationsEnabled: annotationsEnabled, undoRedoEnabled: undoRedoEnabled, zoomingEnabled: zoomingEnabled, diff --git a/webodf/lib/gui/DefaultStyles.js b/webodf/lib/gui/DefaultStyles.js new file mode 100644 index 000000000..957b9eb0e --- /dev/null +++ b/webodf/lib/gui/DefaultStyles.js @@ -0,0 +1,370 @@ +/** + * Copyright (C) 2010-2014 KO GmbH + * + * @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 . + * @licend + * + * @source: http://www.webodf.org/ + * @source: https://github.com/kogmbh/WebODF/ + */ + +/*global gui */ + +/** + * This file contains the default styles for numbered and bulleted lists created by WebODF + * It is used by the list controller to create the corresponding text:list-style nodes in + * the document. The list controller decides which of these default styles to use based on user input. + * Both of these default styles are based off the default numbered and bulleted list styles provided + * by LibreOffice + */ + +/** + * This is the default style for numbered lists created by WebODF. + * This has been modified from the LibreOffice style by enabling multi-level list numbering + * by adding the text:display-level attribute to each styleProperties object. + * @const + * @type {!ops.OpAddListStyle.ListStyle} + */ +gui.DefaultNumberedListStyle = [ + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "1", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "1.27cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "2", + "text:display-levels": "2", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "1.905cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "3", + "text:display-levels": "3", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "2.54cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "4", + "text:display-levels": "4", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "3.175cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "5", + "text:display-levels": "5", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "3.81cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "6", + "text:display-levels": "6", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "4.445cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "7", + "text:display-levels": "7", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "5.08cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "8", + "text:display-levels": "8", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "5.715cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "9", + "text:display-levels": "9", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "6.35cm" + } + } + } + }, + { + styleType: "text:list-level-style-number", + styleProperties: { + "text:level": "10", + "text:display-levels": "10", + "style:num-format": "1", + "style:num-suffix": ".", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "6.985cm" + } + } + } + } +]; + +/** + * This is the default style for bulleted lists created by WebODF. + * @const + * @type {!ops.OpAddListStyle.ListStyle} + */ +gui.DefaultBulletedListStyle = [ + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "1", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "1.27cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "2", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "1.905cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "3", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "2.54cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "4", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "3.175cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "5", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "3.81cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "6", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "4.445cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "7", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "5.08cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "8", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "5.715cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "9", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "6.35cm" + } + } + } + }, + { + styleType: "text:list-level-style-bullet", + styleProperties: { + "text:level": "10", + "text:bullet-char": "•", + "style:list-level-properties": { + "text:list-level-position-and-space-mode": "label-alignment", + "style:list-level-label-alignment": { + "text:label-followed-by": "space", + "fo:text-indent": "-0.635cm", + "fo:margin-left": "6.985cm" + } + } + } + } +]; \ No newline at end of file diff --git a/webodf/lib/gui/ListController.js b/webodf/lib/gui/ListController.js new file mode 100644 index 000000000..4cbb4dc46 --- /dev/null +++ b/webodf/lib/gui/ListController.js @@ -0,0 +1,481 @@ +/** + * Copyright (C) 2010-2014 KO GmbH + * + * @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 . + * @licend + * + * @source: http://www.webodf.org/ + * @source: https://github.com/kogmbh/WebODF/ + */ + +/*global core, ops, gui, odf, NodeFilter, runtime*/ + +/** + * @implements {core.Destroyable} + * @param {!ops.Session} session + * @param {!gui.SessionConstraints} sessionConstraints + * @param {!gui.SessionContext} sessionContext + * @param {!string} inputMemberId + * @constructor + */ +gui.ListController = function ListController(session, sessionConstraints, sessionContext, inputMemberId) { + "use strict"; + var odtDocument = session.getOdtDocument(), + odfUtils = odf.OdfUtils, + domUtils = core.DomUtils, + eventNotifier = new core.EventNotifier([ + gui.ListController.listStylingChanged, + gui.ListController.enabledChanged + ]), + /**@type{!gui.ListController.SelectionInfo}*/ + lastSignalledSelectionInfo, + /**@type{!core.LazyProperty.}*/ + cachedSelectionInfo, + /**@const*/ + NEXT = core.StepDirection.NEXT, + /**@const*/ + DEFAULT_NUMBERING_STYLE = "WebODF-Numbering", + /**@const*/ + DEFAULT_BULLETED_STYLE = "WebODF-Bulleted"; + + /** + * @param {!ops.OdtCursor|!string} cursorOrId + * @return {undefined} + */ + function onCursorEvent(cursorOrId) { + var cursorMemberId = (typeof cursorOrId === "string") + ? cursorOrId : cursorOrId.getMemberId(); + + if (cursorMemberId === inputMemberId) { + cachedSelectionInfo.reset(); + } + } + + /** + * @return {undefined} + */ + function onParagraphStyleModified() { + // this is reset on paragraph style change due to paragraph styles possibly linking to list styles + // through the use of the style:list-style-name attribute + // http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#attribute-style_list-style-name + cachedSelectionInfo.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) { + cachedSelectionInfo.reset(); + } + } + + /** + * @return {!gui.ListController.SelectionInfo} + */ + function getSelectionInfo() { + var cursor = odtDocument.getCursor(inputMemberId), + cursorNode = cursor && cursor.getNode(), + styleSummary = new gui.ListStyleSummary(cursorNode, odtDocument.getRootNode(), odtDocument.getFormatting()), + isEnabled = true; + + if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) { + isEnabled = sessionContext.isLocalCursorWithinOwnAnnotation(); + } + + return new gui.ListController.SelectionInfo(isEnabled, styleSummary); + } + + /** + * @return {undefined} + */ + function emitSelectionChanges() { + var hasStyleChanged = true, + hasEnabledChanged = true, + newSelectionInfo = cachedSelectionInfo.value(), + lastStyleSummary, + newStyleSummary; + + if (lastSignalledSelectionInfo) { + lastStyleSummary = lastSignalledSelectionInfo.styleSummary; + newStyleSummary = newSelectionInfo.styleSummary; + + hasStyleChanged = lastStyleSummary.isNumberedList !== newStyleSummary.isNumberedList || + lastStyleSummary.isBulletedList !== newStyleSummary.isBulletedList; + + hasEnabledChanged = lastSignalledSelectionInfo.isEnabled !== newSelectionInfo.isEnabled; + } + + lastSignalledSelectionInfo = newSelectionInfo; + + if (hasStyleChanged) { + eventNotifier.emit(gui.ListController.listStylingChanged, lastSignalledSelectionInfo.styleSummary); + } + + if (hasEnabledChanged) { + eventNotifier.emit(gui.ListController.enabledChanged, lastSignalledSelectionInfo.isEnabled); + } + } + + /** + * @return {undefined} + */ + function forceSelectionInfoRefresh() { + cachedSelectionInfo.reset(); + emitSelectionChanges(); + } + + /** + * Find all top level text:list elements in the given range. + * This includes any elements that contain the start or end containers of the range. + * @param {!Range} range + * @return {!Array.} + */ + function getTopLevelListElementsInRange(range) { + var elements, + topLevelList, + rootNode = odtDocument.getRootNode(); + + /** + * @param {!Node} node + * @return {!number} + */ + function isListOrListItem(node) { + var result = NodeFilter.FILTER_REJECT; + if (odfUtils.isListElement(node) && !odfUtils.isListItemOrListHeaderElement(node.parentNode)) { + result = NodeFilter.FILTER_ACCEPT; + } else if (odfUtils.isTextContentContainingNode(node) || odfUtils.isGroupingElement(node)) { + result = NodeFilter.FILTER_SKIP; + } + return result; + } + + // ignore the list element if it is nested within another list + elements = domUtils.getNodesInRange(range, isListOrListItem, NodeFilter.SHOW_ELEMENT); + + // add any top level lists that contain the start or end containers of the range + // check in the elements collection for duplicates in case these top level lists intersected the specified range + topLevelList = odfUtils.getTopLevelListElement(/**@type{!Node}*/(range.startContainer), rootNode); + if (topLevelList && topLevelList !== elements[0]) { + elements.unshift(topLevelList); + } + + topLevelList = odfUtils.getTopLevelListElement(/**@type{!Node}*/(range.endContainer), rootNode); + if (topLevelList && topLevelList !== elements[elements.length - 1]) { + elements.push(topLevelList); + } + + return elements; + } + + /** + * @param {!Element} initialParagraph + * @return {!{startParagraph: !Element, endParagraph: !Element}} + */ + function createParagraphGroup(initialParagraph) { + return { + startParagraph: initialParagraph, + endParagraph: initialParagraph + }; + } + + /** + * @param {!string} styleName + * @return {!ops.OpAddListStyle} + */ + function createDefaultListStyleOp(styleName) { + var op = new ops.OpAddListStyle(), + defaultListStyle; + + if (styleName === DEFAULT_NUMBERING_STYLE) { + defaultListStyle = gui.DefaultNumberedListStyle; + } else { + defaultListStyle = gui.DefaultBulletedListStyle; + } + + op.init({ + memberid: inputMemberId, + styleName: styleName, + isAutomaticStyle: true, + listStyle: defaultListStyle + }); + + return op; + } + + /** + * Takes all the paragraph elements in the current selection and breaks + * them into add list operations based on their common ancestors. Paragraph elements + * with the same common ancestor will be grouped into the same operation + * @param {!string=} styleName + * @return {!Array.} + */ + function determineOpsForAddingLists(styleName) { + var paragraphElements, + /**@type{!Array.}*/ + paragraphGroups = [], + paragraphParent, + commonAncestor, + i; + + paragraphElements = odfUtils.getParagraphElements(odtDocument.getCursor(inputMemberId).getSelectedRange()); + + for (i = 0; i < paragraphElements.length; i += 1) { + paragraphParent = paragraphElements[i].parentNode; + + //TODO: handle selections that intersect with existing lists + // This also needs to handle converting a list between numbering or bullets which MUST preserve the list structure + if (odfUtils.isListItemOrListHeaderElement(paragraphParent)) { + runtime.log("DEBUG: Current selection intersects with an existing list which is not supported at this time"); + paragraphGroups.length = 0; + break; + } + + if (paragraphParent === commonAncestor) { + // if the current paragraph has the same common ancestor as the current group of paragraphs + // then the paragraph group gets extended to include the current paragraph + paragraphGroups[paragraphGroups.length - 1].endParagraph = paragraphElements[i]; + } else { + // if the ancestor of this paragraph does not match then begin a new group of paragraphs + commonAncestor = paragraphParent; + paragraphGroups.push(createParagraphGroup(paragraphElements[i])); + } + } + + // each paragraph group becomes one add list operation + return paragraphGroups.map(function (group) { + // take the first step of the start and end paragraph of each group and + // pass them in as the coordinates for the add list operation + var newOp = new ops.OpAddList(); + newOp.init({ + memberid: inputMemberId, + startParagraphPosition: odtDocument.convertDomPointToCursorStep(group.startParagraph, 0, NEXT), + endParagraphPosition: odtDocument.convertDomPointToCursorStep(group.endParagraph, 0, NEXT), + styleName: styleName + }); + return newOp; + }); + } + + /** + * Finds all the lists to be removed in the current selection and creates an operation for each + * top level list element found + * @return {!Array.} + */ + function determineOpsForRemovingLists() { + var topLevelListElements, + stepIterator = odtDocument.createStepIterator( + odtDocument.getRootNode(), + 0, + [odtDocument.getPositionFilter()], + odtDocument.getRootNode()); + + topLevelListElements = getTopLevelListElementsInRange(odtDocument.getCursor(inputMemberId).getSelectedRange()); + + return topLevelListElements.map(function (listElement) { + var newOp = new ops.OpRemoveList(); + + stepIterator.setPosition(listElement, 0); + runtime.assert(stepIterator.roundToNextStep(), "Top level list element contains no steps"); + + newOp.init({ + memberid: inputMemberId, + firstParagraphPosition: odtDocument.convertDomPointToCursorStep(stepIterator.container(), stepIterator.offset()) + }); + return newOp; + }); + } + + /** + * @param {function():!Array.} executeFunc + * @return {!boolean} + */ + function executeListOperations(executeFunc) { + var newOps; + + if (!cachedSelectionInfo.value().isEnabled) { + return false; + } + + newOps = executeFunc(); + + if (newOps.length > 0) { + session.enqueue(newOps); + return true; + } + + return false; + } + + /** + * @return {!boolean} + */ + this.isEnabled = function () { + return cachedSelectionInfo.value().isEnabled; + }; + + /** + * @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 {!string=} styleName + * @return {!boolean} + */ + function makeList(styleName) { + var /**@type{!boolean}*/ + isExistingStyle, + /**@type{!boolean}*/ + isDefaultStyle; + + // check if the style name passed in exists in the document or is a WebODF default numbered or bulleted style. + // If no style name is passed in then the created list will have a style applied as described here: + // http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1419242_253892949 + if (styleName) { + isExistingStyle = Boolean(odtDocument.getFormatting().getStyleElement(styleName, "list-style")); + isDefaultStyle = styleName === DEFAULT_NUMBERING_STYLE || styleName === DEFAULT_BULLETED_STYLE; + + // if the style doesn't exist in the document and isn't a WebODF default style then we can't continue + if (!isExistingStyle && !isDefaultStyle) { + runtime.log("DEBUG: Could not create a list with the style name: " + styleName + " as it does not exist in the document"); + return false; + } + } + + return executeListOperations(function () { + var newOps = determineOpsForAddingLists(styleName); + + // this will only create an add list style op for WebODF default styles and only when they don't exist already + if (newOps.length > 0 && isDefaultStyle && !isExistingStyle) { + newOps.unshift(createDefaultListStyleOp(/**@type{!string}*/(styleName))); + } + return newOps; + }); + } + + /** + * @return {!boolean} + */ + function removeList() { + return executeListOperations(determineOpsForRemovingLists); + } + + this.removeList = removeList; + + /** + * @param {!boolean} checked + * @return {!boolean} + */ + this.setNumberedList = function (checked) { + if (checked) { + return makeList(DEFAULT_NUMBERING_STYLE); + } + return removeList(); + + }; + + /** + * @param {!boolean} checked + * @return {!boolean} + */ + this.setBulletedList = function (checked) { + if (checked) { + return makeList(DEFAULT_BULLETED_STYLE); + } + return removeList(); + }; + + /** + * @param {!function(!Error=)} callback + * @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.signalParagraphChanged, onParagraphChanged); + odtDocument.unsubscribe(ops.OdtDocument.signalParagraphStyleModified, onParagraphStyleModified); + odtDocument.unsubscribe(ops.OdtDocument.signalProcessingBatchEnd, emitSelectionChanges); + sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, forceSelectionInfoRefresh); + callback(); + }; + + /** + * @return {undefined} + */ + function init() { + odtDocument.subscribe(ops.Document.signalCursorAdded, onCursorEvent); + odtDocument.subscribe(ops.Document.signalCursorRemoved, onCursorEvent); + odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent); + odtDocument.subscribe(ops.OdtDocument.signalParagraphChanged, onParagraphChanged); + odtDocument.subscribe(ops.OdtDocument.signalParagraphStyleModified, onParagraphStyleModified); + odtDocument.subscribe(ops.OdtDocument.signalProcessingBatchEnd, emitSelectionChanges); + sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, forceSelectionInfoRefresh); + + cachedSelectionInfo = new core.LazyProperty(getSelectionInfo); + } + + init(); +}; + +/**@const*/ +gui.ListController.listStylingChanged = "listStyling/changed"; + +/**@const*/ +gui.ListController.enabledChanged = "enabled/changed"; + +/** + * @param {!boolean} isEnabled + * @param {!gui.ListStyleSummary} styleSummary + * @constructor + * @struct + */ +gui.ListController.SelectionInfo = function (isEnabled, styleSummary) { + "use strict"; + + /** + * Whether the controller is enabled based on the selection + * @type {!boolean} + */ + this.isEnabled = isEnabled; + + /** + * Summary of list style information for the selection + * @type {!gui.ListStyleSummary} + */ + this.styleSummary = styleSummary; +}; + + diff --git a/webodf/lib/gui/ListStyleSummary.js b/webodf/lib/gui/ListStyleSummary.js new file mode 100644 index 000000000..f2a98dff6 --- /dev/null +++ b/webodf/lib/gui/ListStyleSummary.js @@ -0,0 +1,136 @@ +/** + * Copyright (C) 2010-2014 KO GmbH + * + * @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 . + * @licend + * + * @source: http://www.webodf.org/ + * @source: https://github.com/kogmbh/WebODF/ + */ + +/*global runtime, odf, gui*/ + +/** + * This finds a text:list element containing the given node and then + * determines the type of list based on its list style + * + * @param {?Element} node + * @param {!Element} rootNode + * @param {!odf.Formatting} formatting + * @constructor + */ +gui.ListStyleSummary = function ListStyleSummary(node, rootNode, formatting) { + "use strict"; + + var self = this, + odfUtils = odf.OdfUtils; + + /** + * @type {!boolean} + */ + this.isNumberedList = false; + + /** + * @type {!boolean} + */ + this.isBulletedList = false; + + /** + * @param {!Element} node + * @return {?Element} + */ + function getListStyleElementAtNode(node) { + var appliedStyles, + filteredStyles, + listStyleName, + listStyleElement = null; + + // find the styles applied on this node and search for any list styles + appliedStyles = formatting.getAppliedStyles([node]); + if (appliedStyles[0]) { + filteredStyles = appliedStyles[0].orderedStyles.filter(function (style) { + return style.family === "list-style"; + }); + + listStyleName = filteredStyles[0] && filteredStyles[0].name; + } + + if(listStyleName) { + listStyleElement = formatting.getStyleElement(listStyleName, "list-style"); + } + + return listStyleElement; + } + + /** + * @param {!Element} node + * @return {!number} + */ + function getListLevelAtNode(node) { + var listLevel = 0, + currentNode = node; + + // find the text:list element that contains the given node + // and then find the highest text:list element in this DOM hierarchy + while (currentNode) { + if (odfUtils.isListElement(currentNode)) { + listLevel += 1; + } + + if (currentNode === rootNode) { + break; + } + currentNode = currentNode.parentNode; + } + + return listLevel; + } + + /** + * @return {undefined} + */ + function init() { + var listLevelAtNode, + currentListStyleElement, + textLevelAttribute; + + if(!node) { + return; + } + + // find the depth of the list at the node and then find the matching level + // in the list style applied to that node to determine type of list + listLevelAtNode = getListLevelAtNode(node); + currentListStyleElement = getListStyleElementAtNode(node); + currentListStyleElement = currentListStyleElement && currentListStyleElement.firstElementChild; + + while (currentListStyleElement) { + textLevelAttribute = currentListStyleElement.getAttributeNS(odf.Namespaces.textns, "level"); + + if (textLevelAttribute) { + textLevelAttribute = parseInt(textLevelAttribute, 10); + if (textLevelAttribute === listLevelAtNode) { + self.isBulletedList = currentListStyleElement.localName === "list-level-style-bullet"; + self.isNumberedList = currentListStyleElement.localName === "list-level-style-number"; + } + } + currentListStyleElement = currentListStyleElement.nextElementSibling; + } + } + + init(); +}; \ No newline at end of file diff --git a/webodf/lib/gui/SessionController.js b/webodf/lib/gui/SessionController.js index 31134585f..e84b6f696 100644 --- a/webodf/lib/gui/SessionController.js +++ b/webodf/lib/gui/SessionController.js @@ -90,6 +90,7 @@ gui.SessionControllerOptions = function () { createParagraphStyleOps = /**@type {function (!number):!Array.}*/ (directFormattingController.createParagraphStyleOps), textController = new gui.TextController(session, sessionConstraints, sessionContext, inputMemberId, createCursorStyleOp, createParagraphStyleOps), imageController = new gui.ImageController(session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator), + listController = new gui.ListController(session, sessionConstraints, sessionContext, inputMemberId), imageSelector = new gui.ImageSelector(odtDocument.getOdfCanvas()), shadowCursorIterator = odtDocument.createPositionIterator(odtDocument.getRootNode()), /**@type{!core.ScheduledTask}*/ @@ -1026,6 +1027,13 @@ gui.SessionControllerOptions = function () { return directFormattingController; }; + /** + * @return {!gui.ListController} + */ + this.getListController = function () { + return listController; + }; + /** * @return {!gui.HyperlinkClickHandler} */ @@ -1123,6 +1131,7 @@ gui.SessionControllerOptions = function () { metadataController.destroy, selectionController.destroy, textController.destroy, + listController.destroy, destroy ]; diff --git a/webodf/lib/manifest.json b/webodf/lib/manifest.json index 88d136a25..d86b039b2 100644 --- a/webodf/lib/manifest.json +++ b/webodf/lib/manifest.json @@ -97,6 +97,9 @@ ], "gui.CommonConstraints": [ ], + "gui.DefaultStyles": [ + "ops.OpAddListStyle" + ], "gui.DirectFormattingController": [ "core.LazyProperty", "gui.CommonConstraints", @@ -153,6 +156,17 @@ "core.typedefs", "gui.VisualStepScanner" ], + "gui.ListController": [ + "core.LazyProperty", + "gui.CommonConstraints", + "gui.DefaultStyles", + "gui.ListStyleSummary", + "gui.SessionConstraints", + "gui.SessionContext" + ], + "gui.ListStyleSummary": [ + "odf.Formatting" + ], "gui.MetadataController": [ "ops.Session" ], @@ -204,6 +218,7 @@ "gui.ImageController", "gui.ImageSelector", "gui.InputMethodEditor", + "gui.ListController", "gui.MetadataController", "gui.PasteController", "gui.SelectionController", @@ -404,6 +419,12 @@ "ops.OpAddCursor": [ "ops.OdtDocument" ], + "ops.OpAddList": [ + "ops.OdtDocument" + ], + "ops.OpAddListStyle": [ + "ops.OdtDocument" + ], "ops.OpAddMember": [ "ops.OdtDocument" ], @@ -445,6 +466,9 @@ "ops.OpRemoveHyperlink": [ "ops.OdtDocument" ], + "ops.OpRemoveList": [ + "ops.OdtDocument" + ], "ops.OpRemoveMember": [ "ops.OdtDocument" ], @@ -479,6 +503,8 @@ "ops.OperationFactory": [ "ops.OpAddAnnotation", "ops.OpAddCursor", + "ops.OpAddList", + "ops.OpAddListStyle", "ops.OpAddMember", "ops.OpAddStyle", "ops.OpApplyDirectStyling", @@ -492,6 +518,7 @@ "ops.OpRemoveBlob", "ops.OpRemoveCursor", "ops.OpRemoveHyperlink", + "ops.OpRemoveList", "ops.OpRemoveMember", "ops.OpRemoveStyle", "ops.OpRemoveText", @@ -506,12 +533,15 @@ "ops.OperationFactory" ], "ops.OperationTransformMatrix": [ + "ops.OpAddList", + "ops.OpAddListStyle", "ops.OpAddStyle", "ops.OpApplyDirectStyling", "ops.OpInsertText", "ops.OpMergeParagraph", "ops.OpMoveCursor", "ops.OpRemoveCursor", + "ops.OpRemoveList", "ops.OpRemoveStyle", "ops.OpRemoveText", "ops.OpSetParagraphStyle", diff --git a/webodf/lib/odf/OdfUtils.js b/webodf/lib/odf/OdfUtils.js index ba94567f9..b6827ad36 100644 --- a/webodf/lib/odf/OdfUtils.js +++ b/webodf/lib/odf/OdfUtils.js @@ -212,13 +212,25 @@ odf.OdfUtilsImpl = function OdfUtilsImpl() { }; /** - * Determine if the node is a text:list-item element. + * Determine if the node is a text:list element. * @param {?Node} e * @return {!boolean} */ - this.isListItem = function (e) { + function isListElement(e) { var name = e && e.localName; - return name === "list-item" && e.namespaceURI === textns; + return name === "list" && e.namespaceURI === textns; + } + + this.isListElement = isListElement; + + /** + * Determine if the node is a text:list-item or text:list-header element. + * @param {?Node} e + * @return {!boolean} + */ + this.isListItemOrListHeaderElement = function (e) { + var name = e && e.localName; + return (name === "list-item" || name === "list-header") && e.namespaceURI === textns; }; /** @@ -1054,6 +1066,30 @@ odf.OdfUtilsImpl = function OdfUtilsImpl() { return fontFamilyName; }; /*jslint regexp: false*/ + + /** + * Finds the top level text:list element for a given node + * @param {!Node} node + * @param {!Element} container Root container to stop searching at. + * @return {?Element} + */ + this.getTopLevelListElement = function(node, container) { + var currentNode = node, + listNode = null; + + while(currentNode) { + if(isListElement(currentNode)) { + listNode = /**@type{!Element}*/(currentNode); + } + + if(currentNode === container) { + break; + } + currentNode = currentNode.parentNode; + } + + return listNode; + }; }; /** diff --git a/webodf/lib/ops/OpAddList.js b/webodf/lib/ops/OpAddList.js new file mode 100644 index 000000000..c03966846 --- /dev/null +++ b/webodf/lib/ops/OpAddList.js @@ -0,0 +1,179 @@ +/** + * Copyright (C) 2010-2014 KO GmbH + * + * @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 . + * @licend + * + * @source: http://www.webodf.org/ + * @source: https://github.com/kogmbh/WebODF/ + */ + +/*global ops, runtime, odf, core */ + +/** + * + * @constructor + * @implements ops.Operation + */ +ops.OpAddList = function OpAddList() { + "use strict"; + + var memberid, + timestamp, + /**@type{!number}*/ + startParagraphPosition, + /**@type{!number}*/ + endParagraphPosition, + /**@type{string|undefined}*/ + styleName, + odfUtils = odf.OdfUtils, + domUtils = core.DomUtils, + /**@const*/ + textns = odf.Namespaces.textns; + + /** + * Ensure that the paragraph positions given as the range for this op are the first step + * in the paragraphs at those positions. This check is done to assist operational transforms for this OP. + * Also ensure that all paragraphs in the range supplied to the operation share the same parent. + * @param {!ops.OdtDocument} odtDocument + * @param {!Array.} paragraphs + * @param {!Range} range + * @return {undefined} + */ + function verifyParagraphPositions(odtDocument, paragraphs, range) { + var rootNode = odtDocument.getRootNode(), + stepIterator = odtDocument.createStepIterator(rootNode, + 0, + [odtDocument.getPositionFilter()], + rootNode), + sharedParentNode = paragraphs[0].parentNode; + + stepIterator.setPosition(/**@type{!Node}*/(range.startContainer), range.startOffset); + stepIterator.previousStep(); + runtime.assert(!domUtils.containsNode(paragraphs[0], stepIterator.container()), + "First paragraph position (" + startParagraphPosition + ") is not the first step in the paragraph"); + + stepIterator.setPosition(/**@type{!Node}*/(range.endContainer), range.endOffset); + stepIterator.previousStep(); + runtime.assert(!domUtils.containsNode(paragraphs[paragraphs.length - 1], stepIterator.container()), + "Last paragraph position (" + endParagraphPosition + ") is not the first step in the paragraph"); + + runtime.assert(paragraphs.every(function (paragraph) { + return paragraph.parentNode === sharedParentNode; + }), "All the paragraphs in the range do not have the same parent node"); + } + + /** + * @param {!ops.OpAddList.InitSpec} data + */ + this.init = function (data) { + memberid = data.memberid; + timestamp = data.timestamp; + startParagraphPosition = data.startParagraphPosition; + endParagraphPosition = data.endParagraphPosition; + styleName = data.styleName; + }; + + this.isEdit = true; + this.group = undefined; + + /** + * @return {!ops.OpAddList.Spec} + */ + this.spec = function () { + return { + optype: "AddList", + memberid: memberid, + timestamp: timestamp, + startParagraphPosition: startParagraphPosition, + endParagraphPosition: endParagraphPosition, + styleName: styleName + }; + }; + + /** + * @param {!ops.Document} document + */ + this.execute = function (document) { + var odtDocument = /**@type{ops.OdtDocument}*/(document), + ownerDocument = odtDocument.getDOMDocument(), + range = odtDocument.convertCursorToDomRange(startParagraphPosition, endParagraphPosition - startParagraphPosition), + paragraphsInRange = odfUtils.getParagraphElements(range), + insertionPointParagraph = paragraphsInRange[0], + /**@type{!Element}*/ + newListElement; + + // always want a forward range where the start of the range is less than the end of the range + // this is to make any operational transforms easier by avoiding having to check for backward ranges + runtime.assert(startParagraphPosition <= endParagraphPosition, + "First paragraph in range (" + startParagraphPosition + ") must be " + + "before last paragraph in range (" + endParagraphPosition + ")"); + + if (!insertionPointParagraph) { + return false; + } + + verifyParagraphPositions(odtDocument, paragraphsInRange, range); + + // create the new list element and insert it in the document before the first paragraph we are adding to the list + newListElement = ownerDocument.createElementNS(textns, "text:list"); + insertionPointParagraph.parentNode.insertBefore(newListElement, paragraphsInRange[0]); + + // wrap each paragraph in a list item element and add it to the list + paragraphsInRange.forEach(function (paragraphElement) { + var newListItemElement = ownerDocument.createElementNS(textns, "text:list-item"); + + newListItemElement.appendChild(paragraphElement); + newListElement.appendChild(newListItemElement); + }); + + if (styleName) { + newListElement.setAttributeNS(textns, "text:style-name", styleName); + } + + odtDocument.getOdfCanvas().refreshCSS(); + odtDocument.getOdfCanvas().rerenderAnnotations(); + paragraphsInRange.forEach(function (paragraphElement) { + // pretend the paragraphs affected have changed to force caret updates + odtDocument.emit(ops.OdtDocument.signalParagraphChanged, { + paragraphElement: paragraphElement, + timeStamp: timestamp, + memberId: memberid + }); + }); + return true; + }; +}; + +/**@typedef{{ + optype: !string, + memberid: !string, + timestamp: !number, + startParagraphPosition: !number, + endParagraphPosition: !number, + styleName: (!string|undefined) +}}*/ +ops.OpAddList.Spec; + +/**@typedef{{ + memberid: !string, + timestamp:(!number|undefined), + startParagraphPosition: !number, + endParagraphPosition: !number, + styleName: (!string|undefined) +}}*/ +ops.OpAddList.InitSpec; \ No newline at end of file diff --git a/webodf/lib/ops/OpAddListStyle.js b/webodf/lib/ops/OpAddListStyle.js new file mode 100644 index 000000000..80a5b88d6 --- /dev/null +++ b/webodf/lib/ops/OpAddListStyle.js @@ -0,0 +1,130 @@ +/** + * Copyright (C) 2010-2014 KO GmbH + * + * @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 . + * @licend + * + * @source: http://www.webodf.org/ + * @source: https://github.com/kogmbh/WebODF/ + */ + +/*global ops, runtime, odf, core*/ + +/** + * + * @constructor + * @implements ops.Operation + */ +ops.OpAddListStyle = function OpAddListStyle() { + "use strict"; + + var memberid, + timestamp, + isAutomaticStyle, + styleName, + /**@type{!ops.OpAddListStyle.ListStyle}*/ + listStyle, + /**@const*/ + textns = odf.Namespaces.textns, + /**@const*/ + stylens = odf.Namespaces.stylens; + + /** + * @param {!ops.OpAddListStyle.InitSpec} data + */ + this.init = function (data) { + memberid = data.memberid; + timestamp = data.timestamp; + isAutomaticStyle = data.isAutomaticStyle; + styleName = data.styleName; + listStyle = data.listStyle; + }; + + this.isEdit = true; + this.group = undefined; + + /** + * @return {!ops.OpAddListStyle.Spec} + */ + this.spec = function () { + return { + optype: "AddListStyle", + memberid: memberid, + timestamp: timestamp, + isAutomaticStyle: isAutomaticStyle, + styleName: styleName, + listStyle: listStyle + }; + }; + + /** + * @param {!ops.Document} document + */ + this.execute = function (document) { + var odtDocument = /**@type{!ops.OdtDocument}*/(document), + odfContainer = odtDocument.getOdfCanvas().odfContainer(), + ownerDocument = odtDocument.getDOMDocument(), + formatting = odtDocument.getFormatting(), + styleNode = ownerDocument.createElementNS(textns, "text:list-style"); + + if(!styleNode) { + return false; + } + + listStyle.forEach(function (listLevelStyle) { + var newListLevelNode = ownerDocument.createElementNS(textns, listLevelStyle.styleType); + formatting.updateStyle(newListLevelNode, listLevelStyle.styleProperties); + styleNode.appendChild(newListLevelNode); + }); + + styleNode.setAttributeNS(stylens, 'style:name', styleName); + + if (isAutomaticStyle) { + odfContainer.rootElement.automaticStyles.appendChild(styleNode); + } else { + odfContainer.rootElement.styles.appendChild(styleNode); + } + + odtDocument.getOdfCanvas().refreshCSS(); + if (!isAutomaticStyle) { + odtDocument.emit(ops.OdtDocument.signalCommonStyleCreated, {name: styleName, family: "list-style"}); + } + return true; + }; +}; + +/**@typedef{{ + optype: !string, + memberid: !string, + timestamp: !number, + isAutomaticStyle: !boolean, + styleName: !string, + listStyle: !ops.OpAddListStyle.ListStyle +}}*/ +ops.OpAddListStyle.Spec; + +/**@typedef{{ + memberid: !string, + timestamp:(!number|undefined), + isAutomaticStyle: !boolean, + styleName: !string, + listStyle: !ops.OpAddListStyle.ListStyle +}}*/ +ops.OpAddListStyle.InitSpec; + +/**@typedef{!Array.<{styleType: !string, styleProperties: !Object}>}*/ +ops.OpAddListStyle.ListStyle; \ No newline at end of file diff --git a/webodf/lib/ops/OpRemoveList.js b/webodf/lib/ops/OpRemoveList.js new file mode 100644 index 000000000..f34e3b6ef --- /dev/null +++ b/webodf/lib/ops/OpRemoveList.js @@ -0,0 +1,142 @@ +/** + * Copyright (C) 2010-2014 KO GmbH + * + * @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 . + * @licend + * + * @source: http://www.webodf.org/ + * @source: https://github.com/kogmbh/WebODF/ + */ + +/*global ops, runtime, odf, core*/ + +/** + * + * @constructor + * @implements ops.Operation + */ +ops.OpRemoveList = function OpRemoveList() { + "use strict"; + + var memberid, + timestamp, + /**@type{!number}*/ + firstParagraphPosition, + odfUtils = odf.OdfUtils, + domUtils = core.DomUtils; + + /** + * Ensure that the position supplied to the operation points to the first step in the list + * @param {!ops.OdtDocument} odtDocument + * @param {!Element} topLevelList + * @param {!Element} firstParagraph + * @param {!{node: !Node, offset: !number}} firstParagraphDomPosition + * @return {undefined} + */ + function verifyParagraphPositions(odtDocument, topLevelList, firstParagraph, firstParagraphDomPosition) { + var stepIterator = odtDocument.createStepIterator( + topLevelList, + 0, + [odtDocument.getPositionFilter()], + topLevelList); + + stepIterator.nextStep(); + runtime.assert(domUtils.containsNode(firstParagraph, stepIterator.container()), + "Paragraph at " + firstParagraphPosition + " is not the first paragraph in the list"); + stepIterator.setPosition(firstParagraphDomPosition.node, firstParagraphDomPosition.offset); + runtime.assert(!stepIterator.previousStep(), + "First paragraph position (" + firstParagraphPosition + ") is not the first step in the paragraph"); + } + + /** + * @param {!ops.OpRemoveList.InitSpec} data + */ + this.init = function (data) { + memberid = data.memberid; + timestamp = data.timestamp; + firstParagraphPosition = data.firstParagraphPosition; + }; + + this.isEdit = true; + this.group = undefined; + + /** + * @return {!ops.OpRemoveList.Spec} + */ + this.spec = function () { + return { + optype: "RemoveList", + memberid: memberid, + timestamp: timestamp, + firstParagraphPosition: firstParagraphPosition + }; + }; + + /** + * @param {!ops.Document} document + */ + this.execute = function (document) { + var odtDocument = /**@type{ops.OdtDocument}*/(document), + domPosition = odtDocument.convertCursorStepToDomPoint(firstParagraphPosition), + firstParagraph = /**@type{!Element}*/(odfUtils.getParagraphElement(domPosition.node, domPosition.offset)), + /**@type{!Array.}*/ + affectedParagraphs = [], + topLevelListElement; + + // if the paragraph is not within a list then we can't continue + runtime.assert(odfUtils.isListItemOrListHeaderElement(firstParagraph.parentNode), + "First paragraph at " + firstParagraphPosition + " is not within a list"); + topLevelListElement = /**@type{!Element}*/(odfUtils.getTopLevelListElement(firstParagraph, odtDocument.getRootNode())); + + verifyParagraphPositions(odtDocument, topLevelListElement, firstParagraph, domPosition); + + // remove all list structure and also keep track of affected paragraphs + domUtils.removeUnwantedNodes(topLevelListElement, function (node) { + if (odfUtils.isParagraph(node)) { + affectedParagraphs.push(node); + } + return odfUtils.isListElement(node) || odfUtils.isListItemOrListHeaderElement(node); + }); + + odtDocument.getOdfCanvas().rerenderAnnotations(); + + // pretend the paragraphs removed from the list have changed to force caret updates + affectedParagraphs.forEach(function (paragraph) { + odtDocument.emit(ops.OdtDocument.signalParagraphChanged, { + paragraphElement: paragraph, + timeStamp: timestamp, + memberId: memberid + }); + }); + return true; + }; +}; + +/**@typedef{{ + optype: !string, + memberid: !string, + timestamp: !number, + firstParagraphPosition: !number +}}*/ +ops.OpRemoveList.Spec; + +/**@typedef{{ + memberid: !string, + timestamp:(number|undefined), + firstParagraphPosition: !number +}}*/ +ops.OpRemoveList.InitSpec; \ No newline at end of file diff --git a/webodf/lib/ops/OpSplitParagraph.js b/webodf/lib/ops/OpSplitParagraph.js index d84f6167a..414e2cab9 100644 --- a/webodf/lib/ops/OpSplitParagraph.js +++ b/webodf/lib/ops/OpSplitParagraph.js @@ -89,7 +89,7 @@ ops.OpSplitParagraph = function OpSplitParagraph() { return false; } - if (odfUtils.isListItem(paragraphNode.parentNode)) { + if (odfUtils.isListItemOrListHeaderElement(paragraphNode.parentNode)) { targetNode = paragraphNode.parentNode; } else { targetNode = paragraphNode; @@ -154,7 +154,7 @@ ops.OpSplitParagraph = function OpSplitParagraph() { splitChildNode = splitNode; } - if (odfUtils.isListItem(splitChildNode)) { + if (odfUtils.isListItemOrListHeaderElement(splitChildNode)) { splitChildNode = splitChildNode.childNodes.item(0); } diff --git a/webodf/lib/ops/OperationFactory.js b/webodf/lib/ops/OperationFactory.js index af250a294..af2911de9 100644 --- a/webodf/lib/ops/OperationFactory.js +++ b/webodf/lib/ops/OperationFactory.js @@ -100,7 +100,10 @@ ops.OperationFactory = function OperationFactory() { RemoveAnnotation: construct(ops.OpRemoveAnnotation), UpdateMetadata: construct(ops.OpUpdateMetadata), ApplyHyperlink: construct(ops.OpApplyHyperlink), - RemoveHyperlink: construct(ops.OpRemoveHyperlink) + RemoveHyperlink: construct(ops.OpRemoveHyperlink), + AddList: construct(ops.OpAddList), + RemoveList: construct(ops.OpRemoveList), + AddListStyle: construct(ops.OpAddListStyle) }; } diff --git a/webodf/lib/ops/OperationTransformMatrix.js b/webodf/lib/ops/OperationTransformMatrix.js index c4a743ca6..9696e1aa0 100644 --- a/webodf/lib/ops/OperationTransformMatrix.js +++ b/webodf/lib/ops/OperationTransformMatrix.js @@ -30,6 +30,13 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "use strict"; + var /**@const*/ + INCLUSIVE = true, + /**@const*/ + EXCLUSIVE = false, + /**@type {!Object., opSpecsB:!Array.}>>}*/ + transformations; + /* Utility methods */ /** @@ -269,10 +276,187 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { return result; } + /** + * Checks whether the given position is within the range of the add list operation. + * This range check is always inclusive of the start paragraph position + * @param {!number} position + * @param {!ops.OpAddList.Spec} spec + * @param {!boolean} isInclusiveEndPosition Range check is inclusive of the end paragraph position + * @return {!boolean} + */ + function isWithinRange(position, spec, isInclusiveEndPosition) { + var withinEnd; + withinEnd = isInclusiveEndPosition ? position <= spec.endParagraphPosition : position < spec.endParagraphPosition; + + return position >= spec.startParagraphPosition && withinEnd; + } /* Transformation methods */ + /** + * @param {!ops.OpAddList.Spec} addListSpecA + * @param {!ops.OpAddList.Spec} addListSpecB + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformAddListAddList(addListSpecA, addListSpecB) { + var opSpecsA = [addListSpecA], + opSpecsB = [addListSpecB]; + + //TODO: consider style names. This can't be resolved currently as there is no op to set a style on a list after creation. + // same range so this becomes a no-op + if (addListSpecA.startParagraphPosition === addListSpecB.startParagraphPosition && + addListSpecA.endParagraphPosition === addListSpecB.endParagraphPosition) { + opSpecsA = []; + opSpecsB = []; + } + + // ranges intersect + if (isWithinRange(addListSpecA.startParagraphPosition, addListSpecB, INCLUSIVE) || + isWithinRange(addListSpecA.endParagraphPosition, addListSpecB, INCLUSIVE)) { + //TODO: do something useful here once we get list merge ops and solve the conflict by merging the lists + return null; + } + + return { + opSpecsA: opSpecsA, + opSpecsB: opSpecsB + }; + } + + /** + * @param {!ops.OpAddList.Spec} addListSpec + * @param {!ops.OpInsertText.Spec} insertTextSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformAddListInsertText(addListSpec, insertTextSpec) { + // insert text is before the add list range so adjust the start position and end position + if (insertTextSpec.position < addListSpec.startParagraphPosition) { + addListSpec.startParagraphPosition += insertTextSpec.text.length; + addListSpec.endParagraphPosition += insertTextSpec.text.length; + } else if (isWithinRange(insertTextSpec.position, addListSpec, EXCLUSIVE)) { + // otherwise insert text is within the add list range so only shift the end of the range + addListSpec.endParagraphPosition += insertTextSpec.text.length; + } + + return { + opSpecsA: [addListSpec], + opSpecsB: [insertTextSpec] + }; + } + + /** + * @param {!ops.OpAddList.Spec} addListSpec + * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformAddListMergeParagraph(addListSpec, mergeParagraphSpec) { + if (mergeParagraphSpec.sourceStartPosition === addListSpec.startParagraphPosition) { + // TODO: handle this properly once we have Merge/Split list ops as merge paragraph pulls the paragraph out of the list + return null; + } + + if (mergeParagraphSpec.sourceStartPosition < addListSpec.startParagraphPosition) { + // merge op source paragraph is before the list range so adjust the start and the end + addListSpec.startParagraphPosition -= 1; + addListSpec.endParagraphPosition -= 1; + } else if (isWithinRange(mergeParagraphSpec.sourceStartPosition, addListSpec, EXCLUSIVE)) { + // merge op is fully contained in list range so just shift the end of the list range + addListSpec.endParagraphPosition -= 1; + } else if (mergeParagraphSpec.sourceStartPosition === addListSpec.endParagraphPosition) { + // merge op source paragraph is the same as the end of the list range so shift + // the end of the list range up to the merge op destination paragraph + addListSpec.endParagraphPosition = mergeParagraphSpec.destinationStartPosition; + } + + return { + opSpecsA: [addListSpec], + opSpecsB: [mergeParagraphSpec] + }; + } + + /** + * @param {!ops.OpAddList.Spec} addListSpec + * @param {!ops.OpRemoveList.Spec} removeListSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformAddListRemoveList(addListSpec, removeListSpec) { + // This should never happen as a client must ensure it does not add a list where one already exists + // and remove a list that does not exist in the document. + // This does not detect an overlap where the range of the add list operation occurs after the start position of the + // removed list as we don't know the end position of the removed list. + if (isWithinRange(removeListSpec.firstParagraphPosition, addListSpec, INCLUSIVE)) { + return null; + } + + return { + opSpecsA: [addListSpec], + opSpecsB: [removeListSpec] + }; + } + + /** + * @param {!ops.OpAddList.Spec} addListSpec + * @param {!ops.OpRemoveText.Spec} removeTextSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformAddListRemoveText(addListSpec, removeTextSpec) { + // remove text is before the add list range so adjust the start position and end position + if (removeTextSpec.position < addListSpec.startParagraphPosition) { + addListSpec.startParagraphPosition -= removeTextSpec.length; + addListSpec.endParagraphPosition -= removeTextSpec.length; + } else if (isWithinRange(removeTextSpec.position, addListSpec, EXCLUSIVE)) { + // otherwise remove text is within the add list range so only shift the end of the range + addListSpec.endParagraphPosition -= removeTextSpec.length; + } + + return { + opSpecsA: [addListSpec], + opSpecsB: [removeTextSpec] + }; + } + + /** + * @param {!ops.OpAddList.Spec} addListSpec + * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformAddListSplitParagraph(addListSpec, splitParagraphSpec) { + // split op source paragraph is before the list range so adjust the start and the end + if (splitParagraphSpec.sourceParagraphPosition < addListSpec.startParagraphPosition) { + addListSpec.startParagraphPosition += 1; + addListSpec.endParagraphPosition += 1; + } else if (isWithinRange(splitParagraphSpec.sourceParagraphPosition, addListSpec, EXCLUSIVE)) { + // split op is fully contained in list range so just shift the end of the list range + addListSpec.endParagraphPosition += 1; + } else if (splitParagraphSpec.sourceParagraphPosition === addListSpec.endParagraphPosition) { + // split op source paragraph is the same as the end of the list range so shift the range + // down to the split position which is the new end paragraph + addListSpec.endParagraphPosition = splitParagraphSpec.position + 1; + } + + return { + opSpecsA: [addListSpec], + opSpecsB: [splitParagraphSpec] + }; + } + + /** + * @param {!ops.OpAddListStyle.Spec} addListStyleSpecA + * @param {!ops.OpAddListStyle.Spec} addListStyleSpecB + */ + function transformAddListStyleAddListStyle(addListStyleSpecA, addListStyleSpecB) { + //TODO: handle list style conflicts + if(addListStyleSpecA.styleName === addListStyleSpecB.styleName) { + return null; + } + + return { + opSpecsA: [addListStyleSpecA], + opSpecsB: [addListStyleSpecB] + }; + } + /** * @param {!ops.OpAddStyle.Spec} addStyleSpec * @param {!ops.OpRemoveStyle.Spec} removeStyleSpec @@ -603,6 +787,23 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { }; } + /** + * @param {!ops.OpInsertText.Spec} insertTextSpec + * @param {!ops.OpRemoveList.Spec} removeListSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformInsertTextRemoveList(insertTextSpec, removeListSpec) { + // adjust list start position only if text is inserted before the list start position + if (insertTextSpec.position < removeListSpec.firstParagraphPosition) { + removeListSpec.firstParagraphPosition += insertTextSpec.text.length; + } + + return { + opSpecsA: [insertTextSpec], + opSpecsB: [removeListSpec] + }; + } + /** * @param {!ops.OpInsertText.Spec} insertTextSpec * @param {!ops.OpRemoveText.Spec} removeTextSpec @@ -795,6 +996,28 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { }; } + /** + * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec + * @param {!ops.OpRemoveList.Spec} removeListSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformMergeParagraphRemoveList(mergeParagraphSpec, removeListSpec) { + // adjust list start position only if the paragraph being merged is before the list start position + if (mergeParagraphSpec.sourceStartPosition < removeListSpec.firstParagraphPosition) { + removeListSpec.firstParagraphPosition -= 1; + } else if (mergeParagraphSpec.sourceStartPosition === removeListSpec.firstParagraphPosition) { + // TODO: unable to handle this currently as merge paragraph pulls paragraphs out of the list + // One possible solution would be to add paragraph lengths to the merge paragraph spec + // to allow this transform to know how many steps to move the anchor of the remove list op + return null; + } + + return { + opSpecsA: [mergeParagraphSpec], + opSpecsB: [removeListSpec] + }; + } + /** * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec * @param {!ops.OpRemoveText.Spec} removeTextSpec @@ -910,6 +1133,60 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { }; } + /** + * @param {!ops.OpRemoveList.Spec} removeListSpecA + * @param {!ops.OpRemoveList.Spec} removeListSpecB + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformRemoveListRemoveList(removeListSpecA, removeListSpecB) { + var opSpecsA = [removeListSpecA], + opSpecsB = [removeListSpecB]; + + if (removeListSpecA.firstParagraphPosition === removeListSpecB.firstParagraphPosition) { + opSpecsA = []; + opSpecsB = []; + } + + return { + opSpecsA: opSpecsA, + opSpecsB: opSpecsB + }; + } + + /** + * @param {!ops.OpRemoveList.Spec} removeListSpec + * @param {!ops.OpRemoveText.Spec} removeTextSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformRemoveListRemoveText(removeListSpec, removeTextSpec) { + // adjust list start position only if text is removed before the list start position + if (removeTextSpec.position < removeListSpec.firstParagraphPosition) { + removeListSpec.firstParagraphPosition -= removeTextSpec.length; + } + + return { + opSpecsA: [removeListSpec], + opSpecsB: [removeTextSpec] + }; + } + + /** + * @param {!ops.OpRemoveList.Spec} removeListSpec + * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec + * @return {?{opSpecsA:!Array., opSpecsB:!Array.}} + */ + function transformRemoveListSplitParagraph(removeListSpec, splitParagraphSpec) { + // adjust list start position only if the paragraph being split is before the list start position + if (splitParagraphSpec.sourceParagraphPosition < removeListSpec.firstParagraphPosition) { + removeListSpec.firstParagraphPosition += 1; + } + + return { + opSpecsA: [removeListSpec], + opSpecsB: [splitParagraphSpec] + }; + } + /** * @param {!ops.OpUpdateParagraphStyle.Spec} updateParagraphStyleSpecA * @param {!ops.OpUpdateParagraphStyle.Spec} updateParagraphStyleSpecB @@ -1451,45 +1728,43 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { }; } - - var /** - * This is the lower-left half of the sparse NxN matrix with all the - * transformation methods on the possible pairs of ops. As the matrix - * is symmetric, only that half is used. So the user of this matrix has - * to ensure the proper order of opspecs on lookup and on calling the - * picked transformation method. - * - * Each transformation method takes the two opspecs (and optionally - * a flag if the first has a higher priority, in case of tie breaking - * having to be done). The method returns a record with the two - * resulting arrays of ops, with key names "opSpecsA" and "opSpecsB". - * Those arrays could have more than the initial respective opspec - * inside, in case some additional helper opspecs are needed, or be - * empty if the opspec turned into a no-op in the transformation. - * If a transformation is not doable, the method returns "null". - * - * Some operations are added onto the stack only by the master session, - * for example AddMember, RemoveMember, and UpdateMember. These therefore need - * not be transformed against each other, since the master session is the - * only originator of these ops. Therefore, their pairing entries in the - * matrix are missing. They do however require a passUnchanged entry - * with the other ops. - * - * Here the CC signature of each transformation method: - * param {!Object} opspecA - * param {!Object} opspecB - * (param {!boolean} hasAPriorityOverB) can be left out - * return {?{opSpecsA:!Array., opSpecsB:!Array.}} - * - * Empty cells in this matrix mean there is no such transformation - * possible, and should be handled as if the method returns "null". - * - * @type {!Object., opSpecsB:!Array.}>>} - */ - transformations; + /** + * This is the lower-left half of the sparse NxN matrix with all the + * transformation methods on the possible pairs of ops. As the matrix + * is symmetric, only that half is used. So the user of this matrix has + * to ensure the proper order of opspecs on lookup and on calling the + * picked transformation method. + * + * Each transformation method takes the two opspecs (and optionally + * a flag if the first has a higher priority, in case of tie breaking + * having to be done). The method returns a record with the two + * resulting arrays of ops, with key names "opSpecsA" and "opSpecsB". + * Those arrays could have more than the initial respective opspec + * inside, in case some additional helper opspecs are needed, or be + * empty if the opspec turned into a no-op in the transformation. + * If a transformation is not doable, the method returns "null". + * + * Some operations are added onto the stack only by the master session, + * for example AddMember, RemoveMember, and UpdateMember. These therefore need + * not be transformed against each other, since the master session is the + * only originator of these ops. Therefore, their pairing entries in the + * matrix are missing. They do however require a passUnchanged entry + * with the other ops. + * + * Here the CC signature of each transformation method: + * param {!Object} opspecA + * param {!Object} opspecB + * (param {!boolean} hasAPriorityOverB) can be left out + * return {?{opSpecsA:!Array., opSpecsB:!Array.}} + * + * Empty cells in this matrix mean there is no such transformation + * possible, and should be handled as if the method returns "null". + */ transformations = { "AddCursor": { "AddCursor": passUnchanged, + "AddList": passUnchanged, + "AddListStyle": passUnchanged, "AddMember": passUnchanged, "AddStyle": passUnchanged, "ApplyDirectStyling": passUnchanged, @@ -1497,6 +1772,46 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": passUnchanged, "MoveCursor": passUnchanged, "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, + "RemoveMember": passUnchanged, + "RemoveStyle": passUnchanged, + "RemoveText": passUnchanged, + "SetParagraphStyle": passUnchanged, + "SplitParagraph": passUnchanged, + "UpdateMember": passUnchanged, + "UpdateMetadata": passUnchanged, + "UpdateParagraphStyle": passUnchanged + }, + "AddList": { + "AddList": transformAddListAddList, + "AddListStyle": passUnchanged, + "AddMember": passUnchanged, + "AddStyle": passUnchanged, + "ApplyDirectStyling": passUnchanged, + "InsertText": transformAddListInsertText, + "MergeParagraph": transformAddListMergeParagraph, + "MoveCursor": passUnchanged, + "RemoveCursor": passUnchanged, + "RemoveList": transformAddListRemoveList, + "RemoveMember": passUnchanged, + "RemoveStyle": passUnchanged, + "RemoveText": transformAddListRemoveText, + "SetParagraphStyle": passUnchanged, + "SplitParagraph": transformAddListSplitParagraph, + "UpdateMember": passUnchanged, + "UpdateMetadata": passUnchanged, + "UpdateParagraphStyle": passUnchanged + }, + "AddListStyle": { + "AddListStyle": transformAddListStyleAddListStyle, + "AddMember": passUnchanged, + "AddStyle": passUnchanged, + "ApplyDirectStyling": passUnchanged, + "InsertText": passUnchanged, + "MergeParagraph": passUnchanged, + "MoveCursor": passUnchanged, + "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": passUnchanged, @@ -1513,6 +1828,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": passUnchanged, "MoveCursor": passUnchanged, "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": passUnchanged, "SetParagraphStyle": passUnchanged, @@ -1527,6 +1843,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": passUnchanged, "MoveCursor": passUnchanged, "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, "RemoveMember": passUnchanged, "RemoveStyle": transformAddStyleRemoveStyle, "RemoveText": passUnchanged, @@ -1542,6 +1859,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": transformApplyDirectStylingMergeParagraph, "MoveCursor": passUnchanged, "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": transformApplyDirectStylingRemoveText, @@ -1556,6 +1874,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": transformInsertTextMergeParagraph, "MoveCursor": transformInsertTextMoveCursor, "RemoveCursor": passUnchanged, + "RemoveList": transformInsertTextRemoveList, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": transformInsertTextRemoveText, @@ -1569,6 +1888,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": transformMergeParagraphMergeParagraph, "MoveCursor": transformMergeParagraphMoveCursor, "RemoveCursor": passUnchanged, + "RemoveList": transformMergeParagraphRemoveList, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": transformMergeParagraphRemoveText, @@ -1581,6 +1901,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MoveCursor": { "MoveCursor": passUnchanged, "RemoveCursor": transformMoveCursorRemoveCursor, + "RemoveList": passUnchanged, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": transformMoveCursorRemoveText, @@ -1593,6 +1914,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "RemoveCursor": { "RemoveCursor": transformRemoveCursorRemoveCursor, "RemoveMember": passUnchanged, + "RemoveList": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": passUnchanged, "SetParagraphStyle": passUnchanged, @@ -1601,6 +1923,17 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "UpdateMetadata": passUnchanged, "UpdateParagraphStyle": passUnchanged }, + "RemoveList": { + "RemoveList": transformRemoveListRemoveList, + "RemoveMember": passUnchanged, + "RemoveStyle": passUnchanged, + "RemoveText": transformRemoveListRemoveText, + "SetParagraphStyle": passUnchanged, + "SplitParagraph": transformRemoveListSplitParagraph, + "UpdateMember": passUnchanged, + "UpdateMetadata": passUnchanged, + "UpdateParagraphStyle": passUnchanged + }, "RemoveMember": { "RemoveStyle": passUnchanged, "RemoveText": passUnchanged, diff --git a/webodf/tests/odf/OdfUtilsTests.js b/webodf/tests/odf/OdfUtilsTests.js index 50ccdddea..580d5374a 100644 --- a/webodf/tests/odf/OdfUtilsTests.js +++ b/webodf/tests/odf/OdfUtilsTests.js @@ -393,6 +393,34 @@ odf.OdfUtilsTests = function OdfUtilsTests(runner) { testFontFamilyNameNormalizing("'serif'", "'serif'"); testFontFamilyNameNormalizing("\"serif\"", "\"serif\""); } + + function isListItemElement_ListItemOrListHeaderElements() { + t.doc = createDocument("HeaderTextTestText"); + t.isListItem1 = t.odfUtils.isListItemOrListHeaderElement(t.doc); + t.isListItem2 = t.odfUtils.isListItemOrListHeaderElement(t.doc.childNodes[0]); + t.isListItem3 = t.odfUtils.isListItemOrListHeaderElement(t.doc.childNodes[0].childNodes[0]); + t.isListItem4 = t.odfUtils.isListItemOrListHeaderElement(t.doc.childNodes[1]); + t.isListItem5 = t.odfUtils.isListItemOrListHeaderElement(t.doc.childNodes[1].childNodes[0]); + + + r.shouldBe(t, t.isListItem1, "false"); + r.shouldBe(t, t.isListItem2, "true"); + r.shouldBe(t, t.isListItem3, "false"); + r.shouldBe(t, t.isListItem4, "true"); + r.shouldBe(t, t.isListItem5, "false"); + } + + function isListElement_ListElements() { + t.doc = createDocument("TestText"); + t.isList1 = t.odfUtils.isListElement(t.doc); + t.isList2 = t.odfUtils.isListElement(t.doc.childNodes[0]); + t.isList3 = t.odfUtils.isListElement(t.doc.childNodes[0].childNodes[0]); + + r.shouldBe(t, t.isList1, "true"); + r.shouldBe(t, t.isList2, "false"); + r.shouldBe(t, t.isList3, "false"); + } + this.tests = function () { return r.name([ isAnchoredAsCharacterElement_ReturnTrueForTab, @@ -403,6 +431,9 @@ odf.OdfUtilsTests = function OdfUtilsTests(runner) { isAnchoredAsCharacterElement_ReturnTrueForAnnotationWrapper, isAnchoredAsCharacterElement_ReturnFalseForNonCharacterFrame, + isListElement_ListElements, + isListItemElement_ListItemOrListHeaderElements, + getTextElements_EncompassedWithinParagraph, getTextElements_EncompassedWithinSpan_And_Paragraph, getTextElements_IgnoresEditInfo, diff --git a/webodf/tests/ops/OperationTests.js b/webodf/tests/ops/OperationTests.js index 3155e3061..99e0eb6cb 100644 --- a/webodf/tests/ops/OperationTests.js +++ b/webodf/tests/ops/OperationTests.js @@ -102,9 +102,14 @@ ops.OperationTests = function OperationTests(runner) { for (i = 0; i < n; i += 1) { att = atts.item(i); value = att.value; - if (/^(length|number|position|fo:font-size|fo:margin-right)$/.test(att.localName)) { + // find integer values + if (/(length|position)/i.test(att.localName)) { value = parseInt(value, 10); } + // find boolean values + if (/^(is|moveCursor)/.test(att.localName)) { + value = JSON.parse(value); + } op[att.nodeName] = value; } // read complex data by childs diff --git a/webodf/tests/ops/TransformationTests.js b/webodf/tests/ops/TransformationTests.js index bd5d616b5..3ccdcc09d 100644 --- a/webodf/tests/ops/TransformationTests.js +++ b/webodf/tests/ops/TransformationTests.js @@ -239,20 +239,8 @@ ops.TransformationTests = function TransformationTests(runner) { for (i = 0; i < n; i += 1) { att = atts.item(i); value = att.value; - switch(att.localName) { - case "length": - case "number": - case "position": - case "fontSize": - case "topMargin": - case "bottomMargin": - case "leftMargin": - case "rightMargin": - case "sourceParagraphPosition": - case "destinationStartPosition": - case "sourceStartPosition": - value = parseInt(value, 10); - break; + if (/(length|position)/i.test(att.localName)) { + value = parseInt(value, 10); } op[att.nodeName] = value; } diff --git a/webodf/tests/ops/operationtests.xml b/webodf/tests/ops/operationtests.xml index 8cb328a4f..07031a60c 100644 --- a/webodf/tests/ops/operationtests.xml +++ b/webodf/tests/ops/operationtests.xml @@ -2119,4 +2119,294 @@ ABC D E + + + + + Sample Text + + + + + + + + + + Sample Text + + + + + + + + + Sample Text + + Sample Text + + + + + + + + + + Sample Text + + + + + + Sample Text + + + + + + + + + Sample1 Text + + Sample2 Text + Sample3 Text + Sample4 Text + + + + + + + + Sample1 Text + + + + + + Sample2 Text + + + Sample3 Text + Sample4 Text + + + + + + + Sample Text + + + + + + + + + + Sample Text + + + + + + + + + Sample Text + Sample Text + + + + + + + + + + Sample Text + + + Sample Text + + + + + + + + + SampleText + + + + + + + + + + SampleText + + + + + + + + + Sample Text + + + + + + + + + + Sample Text + + + + + + + + + + + + Sample Text + + + + + + + + + + Sample Text + + + + + + + + + Sample Text + + + Sample Text + + + Sample Text + + + + + + + + + + Sample Text + Sample Text + Sample Text + + + + + + + + + Sample Text + + + Sample Text + + + + + Sample Text + + + Sample Text + + + + + Sample Text + + + Sample Text + + + + + + + + + + + + Sample Text + Sample Text + Sample Text + Sample Text + Sample Text + Sample Text + + + + + + + Sample Text + + + List Text + + + Sample Text + + + + + + + + Sample Text + List Text + Sample Text + + + + + + + + + Sample Text + + + + + + + + + + Sample Text + + + diff --git a/webodf/tests/ops/transformationtests.xml b/webodf/tests/ops/transformationtests.xml index cae358a0f..b8b9a3f4e 100644 --- a/webodf/tests/ops/transformationtests.xml +++ b/webodf/tests/ops/transformationtests.xml @@ -1989,4 +1989,924 @@ 2013-08-05T12:34:07.061Z + + + + + SampleText + SampleText + + + + + + + + + + + + + SampleText + + + SampleText + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + + + + + Sample1TextFOO + + + Sample2Text + + + Sample3Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + + + + + Sample1Text + + + FOOSample2Text + + + Sample3Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + + + + + Sample1Text + + + Sample2Text + + + FOOSample3Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + + + + + Sample1Text + + + Sample2Text + + + SFOOample3Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + Sample4Text + + + + + + + + + + + Sample1TextSample2Text + + + Sample3Text + + + Sample4Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + Sample4Text + + + + + + + + + + + Sample1Text + + + Sample2TextSample3Text + + + Sample4Text + + + + + + + + + Sample1Text + Sample2Text + + + + + + + + + + + + + Sample1TextSample2Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + Sample4Text + + + + + + + + + + + Sample1Text + + + Sample2TextSample3Text + + + Sample4Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + Sample4Text + + + + + + + + + + + Sample1Text + + + Sample2Text + + + Sample3TextSample4Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + Sample4Text + + + + + + + + + + + + + Sample1Text + + + Sample2Text + + + Sample3TextSample4Text + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + + + + + Sample + + + Sample2Text + + + Sample3Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + + + + + Sample1Text + + + Text + + + Sample3Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + + + + + Sample1Text + + + Sample2Text + + + Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + + + + + Sample1Text + + + Sample2Text + + + S + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + Sample4Text + + + + + + + + + + + Sample1 + Text + Sample2Text + + + Sample3Text + + + Sample4Text + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + Sample4Text + + + + + + + + + + + Sample1Text + + + Sample2 + + + Text + + + Sample3Text + + + Sample4Text + + + + + + + + + Sample1Text + Sample2Text + + + + + + + + + + + Sample1Text + + + Sample2 + + + Text + + + + + + + + + Sample1Text + Sample2Text + + + + + + + + + + + + + Sample1Text + + + Sample2 + + + Text + + + + + + + + + + + + SampleText + + + + + + + + + + + + + SampleText + + + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + + + + + + Sample1Text + Sample2Text + Sample3Text + + + + + + + Sample1Text + + + Sample2Text + + + + + + + + + + + + + Sample1TextFOO + Sample2Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + + + + + + Sample1Text + FOOSample2Text + Sample3Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + + + + + + Sample1Text + Sample2Text + FOOSample3Text + + + + + + + Sample1Text + Sample2Text + + + Sample3Text + + + + + + + + + + + + + Sample1TextSample2Text + Sample3Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + + + + + + Sample1TextSample2Text + Sample3Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + + + + + + Sample1Text + Sample2TextSample3Text + + + + + + + Sample1Text + + + Sample2Text + + + + + + + + + + + + + Text + Sample2Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + + + + + + Sample1Text + Text + Sample3Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + + + + + + Sample1Text + Sample2Text + Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + va + + + + + Sample1 + Text + Sample2Text + Sample3Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + va + + + + + Sample1Text + Sample2 + Text + Sample3Text + + + + + + + Sample1Text + + + Sample2Text + + + Sample3Text + + + + + + + + va + + + + + Sample1Text + Sample2Text + Sample3 + Text + + + diff --git a/webodf/tools/karma.conf.js b/webodf/tools/karma.conf.js index 6da6bc6f8..0fbe73f16 100644 --- a/webodf/tools/karma.conf.js +++ b/webodf/tools/karma.conf.js @@ -82,6 +82,8 @@ module.exports = function (config) { 'lib/ops/OdtDocument.js', 'lib/ops/OpAddAnnotation.js', 'lib/ops/OpAddCursor.js', + 'lib/ops/OpAddList.js', + 'lib/ops/OpAddListStyle.js', 'lib/ops/OpAddMember.js', 'lib/ops/OpAddStyle.js', 'lib/odf/ObjectNameGenerator.js', @@ -98,6 +100,7 @@ module.exports = function (config) { 'lib/ops/OpRemoveBlob.js', 'lib/ops/OpRemoveCursor.js', 'lib/ops/OpRemoveHyperlink.js', + 'lib/ops/OpRemoveList.js', 'lib/ops/OpRemoveMember.js', 'lib/ops/OpRemoveStyle.js', 'lib/ops/OpRemoveText.js', @@ -130,6 +133,9 @@ module.exports = function (config) { 'lib/gui/ImageController.js', 'lib/gui/ImageSelector.js', 'lib/gui/InputMethodEditor.js', + 'lib/gui/DefaultStyles.js', + 'lib/gui/ListStyleSummary.js', + 'lib/gui/ListController.js', 'lib/gui/MetadataController.js', 'lib/gui/PasteController.js', 'lib/gui/ClosestXOffsetScanner.js',