Skip to content

Commit 19b0dab

Browse files
author
Satvik Kumar
committed
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
1 parent 5a8e387 commit 19b0dab

10 files changed

Lines changed: 661 additions & 6 deletions

File tree

programs/editor/widgets/toggleLists.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ define("webodf/editor/widgets/toggleLists", [
4545
showLabel: false,
4646
checked: false,
4747
iconClass: "dijitEditorIcon dijitEditorIconInsertOrderedList",
48-
onChange: function () {
48+
onChange: function (checked) {
49+
var success = listController.setNumberedList(checked);
50+
//TODO: remove this when the list controller supports all use cases triggered by this button
51+
if(!success) {
52+
numberedList.set("checked", !checked, false);
53+
}
54+
self.onToolDone();
4955
}
5056
});
5157

@@ -55,7 +61,13 @@ define("webodf/editor/widgets/toggleLists", [
5561
showLabel: false,
5662
checked: false,
5763
iconClass: "dijitEditorIcon dijitEditorIconInsertUnorderedList",
58-
onChange: function () {
64+
onChange: function (checked) {
65+
var success = listController.setBulletedList(checked);
66+
//TODO: remove this when the list controller supports all use cases triggered by this button
67+
if(!success) {
68+
bulletedList.set("checked", !checked, false);
69+
}
70+
self.onToolDone();
5971
}
6072
});
6173

webodf/lib/gui/ListController.js

Lines changed: 201 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* @source: https://github.com/kogmbh/WebODF/
2323
*/
2424

25-
/*global core, ops, gui, odf, runtime*/
25+
/*global core, ops, gui, odf, NodeFilter, runtime*/
2626

2727
/**
2828
* @implements {core.Destroyable}
@@ -36,14 +36,17 @@ gui.ListController = function ListController(session, sessionConstraints, sessio
3636
"use strict";
3737
var odtDocument = session.getOdtDocument(),
3838
odfUtils = odf.OdfUtils,
39+
domUtils = core.DomUtils,
3940
eventNotifier = new core.EventNotifier([
4041
gui.ListController.listStylingChanged,
4142
gui.ListController.enabledChanged
4243
]),
4344
/**@type{!gui.ListController.SelectionInfo}*/
4445
lastSignalledSelectionInfo,
4546
/**@type{!core.LazyProperty.<!gui.ListController.SelectionInfo>}*/
46-
cachedSelectionInfo;
47+
cachedSelectionInfo,
48+
/**@const*/
49+
NEXT = core.StepDirection.NEXT;
4750

4851
/**
4952
* @param {!ops.OdtCursor|!string} cursorOrId
@@ -136,6 +139,161 @@ gui.ListController = function ListController(session, sessionConstraints, sessio
136139
emitSelectionChanges();
137140
}
138141

142+
/**
143+
* Find all top level text:list elements in the given range.
144+
* This includes any elements that contain the start or end containers of the range.
145+
* @param {!Range} range
146+
* @return {!Array.<!Element>}
147+
*/
148+
function getTopLevelListElementsInRange(range) {
149+
var elements,
150+
topLevelList,
151+
rootNode = odtDocument.getRootNode();
152+
153+
/**
154+
* @param {!Node} node
155+
* @return {!number}
156+
*/
157+
function isListOrListItem(node) {
158+
var result = NodeFilter.FILTER_REJECT;
159+
if (odfUtils.isListElement(node) && !odfUtils.isListItemElement(node.parentNode)) {
160+
result = NodeFilter.FILTER_ACCEPT;
161+
} else if (odfUtils.isTextContentContainingNode(node) || odfUtils.isGroupingElement(node)) {
162+
result = NodeFilter.FILTER_SKIP;
163+
}
164+
return result;
165+
}
166+
167+
// ignore the list element if it is nested within another list
168+
elements = domUtils.getNodesInRange(range, isListOrListItem, NodeFilter.SHOW_ELEMENT);
169+
170+
// add any top level lists that contain the start or end containers of the range
171+
// check in the elements collection for duplicates in case these top level lists intersected the specified range
172+
topLevelList = odfUtils.getTopLevelListElement(/**@type{!Node}*/(range.startContainer), rootNode);
173+
if (topLevelList && topLevelList !== elements[0]) {
174+
elements.unshift(topLevelList);
175+
}
176+
177+
topLevelList = odfUtils.getTopLevelListElement(/**@type{!Node}*/(range.endContainer), rootNode);
178+
if (topLevelList && topLevelList !== elements[elements.length - 1]) {
179+
elements.push(topLevelList);
180+
}
181+
182+
return elements;
183+
}
184+
185+
/**
186+
* @param {!Element} initialParagraph
187+
* @return {!{startParagraph: !Element, endParagraph: !Element}}
188+
*/
189+
function createParagraphGroup(initialParagraph) {
190+
return {
191+
startParagraph: initialParagraph,
192+
endParagraph: initialParagraph
193+
};
194+
}
195+
196+
/**
197+
* Takes all the paragraph elements in the current selection and breaks
198+
* them into add list operations based on their common ancestors. Paragraph elements
199+
* with the same common ancestor will be grouped into the same operation
200+
* @return {!Array.<!ops.Operation>}
201+
*/
202+
function determineOpsForAddingLists() {
203+
var paragraphElements,
204+
/**@type{!Array.<!{startParagraph: !Element, endParagraph: !Element}>}*/
205+
paragraphGroups = [],
206+
paragraphParent,
207+
commonAncestor,
208+
i;
209+
210+
paragraphElements = odfUtils.getParagraphElements(odtDocument.getCursor(inputMemberId).getSelectedRange());
211+
212+
for (i = 0; i < paragraphElements.length; i += 1) {
213+
paragraphParent = paragraphElements[i].parentNode;
214+
215+
//TODO: handle selections that intersect with existing lists
216+
if (odfUtils.isListItemElement(paragraphParent)) {
217+
runtime.log("DEBUG: Current selection intersects with an existing list which is not supported at this time");
218+
paragraphGroups.length = 0;
219+
break;
220+
}
221+
222+
if (paragraphParent === commonAncestor) {
223+
// if the current paragraph has the same common ancestor as the current group of paragraphs
224+
// then the paragraph group gets extended to include the current paragraph
225+
paragraphGroups[paragraphGroups.length - 1].endParagraph = paragraphElements[i];
226+
} else {
227+
// if the ancestor of this paragraph does not match then begin a new group of paragraphs
228+
commonAncestor = paragraphParent;
229+
paragraphGroups.push(createParagraphGroup(paragraphElements[i]));
230+
}
231+
}
232+
233+
// each paragraph group becomes one add list operation
234+
return paragraphGroups.map(function (group) {
235+
// take the first step of the start and end paragraph of each group and
236+
// pass them in as the coordinates for the add list operation
237+
var newOp = new ops.OpAddList();
238+
newOp.init({
239+
memberid: inputMemberId,
240+
startParagraphPosition: odtDocument.convertDomPointToCursorStep(group.startParagraph, 0, NEXT),
241+
endParagraphPosition: odtDocument.convertDomPointToCursorStep(group.endParagraph, 0, NEXT)
242+
});
243+
return newOp;
244+
});
245+
}
246+
247+
/**
248+
* Finds all the lists to be removed in the current selection and creates an operation for each
249+
* top level list element found
250+
* @return {!Array.<!ops.Operation>}
251+
*/
252+
function determineOpsForRemovingLists() {
253+
var topLevelListElements,
254+
stepIterator = odtDocument.createStepIterator(
255+
odtDocument.getRootNode(),
256+
0,
257+
[odtDocument.getPositionFilter()],
258+
odtDocument.getRootNode());
259+
260+
topLevelListElements = getTopLevelListElementsInRange(odtDocument.getCursor(inputMemberId).getSelectedRange());
261+
262+
return topLevelListElements.map(function (listElement) {
263+
var newOp = new ops.OpRemoveList();
264+
265+
stepIterator.setPosition(listElement, 0);
266+
runtime.assert(stepIterator.roundToNextStep(), "Top level list element contains no steps");
267+
268+
newOp.init({
269+
memberid: inputMemberId,
270+
firstParagraphPosition: odtDocument.convertDomPointToCursorStep(stepIterator.container(), stepIterator.offset())
271+
});
272+
return newOp;
273+
});
274+
}
275+
276+
/**
277+
* @param {function():!Array.<!ops.Operation>} executeFunc
278+
* @return {!boolean}
279+
*/
280+
function executeListOperations(executeFunc) {
281+
var newOps;
282+
283+
if (!cachedSelectionInfo.value().isEnabled) {
284+
return false;
285+
}
286+
287+
newOps = executeFunc();
288+
289+
if (newOps.length > 0) {
290+
session.enqueue(newOps);
291+
return true;
292+
}
293+
294+
return false;
295+
}
296+
139297
/**
140298
* @return {!boolean}
141299
*/
@@ -161,6 +319,47 @@ gui.ListController = function ListController(session, sessionConstraints, sessio
161319
eventNotifier.unsubscribe(eventid, cb);
162320
};
163321

322+
/**
323+
* @return {!boolean}
324+
*/
325+
function makeList() {
326+
return executeListOperations(determineOpsForAddingLists);
327+
}
328+
329+
this.makeList = makeList;
330+
331+
/**
332+
* @return {!boolean}
333+
*/
334+
function removeList() {
335+
return executeListOperations(determineOpsForRemovingLists);
336+
}
337+
338+
this.removeList = removeList;
339+
340+
/**
341+
* @param {!boolean} checked
342+
* @return {!boolean}
343+
*/
344+
this.setNumberedList = function (checked) {
345+
if (checked) {
346+
return makeList();
347+
}
348+
return removeList();
349+
350+
};
351+
352+
/**
353+
* @param {!boolean} checked
354+
* @return {!boolean}
355+
*/
356+
this.setBulletedList = function (checked) {
357+
if (checked) {
358+
return makeList();
359+
}
360+
return removeList();
361+
};
362+
164363
/**
165364
* @param {!function(!Error=)} callback
166365
* @return {undefined}

webodf/lib/manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,9 @@
412412
"ops.OpAddCursor": [
413413
"ops.OdtDocument"
414414
],
415+
"ops.OpAddList": [
416+
"ops.OdtDocument"
417+
],
415418
"ops.OpAddMember": [
416419
"ops.OdtDocument"
417420
],
@@ -453,6 +456,9 @@
453456
"ops.OpRemoveHyperlink": [
454457
"ops.OdtDocument"
455458
],
459+
"ops.OpRemoveList": [
460+
"ops.OdtDocument"
461+
],
456462
"ops.OpRemoveMember": [
457463
"ops.OdtDocument"
458464
],
@@ -487,6 +493,7 @@
487493
"ops.OperationFactory": [
488494
"ops.OpAddAnnotation",
489495
"ops.OpAddCursor",
496+
"ops.OpAddList",
490497
"ops.OpAddMember",
491498
"ops.OpAddStyle",
492499
"ops.OpApplyDirectStyling",
@@ -500,6 +507,7 @@
500507
"ops.OpRemoveBlob",
501508
"ops.OpRemoveCursor",
502509
"ops.OpRemoveHyperlink",
510+
"ops.OpRemoveList",
503511
"ops.OpRemoveMember",
504512
"ops.OpRemoveStyle",
505513
"ops.OpRemoveText",

webodf/lib/odf/OdfUtils.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,30 @@ odf.OdfUtilsImpl = function OdfUtilsImpl() {
10661066
return fontFamilyName;
10671067
};
10681068
/*jslint regexp: false*/
1069+
1070+
/**
1071+
* Finds the top level text:list element for a given node
1072+
* @param {!Node} node
1073+
* @param {!Element} container Root container to stop searching at.
1074+
* @return {?Element}
1075+
*/
1076+
this.getTopLevelListElement = function(node, container) {
1077+
var currentNode = node,
1078+
listNode = null;
1079+
1080+
while(currentNode) {
1081+
if(isListElement(currentNode)) {
1082+
listNode = /**@type{!Element}*/(currentNode);
1083+
}
1084+
1085+
if(currentNode === container) {
1086+
break;
1087+
}
1088+
currentNode = currentNode.parentNode;
1089+
}
1090+
1091+
return listNode;
1092+
};
10691093
};
10701094

10711095
/**

0 commit comments

Comments
 (0)