From 8f8df7bc83384778316a30525c88627bec78c8d7 Mon Sep 17 00:00:00 2001 From: Satvik Kumar Date: Fri, 30 May 2014 15:36:21 +1000 Subject: [PATCH 1/7] Add UI buttons for list creation --- programs/editor/Tools.js | 7 ++- programs/editor/widgets/toggleLists.js | 87 ++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 programs/editor/widgets/toggleLists.js diff --git a/programs/editor/Tools.js b/programs/editor/Tools.js index b58f0ebd3..2d8194646 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, true); + // 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..d0adbdbbf --- /dev/null +++ b/programs/editor/widgets/toggleLists.js @@ -0,0 +1,87 @@ +/** + * 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, + widget = {}, + numberedList, + bulletedList; + + numberedList = new ToggleButton({ + label: runtime.tr('Numbering'), + disabled: true, + showLabel: false, + checked: false, + iconClass: "dijitEditorIcon dijitEditorIconInsertOrderedList", + onChange: function () { + } + }); + + bulletedList = new ToggleButton({ + label: runtime.tr('Bullets'), + disabled: true, + showLabel: false, + checked: false, + iconClass: "dijitEditorIcon dijitEditorIconInsertUnorderedList", + onChange: function () { + } + }); + + 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; + }; + + this.onToolDone = function () { + }; + + this.setEditorSession = function (session) { + }; + + callback(widget); + }; + + return ToggleLists; + } +); \ No newline at end of file From 0520f0c3f2cc1bd49b010fef5e7307b25a20aa1f Mon Sep 17 00:00:00 2001 From: Satvik Kumar Date: Mon, 2 Jun 2014 16:42:06 +1000 Subject: [PATCH 2/7] Add list controller and responding to cursor events --- programs/editor/widgets/toggleLists.js | 17 +++ webodf/lib/gui/ListController.js | 159 +++++++++++++++++++++++++ webodf/lib/gui/ListStyleSummary.js | 136 +++++++++++++++++++++ webodf/lib/gui/SessionController.js | 9 ++ webodf/lib/manifest.json | 9 ++ webodf/lib/odf/OdfUtils.js | 18 ++- webodf/lib/ops/OpSplitParagraph.js | 4 +- webodf/tests/odf/OdfUtilsTests.js | 31 +++++ webodf/tools/karma.conf.js | 2 + 9 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 webodf/lib/gui/ListController.js create mode 100644 webodf/lib/gui/ListStyleSummary.js diff --git a/programs/editor/widgets/toggleLists.js b/programs/editor/widgets/toggleLists.js index d0adbdbbf..6c598ef3d 100644 --- a/programs/editor/widgets/toggleLists.js +++ b/programs/editor/widgets/toggleLists.js @@ -34,6 +34,7 @@ define("webodf/editor/widgets/toggleLists", [ var ToggleLists = function (callback) { var self = this, editorSession, + listController, widget = {}, numberedList, bulletedList; @@ -73,10 +74,26 @@ define("webodf/editor/widgets/toggleLists", [ return widget; }; + 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); + } + + editorSession = session; + + if (editorSession) { + listController = editorSession.sessionController.getListController(); + listController.subscribe(gui.ListController.listStylingChanged, updateToggleButtons); + } }; callback(widget); diff --git a/webodf/lib/gui/ListController.js b/webodf/lib/gui/ListController.js new file mode 100644 index 000000000..9f16fbf7a --- /dev/null +++ b/webodf/lib/gui/ListController.js @@ -0,0 +1,159 @@ +/** + * 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, runtime*/ + +/** + * @implements {core.Destroyable} + * @param {!ops.Session} session + * @param {!string} inputMemberId + * @constructor + */ +gui.ListController = function ListController(session, inputMemberId) { + "use strict"; + var odtDocument = session.getOdtDocument(), + odfUtils = odf.OdfUtils, + eventNotifier = new core.EventNotifier([ + gui.ListController.listStylingChanged + ]), + /**@type{!gui.ListStyleSummary}*/ + lastSignalledSelectionInfo, + /**@type{!core.LazyProperty.}*/ + cachedSelectionInfo; + + /** + * @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.ListStyleSummary} + */ + function getSelectionInfo() { + var cursor = odtDocument.getCursor(inputMemberId), + cursorNode = cursor && cursor.getNode(); + + return new gui.ListStyleSummary(cursorNode, odtDocument.getRootNode(), odtDocument.getFormatting()); + } + + /** + * @return {undefined} + */ + function emitSelectionChanges() { + var hasChanged = true, + newStyleSummary = cachedSelectionInfo.value(); + + if (lastSignalledSelectionInfo) { + hasChanged = lastSignalledSelectionInfo.isNumberedList !== newStyleSummary.isNumberedList || + lastSignalledSelectionInfo.isBulletedList !== newStyleSummary.isBulletedList; + } + + if (hasChanged) { + lastSignalledSelectionInfo = newStyleSummary; + eventNotifier.emit(gui.ListController.listStylingChanged, lastSignalledSelectionInfo); + } + } + + /** + * @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 + * @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); + 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); + + cachedSelectionInfo = new core.LazyProperty(getSelectionInfo); + } + + init(); +}; + +/**@const*/ +gui.ListController.listStylingChanged = "listStyling/changed"; 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..50d335311 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, 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..288280988 100644 --- a/webodf/lib/manifest.json +++ b/webodf/lib/manifest.json @@ -153,6 +153,14 @@ "core.typedefs", "gui.VisualStepScanner" ], + "gui.ListController": [ + "core.LazyProperty", + "gui.ListStyleSummary", + "ops.Session" + ], + "gui.ListStyleSummary": [ + "odf.Formatting" + ], "gui.MetadataController": [ "ops.Session" ], @@ -204,6 +212,7 @@ "gui.ImageController", "gui.ImageSelector", "gui.InputMethodEditor", + "gui.ListController", "gui.MetadataController", "gui.PasteController", "gui.SelectionController", diff --git a/webodf/lib/odf/OdfUtils.js b/webodf/lib/odf/OdfUtils.js index ba94567f9..507c9d07e 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; }; /** 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/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/tools/karma.conf.js b/webodf/tools/karma.conf.js index 6da6bc6f8..afc75867c 100644 --- a/webodf/tools/karma.conf.js +++ b/webodf/tools/karma.conf.js @@ -130,6 +130,8 @@ module.exports = function (config) { 'lib/gui/ImageController.js', 'lib/gui/ImageSelector.js', 'lib/gui/InputMethodEditor.js', + 'lib/gui/ListStyleSummary.js', + 'lib/gui/ListController.js', 'lib/gui/MetadataController.js', 'lib/gui/PasteController.js', 'lib/gui/ClosestXOffsetScanner.js', From 27704890fbd712f0aa7b87063feed6cdaf207239 Mon Sep 17 00:00:00 2001 From: Satvik Kumar Date: Thu, 14 Aug 2014 14:41:07 +1000 Subject: [PATCH 3/7] Add support for session constraints to list controller --- programs/editor/Tools.js | 2 +- programs/editor/widgets/toggleLists.js | 11 +++ programs/editor/wodocollabtexteditor.js | 3 + programs/editor/wodotexteditor.js | 3 + webodf/lib/gui/ListController.js | 94 +++++++++++++++++++++---- webodf/lib/gui/SessionController.js | 2 +- webodf/lib/manifest.json | 4 +- 7 files changed, 102 insertions(+), 17 deletions(-) diff --git a/programs/editor/Tools.js b/programs/editor/Tools.js index 2d8194646..1a8607f1e 100644 --- a/programs/editor/Tools.js +++ b/programs/editor/Tools.js @@ -174,7 +174,7 @@ define("webodf/editor/Tools", [ paragraphAlignment = createTool(ParagraphAlignment, args.directParagraphStylingEnabled); // Numbered and bulleted list toggle buttons - toggleLists = createTool(ToggleLists, true); + 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 index 6c598ef3d..bdb53f218 100644 --- a/programs/editor/widgets/toggleLists.js +++ b/programs/editor/widgets/toggleLists.js @@ -74,6 +74,12 @@ define("webodf/editor/widgets/toggleLists", [ 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); @@ -86,6 +92,7 @@ define("webodf/editor/widgets/toggleLists", [ this.setEditorSession = function (session) { if (editorSession) { listController.unsubscribe(gui.ListController.listStylingChanged, updateToggleButtons); + listController.unsubscribe(gui.ListController.enabledChanged, enableToggleButtons); } editorSession = session; @@ -93,6 +100,10 @@ define("webodf/editor/widgets/toggleLists", [ if (editorSession) { listController = editorSession.sessionController.getListController(); listController.subscribe(gui.ListController.listStylingChanged, updateToggleButtons); + listController.subscribe(gui.ListController.enabledChanged, enableToggleButtons); + enableToggleButtons(listController.isEnabled()); + } else { + enableToggleButtons(false); } }; diff --git a/programs/editor/wodocollabtexteditor.js b/programs/editor/wodocollabtexteditor.js index befead563..71653e99d 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), 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/ListController.js b/webodf/lib/gui/ListController.js index 9f16fbf7a..c08660f5a 100644 --- a/webodf/lib/gui/ListController.js +++ b/webodf/lib/gui/ListController.js @@ -27,19 +27,22 @@ /** * @implements {core.Destroyable} * @param {!ops.Session} session + * @param {!gui.SessionConstraints} sessionConstraints + * @param {!gui.SessionContext} sessionContext * @param {!string} inputMemberId * @constructor */ -gui.ListController = function ListController(session, inputMemberId) { +gui.ListController = function ListController(session, sessionConstraints, sessionContext, inputMemberId) { "use strict"; var odtDocument = session.getOdtDocument(), odfUtils = odf.OdfUtils, eventNotifier = new core.EventNotifier([ - gui.ListController.listStylingChanged + gui.ListController.listStylingChanged, + gui.ListController.enabledChanged ]), - /**@type{!gui.ListStyleSummary}*/ + /**@type{!gui.ListController.SelectionInfo}*/ lastSignalledSelectionInfo, - /**@type{!core.LazyProperty.}*/ + /**@type{!core.LazyProperty.}*/ cachedSelectionInfo; /** @@ -79,33 +82,67 @@ gui.ListController = function ListController(session, inputMemberId) { } /** - * @return {!gui.ListStyleSummary} + * @return {!gui.ListController.SelectionInfo} */ function getSelectionInfo() { var cursor = odtDocument.getCursor(inputMemberId), - cursorNode = cursor && cursor.getNode(); + cursorNode = cursor && cursor.getNode(), + styleSummary = new gui.ListStyleSummary(cursorNode, odtDocument.getRootNode(), odtDocument.getFormatting()), + isEnabled = true; - return new gui.ListStyleSummary(cursorNode, odtDocument.getRootNode(), odtDocument.getFormatting()); + if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) { + isEnabled = sessionContext.isLocalCursorWithinOwnAnnotation(); + } + + return new gui.ListController.SelectionInfo(isEnabled, styleSummary); } /** * @return {undefined} */ function emitSelectionChanges() { - var hasChanged = true, - newStyleSummary = cachedSelectionInfo.value(); + var hasStyleChanged = true, + hasEnabledChanged = true, + newSelectionInfo = cachedSelectionInfo.value(), + lastStyleSummary, + newStyleSummary; if (lastSignalledSelectionInfo) { - hasChanged = lastSignalledSelectionInfo.isNumberedList !== newStyleSummary.isNumberedList || - lastSignalledSelectionInfo.isBulletedList !== newStyleSummary.isBulletedList; + lastStyleSummary = lastSignalledSelectionInfo.styleSummary; + newStyleSummary = newSelectionInfo.styleSummary; + + hasStyleChanged = lastStyleSummary.isNumberedList !== newStyleSummary.isNumberedList || + lastStyleSummary.isBulletedList !== newStyleSummary.isBulletedList; + + hasEnabledChanged = lastSignalledSelectionInfo.isEnabled !== newSelectionInfo.isEnabled; } - if (hasChanged) { - lastSignalledSelectionInfo = newStyleSummary; - eventNotifier.emit(gui.ListController.listStylingChanged, lastSignalledSelectionInfo); + 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(); + } + + /** + * @return {!boolean} + */ + this.isEnabled = function () { + return cachedSelectionInfo.value().isEnabled; + }; + /** * @param {!string} eventid * @param {!Function} cb @@ -135,6 +172,7 @@ gui.ListController = function ListController(session, inputMemberId) { 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(); }; @@ -148,6 +186,7 @@ gui.ListController = function ListController(session, inputMemberId) { 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); } @@ -157,3 +196,30 @@ gui.ListController = function ListController(session, inputMemberId) { /**@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/SessionController.js b/webodf/lib/gui/SessionController.js index 50d335311..e84b6f696 100644 --- a/webodf/lib/gui/SessionController.js +++ b/webodf/lib/gui/SessionController.js @@ -90,7 +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, inputMemberId), + listController = new gui.ListController(session, sessionConstraints, sessionContext, inputMemberId), imageSelector = new gui.ImageSelector(odtDocument.getOdfCanvas()), shadowCursorIterator = odtDocument.createPositionIterator(odtDocument.getRootNode()), /**@type{!core.ScheduledTask}*/ diff --git a/webodf/lib/manifest.json b/webodf/lib/manifest.json index 288280988..a3553eb87 100644 --- a/webodf/lib/manifest.json +++ b/webodf/lib/manifest.json @@ -155,8 +155,10 @@ ], "gui.ListController": [ "core.LazyProperty", + "gui.CommonConstraints", "gui.ListStyleSummary", - "ops.Session" + "gui.SessionConstraints", + "gui.SessionContext" ], "gui.ListStyleSummary": [ "odf.Formatting" From 544529c895a39f84a299bbebf5665cf1bf0219d1 Mon Sep 17 00:00:00 2001 From: Satvik Kumar Date: Mon, 18 Aug 2014 17:12:03 +1000 Subject: [PATCH 4/7] Add unstable flags for the list editing feature --- programs/editor/wodocollabtexteditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/editor/wodocollabtexteditor.js b/programs/editor/wodocollabtexteditor.js index 71653e99d..b8dd16073 100644 --- a/programs/editor/wodocollabtexteditor.js +++ b/programs/editor/wodocollabtexteditor.js @@ -177,7 +177,7 @@ var Wodo = Wodo || (function () { paragraphStyleEditingEnabled = isEnabled(editorOptions.paragraphStyleEditingEnabled), imageEditingEnabled = isEnabled(editorOptions.imageEditingEnabled, true), hyperlinkEditingEnabled = isEnabled(editorOptions.hyperlinkEditingEnabled, true), - listEditingEnabled = isEnabled(editorOptions.listEditingEnabled), + listEditingEnabled = isEnabled(editorOptions.listEditingEnabled, true), reviewModeEnabled = isEnabled(editorOptions.reviewModeEnabled, true), annotationsEnabled = reviewModeEnabled || isEnabled(editorOptions.annotationsEnabled, true), undoRedoEnabled = false, // no proper mechanism yet for collab From 06f0562a5c8da5db684c82c9c0897ca9634e0202 Mon Sep 17 00:00:00 2001 From: Satvik Kumar Date: Fri, 11 Jul 2014 16:09:10 +1000 Subject: [PATCH 5/7] Operations for adding and removing lists These operations work on the level of top level lists only Added tests for the new operations and changed the op test code to parse any op parameter containing "length" or "position" in the name as an integer --- programs/editor/widgets/toggleLists.js | 16 +- webodf/lib/gui/ListController.js | 203 ++++++++++++++++- webodf/lib/manifest.json | 8 + webodf/lib/odf/OdfUtils.js | 24 ++ webodf/lib/ops/OpAddList.js | 179 +++++++++++++++ webodf/lib/ops/OpRemoveList.js | 142 ++++++++++++ webodf/lib/ops/OperationFactory.js | 4 +- webodf/tests/ops/OperationTests.js | 3 +- webodf/tests/ops/operationtests.xml | 290 +++++++++++++++++++++++++ webodf/tools/karma.conf.js | 2 + 10 files changed, 865 insertions(+), 6 deletions(-) create mode 100644 webodf/lib/ops/OpAddList.js create mode 100644 webodf/lib/ops/OpRemoveList.js diff --git a/programs/editor/widgets/toggleLists.js b/programs/editor/widgets/toggleLists.js index bdb53f218..f3685e32d 100644 --- a/programs/editor/widgets/toggleLists.js +++ b/programs/editor/widgets/toggleLists.js @@ -45,7 +45,13 @@ define("webodf/editor/widgets/toggleLists", [ showLabel: false, checked: false, iconClass: "dijitEditorIcon dijitEditorIconInsertOrderedList", - onChange: function () { + 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(); } }); @@ -55,7 +61,13 @@ define("webodf/editor/widgets/toggleLists", [ showLabel: false, checked: false, iconClass: "dijitEditorIcon dijitEditorIconInsertUnorderedList", - onChange: function () { + 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(); } }); diff --git a/webodf/lib/gui/ListController.js b/webodf/lib/gui/ListController.js index c08660f5a..647dff722 100644 --- a/webodf/lib/gui/ListController.js +++ b/webodf/lib/gui/ListController.js @@ -22,7 +22,7 @@ * @source: https://github.com/kogmbh/WebODF/ */ -/*global core, ops, gui, odf, runtime*/ +/*global core, ops, gui, odf, NodeFilter, runtime*/ /** * @implements {core.Destroyable} @@ -36,6 +36,7 @@ gui.ListController = function ListController(session, sessionConstraints, sessio "use strict"; var odtDocument = session.getOdtDocument(), odfUtils = odf.OdfUtils, + domUtils = core.DomUtils, eventNotifier = new core.EventNotifier([ gui.ListController.listStylingChanged, gui.ListController.enabledChanged @@ -43,7 +44,9 @@ gui.ListController = function ListController(session, sessionConstraints, sessio /**@type{!gui.ListController.SelectionInfo}*/ lastSignalledSelectionInfo, /**@type{!core.LazyProperty.}*/ - cachedSelectionInfo; + cachedSelectionInfo, + /**@const*/ + NEXT = core.StepDirection.NEXT; /** * @param {!ops.OdtCursor|!string} cursorOrId @@ -136,6 +139,161 @@ gui.ListController = function ListController(session, sessionConstraints, sessio 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 + }; + } + + /** + * 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 + * @return {!Array.} + */ + function determineOpsForAddingLists() { + 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 + 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) + }); + 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} */ @@ -161,6 +319,47 @@ gui.ListController = function ListController(session, sessionConstraints, sessio eventNotifier.unsubscribe(eventid, cb); }; + /** + * @return {!boolean} + */ + function makeList() { + return executeListOperations(determineOpsForAddingLists); + } + + this.makeList = makeList; + + /** + * @return {!boolean} + */ + function removeList() { + return executeListOperations(determineOpsForRemovingLists); + } + + this.removeList = removeList; + + /** + * @param {!boolean} checked + * @return {!boolean} + */ + this.setNumberedList = function (checked) { + if (checked) { + return makeList(); + } + return removeList(); + + }; + + /** + * @param {!boolean} checked + * @return {!boolean} + */ + this.setBulletedList = function (checked) { + if (checked) { + return makeList(); + } + return removeList(); + }; + /** * @param {!function(!Error=)} callback * @return {undefined} diff --git a/webodf/lib/manifest.json b/webodf/lib/manifest.json index a3553eb87..1a64f7167 100644 --- a/webodf/lib/manifest.json +++ b/webodf/lib/manifest.json @@ -415,6 +415,9 @@ "ops.OpAddCursor": [ "ops.OdtDocument" ], + "ops.OpAddList": [ + "ops.OdtDocument" + ], "ops.OpAddMember": [ "ops.OdtDocument" ], @@ -456,6 +459,9 @@ "ops.OpRemoveHyperlink": [ "ops.OdtDocument" ], + "ops.OpRemoveList": [ + "ops.OdtDocument" + ], "ops.OpRemoveMember": [ "ops.OdtDocument" ], @@ -490,6 +496,7 @@ "ops.OperationFactory": [ "ops.OpAddAnnotation", "ops.OpAddCursor", + "ops.OpAddList", "ops.OpAddMember", "ops.OpAddStyle", "ops.OpApplyDirectStyling", @@ -503,6 +510,7 @@ "ops.OpRemoveBlob", "ops.OpRemoveCursor", "ops.OpRemoveHyperlink", + "ops.OpRemoveList", "ops.OpRemoveMember", "ops.OpRemoveStyle", "ops.OpRemoveText", diff --git a/webodf/lib/odf/OdfUtils.js b/webodf/lib/odf/OdfUtils.js index 507c9d07e..b6827ad36 100644 --- a/webodf/lib/odf/OdfUtils.js +++ b/webodf/lib/odf/OdfUtils.js @@ -1066,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/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/OperationFactory.js b/webodf/lib/ops/OperationFactory.js index af250a294..9d6e8bb91 100644 --- a/webodf/lib/ops/OperationFactory.js +++ b/webodf/lib/ops/OperationFactory.js @@ -100,7 +100,9 @@ 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) }; } diff --git a/webodf/tests/ops/OperationTests.js b/webodf/tests/ops/OperationTests.js index 3155e3061..37e8a7235 100644 --- a/webodf/tests/ops/OperationTests.js +++ b/webodf/tests/ops/OperationTests.js @@ -102,7 +102,8 @@ 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); } 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/tools/karma.conf.js b/webodf/tools/karma.conf.js index afc75867c..77570b36a 100644 --- a/webodf/tools/karma.conf.js +++ b/webodf/tools/karma.conf.js @@ -82,6 +82,7 @@ module.exports = function (config) { 'lib/ops/OdtDocument.js', 'lib/ops/OpAddAnnotation.js', 'lib/ops/OpAddCursor.js', + 'lib/ops/OpAddList.js', 'lib/ops/OpAddMember.js', 'lib/ops/OpAddStyle.js', 'lib/odf/ObjectNameGenerator.js', @@ -98,6 +99,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', From d63e8058e809dbf02c12157a2f271237a755e01d Mon Sep 17 00:00:00 2001 From: Satvik Kumar Date: Tue, 29 Jul 2014 17:42:08 +1000 Subject: [PATCH 6/7] Operational transforms for adding and removing lists This handles transforms between Add/Remove list and other operations that modify the number of steps in the document. There are some unresolved conflicts with merging paragraphs and adding lists that return null as the transform result which required merge and split operations on lists to be resolved correctly. Added tests for the new parts of the op transform matrix and changed the op transform test code to parse any op parameter containing "length" or "position" in the name as an integer. --- webodf/lib/manifest.json | 2 + webodf/lib/ops/OperationTransformMatrix.js | 368 ++++++++- webodf/tests/ops/TransformationTests.js | 16 +- webodf/tests/ops/transformationtests.xml | 920 +++++++++++++++++++++ 4 files changed, 1256 insertions(+), 50 deletions(-) diff --git a/webodf/lib/manifest.json b/webodf/lib/manifest.json index 1a64f7167..47789d1a0 100644 --- a/webodf/lib/manifest.json +++ b/webodf/lib/manifest.json @@ -525,12 +525,14 @@ "ops.OperationFactory" ], "ops.OperationTransformMatrix": [ + "ops.OpAddList", "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/ops/OperationTransformMatrix.js b/webodf/lib/ops/OperationTransformMatrix.js index c4a743ca6..86cc3c317 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,171 @@ 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.OpAddStyle.Spec} addStyleSpec * @param {!ops.OpRemoveStyle.Spec} removeStyleSpec @@ -603,6 +771,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 +980,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 +1117,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 +1712,42 @@ 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, "AddMember": passUnchanged, "AddStyle": passUnchanged, "ApplyDirectStyling": passUnchanged, @@ -1497,6 +1755,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": passUnchanged, "MoveCursor": passUnchanged, "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": passUnchanged, @@ -1506,6 +1765,25 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "UpdateMetadata": passUnchanged, "UpdateParagraphStyle": passUnchanged }, + "AddList": { + "AddList": transformAddListAddList, + "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 + }, "AddMember": { "AddStyle": passUnchanged, "ApplyDirectStyling": passUnchanged, @@ -1513,6 +1791,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": passUnchanged, "MoveCursor": passUnchanged, "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": passUnchanged, "SetParagraphStyle": passUnchanged, @@ -1527,6 +1806,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": passUnchanged, "MoveCursor": passUnchanged, "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, "RemoveMember": passUnchanged, "RemoveStyle": transformAddStyleRemoveStyle, "RemoveText": passUnchanged, @@ -1542,6 +1822,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": transformApplyDirectStylingMergeParagraph, "MoveCursor": passUnchanged, "RemoveCursor": passUnchanged, + "RemoveList": passUnchanged, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": transformApplyDirectStylingRemoveText, @@ -1556,6 +1837,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": transformInsertTextMergeParagraph, "MoveCursor": transformInsertTextMoveCursor, "RemoveCursor": passUnchanged, + "RemoveList": transformInsertTextRemoveList, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": transformInsertTextRemoveText, @@ -1569,6 +1851,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MergeParagraph": transformMergeParagraphMergeParagraph, "MoveCursor": transformMergeParagraphMoveCursor, "RemoveCursor": passUnchanged, + "RemoveList": transformMergeParagraphRemoveList, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": transformMergeParagraphRemoveText, @@ -1581,6 +1864,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "MoveCursor": { "MoveCursor": passUnchanged, "RemoveCursor": transformMoveCursorRemoveCursor, + "RemoveList": passUnchanged, "RemoveMember": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": transformMoveCursorRemoveText, @@ -1593,6 +1877,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "RemoveCursor": { "RemoveCursor": transformRemoveCursorRemoveCursor, "RemoveMember": passUnchanged, + "RemoveList": passUnchanged, "RemoveStyle": passUnchanged, "RemoveText": passUnchanged, "SetParagraphStyle": passUnchanged, @@ -1601,6 +1886,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/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/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 + + + From 33566eb78b29b6fc3f854d221042bc8a101adc65 Mon Sep 17 00:00:00 2001 From: Satvik Kumar Date: Wed, 13 Aug 2014 01:03:16 +1000 Subject: [PATCH 7/7] Default styles for numbered and bulleted lists --- webodf/lib/gui/DefaultStyles.js | 370 +++++++++++++++++++++ webodf/lib/gui/ListController.js | 75 ++++- webodf/lib/manifest.json | 9 + webodf/lib/ops/OpAddListStyle.js | 130 ++++++++ webodf/lib/ops/OperationFactory.js | 3 +- webodf/lib/ops/OperationTransformMatrix.js | 37 +++ webodf/tests/ops/OperationTests.js | 4 + webodf/tools/karma.conf.js | 2 + 8 files changed, 620 insertions(+), 10 deletions(-) create mode 100644 webodf/lib/gui/DefaultStyles.js create mode 100644 webodf/lib/ops/OpAddListStyle.js 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 index 647dff722..4cbb4dc46 100644 --- a/webodf/lib/gui/ListController.js +++ b/webodf/lib/gui/ListController.js @@ -46,7 +46,11 @@ gui.ListController = function ListController(session, sessionConstraints, sessio /**@type{!core.LazyProperty.}*/ cachedSelectionInfo, /**@const*/ - NEXT = core.StepDirection.NEXT; + NEXT = core.StepDirection.NEXT, + /**@const*/ + DEFAULT_NUMBERING_STYLE = "WebODF-Numbering", + /**@const*/ + DEFAULT_BULLETED_STYLE = "WebODF-Bulleted"; /** * @param {!ops.OdtCursor|!string} cursorOrId @@ -193,13 +197,38 @@ gui.ListController = function ListController(session, sessionConstraints, sessio }; } + /** + * @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() { + function determineOpsForAddingLists(styleName) { var paragraphElements, /**@type{!Array.}*/ paragraphGroups = [], @@ -213,6 +242,7 @@ gui.ListController = function ListController(session, sessionConstraints, sessio 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; @@ -238,7 +268,8 @@ gui.ListController = function ListController(session, sessionConstraints, sessio newOp.init({ memberid: inputMemberId, startParagraphPosition: odtDocument.convertDomPointToCursorStep(group.startParagraph, 0, NEXT), - endParagraphPosition: odtDocument.convertDomPointToCursorStep(group.endParagraph, 0, NEXT) + endParagraphPosition: odtDocument.convertDomPointToCursorStep(group.endParagraph, 0, NEXT), + styleName: styleName }); return newOp; }); @@ -320,13 +351,39 @@ gui.ListController = function ListController(session, sessionConstraints, sessio }; /** + * @param {!string=} styleName * @return {!boolean} */ - function makeList() { - return executeListOperations(determineOpsForAddingLists); - } + 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; + } + } - this.makeList = makeList; + 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} @@ -343,7 +400,7 @@ gui.ListController = function ListController(session, sessionConstraints, sessio */ this.setNumberedList = function (checked) { if (checked) { - return makeList(); + return makeList(DEFAULT_NUMBERING_STYLE); } return removeList(); @@ -355,7 +412,7 @@ gui.ListController = function ListController(session, sessionConstraints, sessio */ this.setBulletedList = function (checked) { if (checked) { - return makeList(); + return makeList(DEFAULT_BULLETED_STYLE); } return removeList(); }; diff --git a/webodf/lib/manifest.json b/webodf/lib/manifest.json index 47789d1a0..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", @@ -156,6 +159,7 @@ "gui.ListController": [ "core.LazyProperty", "gui.CommonConstraints", + "gui.DefaultStyles", "gui.ListStyleSummary", "gui.SessionConstraints", "gui.SessionContext" @@ -418,6 +422,9 @@ "ops.OpAddList": [ "ops.OdtDocument" ], + "ops.OpAddListStyle": [ + "ops.OdtDocument" + ], "ops.OpAddMember": [ "ops.OdtDocument" ], @@ -497,6 +504,7 @@ "ops.OpAddAnnotation", "ops.OpAddCursor", "ops.OpAddList", + "ops.OpAddListStyle", "ops.OpAddMember", "ops.OpAddStyle", "ops.OpApplyDirectStyling", @@ -526,6 +534,7 @@ ], "ops.OperationTransformMatrix": [ "ops.OpAddList", + "ops.OpAddListStyle", "ops.OpAddStyle", "ops.OpApplyDirectStyling", "ops.OpInsertText", 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/OperationFactory.js b/webodf/lib/ops/OperationFactory.js index 9d6e8bb91..af2911de9 100644 --- a/webodf/lib/ops/OperationFactory.js +++ b/webodf/lib/ops/OperationFactory.js @@ -102,7 +102,8 @@ ops.OperationFactory = function OperationFactory() { ApplyHyperlink: construct(ops.OpApplyHyperlink), RemoveHyperlink: construct(ops.OpRemoveHyperlink), AddList: construct(ops.OpAddList), - RemoveList: construct(ops.OpRemoveList) + RemoveList: construct(ops.OpRemoveList), + AddListStyle: construct(ops.OpAddListStyle) }; } diff --git a/webodf/lib/ops/OperationTransformMatrix.js b/webodf/lib/ops/OperationTransformMatrix.js index 86cc3c317..9696e1aa0 100644 --- a/webodf/lib/ops/OperationTransformMatrix.js +++ b/webodf/lib/ops/OperationTransformMatrix.js @@ -441,6 +441,22 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { }; } + /** + * @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 @@ -1748,6 +1764,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "AddCursor": { "AddCursor": passUnchanged, "AddList": passUnchanged, + "AddListStyle": passUnchanged, "AddMember": passUnchanged, "AddStyle": passUnchanged, "ApplyDirectStyling": passUnchanged, @@ -1767,6 +1784,7 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { }, "AddList": { "AddList": transformAddListAddList, + "AddListStyle": passUnchanged, "AddMember": passUnchanged, "AddStyle": passUnchanged, "ApplyDirectStyling": passUnchanged, @@ -1784,6 +1802,25 @@ ops.OperationTransformMatrix = function OperationTransformMatrix() { "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, + "SetParagraphStyle": passUnchanged, + "SplitParagraph": passUnchanged, + "UpdateMember": passUnchanged, + "UpdateMetadata": passUnchanged, + "UpdateParagraphStyle": passUnchanged + }, "AddMember": { "AddStyle": passUnchanged, "ApplyDirectStyling": passUnchanged, diff --git a/webodf/tests/ops/OperationTests.js b/webodf/tests/ops/OperationTests.js index 37e8a7235..99e0eb6cb 100644 --- a/webodf/tests/ops/OperationTests.js +++ b/webodf/tests/ops/OperationTests.js @@ -106,6 +106,10 @@ ops.OperationTests = function OperationTests(runner) { 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/tools/karma.conf.js b/webodf/tools/karma.conf.js index 77570b36a..0fbe73f16 100644 --- a/webodf/tools/karma.conf.js +++ b/webodf/tools/karma.conf.js @@ -83,6 +83,7 @@ module.exports = function (config) { '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', @@ -132,6 +133,7 @@ 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',