Skip to content

Commit 54f56cc

Browse files
committed
Refactor deleteRange
Also refactor `cutSection`, change it to accept positions rather than offsets. Remove `splitMarker` (now-unused). Fix one incorrect acceptance test for list items. Add more unit tests for `deleteRange`.
1 parent 93a5709 commit 54f56cc

File tree

5 files changed

+121
-299
lines changed

5 files changed

+121
-299
lines changed

src/js/editor/post.js

Lines changed: 71 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -78,197 +78,106 @@ class PostEditor {
7878
* ```
7979
* let { range } = editor;
8080
* editor.run((postEditor) => {
81-
* postEditor.deleteRange(range);
81+
* let nextPosition = postEditor.deleteRange(range);
82+
* postEditor.setRange(new Range(nextPosition));
8283
* });
8384
* ```
8485
* @param {Range} range Cursor Range object with head and tail Positions
8586
* @return {Position} The position where the cursor would go after deletion
8687
* @public
8788
*/
8889
deleteRange(range) {
89-
// types of selection deletion:
90-
// * a selection starts at the beginning of a section
91-
// -- cursor should end up at the beginning of that section
92-
// -- if the section not longer has markers, add a blank one for the cursor to focus on
93-
// * a selection is entirely within a section
94-
// -- split the markers with the selection, remove those new markers from their section
95-
// -- cursor goes at end of the marker before the selection start, or if the
96-
// -- selection was at the start of the section, cursor goes at section start
97-
// * a selection crosses multiple sections
98-
// -- remove all the sections that are between (exclusive) selection start and end
99-
// -- join the start and end sections
100-
// -- mark the end section for removal
101-
// -- cursor goes at end of marker before the selection start
102-
103-
if (range.isCollapsed) {
104-
return range.head;
105-
}
106-
10790
let {
108-
head, head: {section: headSection, offset: headSectionOffset},
109-
tail, tail: {section: tailSection, offset: tailSectionOffset}
91+
head, head: {section: headSection},
92+
tail, tail: {section: tailSection}
11093
} = range;
111-
const { post } = this.editor;
11294

113-
let nextPosition;
95+
let { editor: { post } } = this;
11496

11597
if (headSection === tailSection) {
116-
nextPosition = this.cutSection(headSection, headSectionOffset, tailSectionOffset);
117-
} else {
118-
let removedSections = [];
119-
let appendSection = headSection;
120-
121-
let recursiveRangeToDelete = null;
98+
return this.cutSection(headSection, head, tail);
99+
}
122100

123-
post.walkLeafSections(range, section => {
124-
if (recursiveRangeToDelete) {
125-
// avoid walking the entire post if we will recurse
126-
return;
127-
}
101+
let nextSection = headSection.nextLeafSection();
128102

129-
switch (section) {
130-
case headSection:
131-
if (section.isCardSection) {
132-
// Create a section to join tailSection with if necessary
133-
appendSection = this.builder.createMarkupSection();
134-
135-
if (head.isHead()) {
136-
// Remove he entire card because we are deleting from its head
137-
// position into section(s) after it
138-
removedSections.push(section);
139-
nextPosition = appendSection.headPosition();
140-
} else {
141-
// Set the position to be the card's tail
142-
nextPosition = section.tailPosition();
143-
}
144-
} else if (section.isBlank) {
145-
// Grab the next leaf section before removing blank head
146-
let nextHeadSection = section.nextLeafSection();
147-
148-
// Remove the blank head section now
149-
this.removeSection(section);
150-
151-
// Advance the range and recursively delete it
152-
recursiveRangeToDelete = new Range(nextHeadSection.headPosition(), range.tail);
153-
} else {
154-
nextPosition = this.cutSection(section, headSectionOffset, section.length);
155-
}
156-
break;
157-
case tailSection:
158-
if (section.isCardSection) {
159-
if (tail.isTail()) {
160-
// If the range extends to the end of the tail section, remove it
161-
removedSections.push(section);
162-
}
163-
} else {
164-
165-
// join the tail section's markers (afer `tailSectionOffset`) to
166-
// the appendSection
167-
section.markersFor(tailSectionOffset, section.text.length).forEach(marker => {
168-
appendSection.markers.append(marker);
169-
});
170-
171-
// If we've modified headSection, ensure it is marked dirty so that it is re-rendered.
172-
// If appendSection !== headSection, it's a new section that will be rendered regardless.
173-
if (appendSection === headSection) {
174-
this._markDirty(headSection);
175-
}
176-
177-
removedSections.push(section);
178-
}
179-
break;
180-
default:
181-
// Default is to remove sections in between head and tail sections
182-
removedSections.push(section);
183-
}
184-
});
103+
let nextPos = this.cutSection(headSection, head, headSection.tailPosition());
104+
// cutSection can replace the section, so re-read headSection here
105+
headSection = nextPos.section;
185106

186-
if (recursiveRangeToDelete) {
187-
// If we are recursing, return early here
188-
return this.deleteRange(recursiveRangeToDelete);
189-
}
107+
// Remove sections in the middle of the range
108+
while (nextSection !== tailSection) {
109+
let tmp = nextSection;
110+
nextSection = nextSection.nextLeafSection();
111+
this.removeSection(tmp);
112+
}
190113

191-
removedSections.forEach(section => this.removeSection(section) );
114+
let tailPos = this.cutSection(tailSection, tailSection.headPosition(), tail);
115+
// cutSection can replace the section, so re-read tailSection here
116+
tailSection = tailPos.section;
192117

193-
if (headSection !== appendSection) {
194-
if (!appendSection.isBlank) {
195-
// if the appendSection is not blank, make sure to add it
196-
this.insertSectionBefore(post.sections, appendSection, headSection.next);
197-
} else if (post.isBlank) {
198-
// if we've removed all the sections from the post, add the blank append section
199-
this.insertSectionBefore(post.sections, appendSection, headSection.next);
200-
}
118+
if (tailSection.isBlank) {
119+
this.removeSection(tailSection);
120+
} else {
121+
// If head and tail sections are markerable, join them
122+
// Note: They may not be the same section type. E.g. this may join
123+
// a tail section that was a list item onto a markup section, or vice versa.
124+
// (This is the desired behavior.)
125+
if (headSection.isMarkerable && tailSection.isMarkerable) {
126+
headSection.join(tailSection);
127+
this._markDirty(headSection);
128+
this.removeSection(tailSection);
129+
} else if (headSection.isBlank) {
130+
this.removeSection(headSection);
131+
nextPos = tailPos;
201132
}
202133
}
203134

204-
return nextPosition;
135+
if (post.isBlank) {
136+
post.sections.append(this.builder.createMarkupSection('p'));
137+
nextPos = post.headPosition();
138+
}
139+
140+
return nextPos;
205141
}
206142

207143
/**
144+
* Note: This method may replace `section` with a different section.
145+
*
208146
* "Cut" out the part of the section inside `headOffset` and `tailOffset`.
209-
* If the section is markerable this removes markers that are wholly inside the offsets,
210-
* and splits markers that straddle the head or tail.
147+
* If section is markerable this splits markers that straddle the head or tail (if necessary),
148+
* and removes markers that are wholly inside the offsets.
149+
* If section is a card, this may replace it with a blank markup section if the
150+
* positions contain the entire card.
151+
*
211152
* @param {Section} section
212-
* @param {Number} headOffset
213-
* @param {Number} tailOffset
153+
* @param {Position} head
154+
* @param {Position} tail
214155
* @return {Position}
215156
* @private
216157
*/
217-
cutSection(section, headOffset, tailOffset) {
218-
if (section.isBlank || headOffset === tailOffset) {
219-
return new Position(section, headOffset);
158+
cutSection(section, head, tail) {
159+
assert('Must pass head position and tail position to `cutSection`',
160+
head instanceof Position && tail instanceof Position);
161+
assert('Must pass positions within same section to `cutSection`',
162+
head.section === tail.section);
163+
164+
if (section.isBlank || head.isEqual(tail)) {
165+
return head;
220166
}
221167
if (section.isCardSection) {
222-
let newSection = this.builder.createMarkupSection();
223-
this.replaceSection(section, newSection);
224-
return newSection.headPosition();
225-
}
226-
227-
let adjustedHead = 0,
228-
marker = section.markers.head,
229-
adjustedTail = marker.length;
230-
231-
// Walk to the first node inside the headOffset, splitting
232-
// a marker if needed. Leave marker as the first node inside.
233-
while (marker) {
234-
if (adjustedTail >= headOffset) {
235-
let splitOffset = headOffset - adjustedHead;
236-
let { afterMarker } = this.splitMarker(marker, splitOffset);
237-
adjustedHead = adjustedHead + splitOffset;
238-
// FIXME: That these two loops cannot agree on adjustedTail being
239-
// incremented at the start or end seems prime for refactoring.
240-
adjustedTail = adjustedHead;
241-
marker = afterMarker;
242-
break;
243-
}
244-
adjustedHead += marker.length;
245-
marker = marker.next;
246-
if (marker) {
247-
adjustedTail += marker.length;
168+
if (head.isHead() && tail.isTail()) {
169+
let newSection = this.builder.createMarkupSection();
170+
this.replaceSection(section, newSection);
171+
return newSection.headPosition();
172+
} else {
173+
return tail;
248174
}
249175
}
250176

251-
// Walk each marker inside, removing it if needed. when the last is
252-
// reached split it and remove the part inside the tailOffset
253-
while (marker) {
254-
adjustedTail += marker.length;
255-
if (adjustedTail >= tailOffset) {
256-
let splitOffset = marker.length - (adjustedTail - tailOffset);
257-
let {
258-
beforeMarker
259-
} = this.splitMarker(marker, splitOffset);
260-
if (beforeMarker) {
261-
this.removeMarker(beforeMarker);
262-
}
263-
break;
264-
}
265-
adjustedHead += marker.length;
266-
let nextMarker = marker.next;
267-
this.removeMarker(marker);
268-
marker = nextMarker;
269-
}
177+
let range = new Range(head, tail);
178+
this.splitMarkers(range).forEach(m => this.removeMarker(m));
270179

271-
return new Position(section, headOffset);
180+
return head;
272181
}
273182

274183
_coalesceMarkers(section) {
@@ -469,18 +378,17 @@ class PostEditor {
469378

470379
/**
471380
* Split markers at two positions, once at the head, and if necessary once
472-
* at the tail. This method is designed to accept a range
473-
* (e.g. `editor.range`) as an argument.
381+
* at the tail.
474382
*
475383
* Usage:
476384
* ```
477-
* let markerRange = this.cursor.offsets;
385+
* let range = editor.range;
478386
* editor.run((postEditor) => {
479-
* postEditor.splitMarkers(markerRange);
387+
* postEditor.splitMarkers(range);
480388
* });
481389
* ```
482390
* The return value will be marker object completely inside the offsets
483-
* provided. Markers on the outside of the split may also have been modified.
391+
* provided. Markers outside of the split may also have been modified.
484392
*
485393
* @param {Range} markerRange
486394
* @return {Array} of markers that are inside the split
@@ -501,29 +409,6 @@ class PostEditor {
501409
edit.removed.forEach(m => this.removeMarker(m));
502410
}
503411

504-
splitMarker(marker, offset) {
505-
let beforeMarker, afterMarker;
506-
507-
if (offset === 0) {
508-
beforeMarker = marker.prev;
509-
afterMarker = marker;
510-
} else if (offset === marker.length) {
511-
beforeMarker = marker;
512-
afterMarker = marker.next;
513-
} else {
514-
let { builder } = this.editor,
515-
{ section } = marker;
516-
beforeMarker = builder.createMarker(marker.value.substring(0, offset), marker.markups);
517-
afterMarker = builder.createMarker(marker.value.substring(offset, marker.length), marker.markups);
518-
section.markers.splice(marker, 1, [beforeMarker, afterMarker]);
519-
520-
this.removeMarker(marker);
521-
this._markDirty(section);
522-
}
523-
524-
return { beforeMarker, afterMarker };
525-
}
526-
527412
/**
528413
* Split the section at the position.
529414
*
@@ -541,7 +426,7 @@ class PostEditor {
541426
* headMarkerOffset is 0.
542427
*
543428
* @param {Position} position
544-
* @return {Array} new sections, one for the first half and one for the second
429+
* @return {Array} new sections, one for the first half and one for the second (either one can be null)
545430
* @public
546431
*/
547432
splitSection(position) {

tests/acceptance/editor-list-test.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ test('forward-delete in empty li with li after it joins with li', (assert) => {
304304
assert.hasElement('#editor li:contains(Xabc)', 'inserts text at right spot');
305305
});
306306

307-
test('forward-delete in empty li with markup section after it deletes li', (assert) => {
307+
test('forward-delete in empty li with markup section after it joins markup section', (assert) => {
308308
const mobiledoc = Helpers.mobiledoc.build(builder => {
309309
const {post, listSection, listItem, markupSection, marker} = builder;
310310
return post([
@@ -319,13 +319,12 @@ test('forward-delete in empty li with markup section after it deletes li', (asse
319319
Helpers.dom.moveCursorTo(editor, node, 0);
320320
Helpers.dom.triggerForwardDelete(editor);
321321

322-
assert.hasNoElement('#editor li', 'li is removed');
323-
assert.hasElement('#editor p:contains(abc)', 'p remains');
322+
assert.hasElement('#editor li:contains(abc)', 'joins markup section');
323+
assert.hasNoElement('#editor p', 'p is removed');
324324

325325
Helpers.dom.insertText(editor, 'X');
326326

327-
assert.hasElement('#editor p:contains(Xabc)', 'inserts text at right spot');
328-
327+
assert.hasElement('#editor li:contains(Xabc)', 'inserts text at right spot');
329328
});
330329

331330
test('forward-delete end of li with nothing after', (assert) => {

0 commit comments

Comments
 (0)