Skip to content
This repository was archived by the owner on Feb 6, 2023. It is now read-only.

Commit e145a2a

Browse files
niveditcfacebook-github-bot
authored andcommitted
Implement moveChildUp operation for untab
Summary: Implements a `moveChildUp` method to help with untab (`onTab` with shift key) in `NestedRichTextEditorUtil`. This will move the first/last child up as the previous / next sibling of its parent. These are the operations that are implemented (and they work as the test cases): {F137866518} Differential Revision: D9672458 fbshipit-source-id: f2ba306ae56d243071232f04bef2746c4a060dfb
1 parent 8c373b6 commit e145a2a

File tree

3 files changed

+1253
-0
lines changed

3 files changed

+1253
-0
lines changed

src/model/modifier/exploration/DraftTreeOperations.js

+145
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,155 @@ const updateAsSiblingsChild = (
289289
return newBlockMap;
290290
};
291291

292+
/**
293+
* This is a utility method that abstracts the operation of moving a node up to become
294+
* a sibling of its parent. If the operation results in a parent with no children,
295+
* also delete the parent node.
296+
*
297+
* Can only operate on the first or last child (this is an invariant)
298+
*
299+
* This operation respects the tree data invariants - it expects and returns a
300+
* valid tree.
301+
*/
302+
const moveChildUp = (blockMap: BlockMap, key: string): BlockMap => {
303+
verifyTree(blockMap);
304+
const block = blockMap.get(key);
305+
invariant(block != null, 'block must exist in block map');
306+
307+
// if there is no parent, do nothing
308+
const parentKey = block.getParentKey();
309+
if (parentKey == null) {
310+
return blockMap;
311+
}
312+
313+
let parent = blockMap.get(parentKey);
314+
invariant(parent !== null, 'parent must exist in block map');
315+
let newBlockMap = blockMap;
316+
const childIndex = parent.getChildKeys().indexOf(key);
317+
invariant(
318+
childIndex === 0 || childIndex === parent.getChildKeys().count() - 1,
319+
'block is not first or last child of its parent',
320+
);
321+
322+
// If it's the first child, move as previous sibling of parent
323+
if (childIndex === 0) {
324+
const parentPrevSibling = parent.getPrevSiblingKey();
325+
newBlockMap = updateSibling(newBlockMap, key, parentKey);
326+
// link to parent's previous sibling
327+
if (parentPrevSibling != null) {
328+
newBlockMap = updateSibling(newBlockMap, parentPrevSibling, key);
329+
}
330+
// remove as parent's child
331+
parent = newBlockMap.get(parentKey);
332+
newBlockMap = newBlockMap.set(
333+
parentKey,
334+
parent.merge({
335+
children: parent.getChildKeys().slice(1),
336+
}),
337+
);
338+
parent = newBlockMap.get(parentKey);
339+
// remove as previous sibling of parent's children
340+
if (parent.getChildKeys().count() > 0) {
341+
const firstChildKey = parent.getChildKeys().first();
342+
const firstChild = newBlockMap.get(firstChildKey);
343+
newBlockMap = newBlockMap.set(
344+
firstChildKey,
345+
firstChild.merge({prevSibling: null}),
346+
);
347+
}
348+
// add the node just before its former parent in the block map
349+
newBlockMap = newBlockMap
350+
.takeUntil(block => block.getKey() === parentKey)
351+
.concat(
352+
Immutable.OrderedMap([
353+
[key, newBlockMap.get(key)],
354+
[parentKey, newBlockMap.get(parentKey)],
355+
]),
356+
)
357+
.concat(newBlockMap.skipUntil(block => block.getKey() === key).slice(1));
358+
359+
// If it's the last child, move as next sibling of parent
360+
} else if (childIndex === parent.getChildKeys().count() - 1) {
361+
const parentNextSibling = parent.getNextSiblingKey();
362+
newBlockMap = updateSibling(newBlockMap, parentKey, key);
363+
// link to parent's next sibling
364+
if (parentNextSibling != null) {
365+
newBlockMap = updateSibling(newBlockMap, key, parentNextSibling);
366+
}
367+
// remove as parent's child
368+
parent = newBlockMap.get(parentKey);
369+
newBlockMap = newBlockMap.set(
370+
parentKey,
371+
parent.merge({
372+
children: parent.getChildKeys().slice(0, -1),
373+
}),
374+
);
375+
parent = newBlockMap.get(parentKey);
376+
// remove as next sibling of parent's children
377+
if (parent.getChildKeys().count() > 0) {
378+
const lastChildKey = parent.getChildKeys().last();
379+
const lastChild = newBlockMap.get(lastChildKey);
380+
newBlockMap = newBlockMap.set(
381+
lastChildKey,
382+
lastChild.merge({nextSibling: null}),
383+
);
384+
}
385+
}
386+
387+
// For both cases, also link to parent's parent
388+
const grandparentKey = parent.getParentKey();
389+
if (grandparentKey != null) {
390+
const grandparentInsertPosition = newBlockMap
391+
.get(grandparentKey)
392+
.getChildKeys()
393+
.findIndex(n => n === parentKey);
394+
newBlockMap = updateParentChild(
395+
newBlockMap,
396+
grandparentKey,
397+
key,
398+
childIndex === 0
399+
? grandparentInsertPosition
400+
: grandparentInsertPosition + 1,
401+
);
402+
} else {
403+
newBlockMap = newBlockMap.set(
404+
key,
405+
newBlockMap.get(key).merge({parent: null}),
406+
);
407+
}
408+
409+
// Delete parent if it has no children
410+
parent = newBlockMap.get(parentKey);
411+
if (parent.getChildKeys().count() === 0) {
412+
const prevSiblingKey = parent.getPrevSiblingKey();
413+
const nextSiblingKey = parent.getNextSiblingKey();
414+
if (prevSiblingKey != null && nextSiblingKey != null) {
415+
newBlockMap = updateSibling(newBlockMap, prevSiblingKey, nextSiblingKey);
416+
}
417+
if (prevSiblingKey == null && nextSiblingKey != null) {
418+
newBlockMap = newBlockMap.set(
419+
nextSiblingKey,
420+
newBlockMap.get(nextSiblingKey).merge({prevSibling: null}),
421+
);
422+
}
423+
if (nextSiblingKey == null && prevSiblingKey != null) {
424+
newBlockMap = newBlockMap.set(
425+
prevSiblingKey,
426+
newBlockMap.get(prevSiblingKey).merge({nextSibling: null}),
427+
);
428+
}
429+
newBlockMap = newBlockMap.delete(parentKey);
430+
}
431+
432+
verifyTree(newBlockMap);
433+
return newBlockMap;
434+
};
435+
292436
module.exports = {
293437
updateParentChild,
294438
replaceParentChild,
295439
updateSibling,
296440
createNewParent,
297441
updateAsSiblingsChild,
442+
moveChildUp,
298443
};

src/model/modifier/exploration/__tests__/DraftTreeOperations-test.js

+164
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,167 @@ test("test creating a updating node to become previous sibling's child 2", () =>
380380
DraftTreeOperations.updateAsSiblingsChild(blockMap6, 'C', 'previous'),
381381
).toMatchSnapshot();
382382
});
383+
384+
test('test moving child up with no parent - no-op', () => {
385+
expect(DraftTreeOperations.moveChildUp(blockMap3, 'B')).toMatchSnapshot();
386+
});
387+
388+
test('test moving first child up 1', () => {
389+
expect(DraftTreeOperations.moveChildUp(blockMap1, 'B')).toMatchSnapshot();
390+
});
391+
392+
const blockMap7 = Immutable.OrderedMap({
393+
X: new ContentBlockNode({
394+
key: 'X',
395+
parent: null,
396+
text: '',
397+
children: Immutable.List(['A', 'Y']),
398+
prevSibling: null,
399+
nextSibling: null,
400+
}),
401+
A: new ContentBlockNode({
402+
key: 'A',
403+
parent: 'X',
404+
text: 'alpha',
405+
children: Immutable.List([]),
406+
prevSibling: null,
407+
nextSibling: 'Y',
408+
}),
409+
Y: new ContentBlockNode({
410+
key: 'Y',
411+
parent: 'X',
412+
text: '',
413+
children: Immutable.List(['B', 'C']),
414+
prevSibling: 'A',
415+
nextSibling: null,
416+
}),
417+
B: new ContentBlockNode({
418+
key: 'B',
419+
parent: 'Y',
420+
text: 'beta',
421+
children: Immutable.List([]),
422+
prevSibling: null,
423+
nextSibling: 'C',
424+
}),
425+
C: new ContentBlockNode({
426+
key: 'C',
427+
parent: 'Y',
428+
text: 'charlie',
429+
children: Immutable.List([]),
430+
prevSibling: 'B',
431+
nextSibling: null,
432+
}),
433+
});
434+
435+
test('test moving first child up 2', () => {
436+
expect(DraftTreeOperations.moveChildUp(blockMap7, 'B')).toMatchSnapshot();
437+
});
438+
439+
test('test moving last child up 1', () => {
440+
expect(DraftTreeOperations.moveChildUp(blockMap1, 'C')).toMatchSnapshot();
441+
});
442+
443+
test('test moving last child up 2', () => {
444+
expect(DraftTreeOperations.moveChildUp(blockMap7, 'C')).toMatchSnapshot();
445+
});
446+
447+
const blockMap8 = Immutable.OrderedMap({
448+
A: new ContentBlockNode({
449+
key: 'A',
450+
parent: null,
451+
text: 'alpha',
452+
children: Immutable.List([]),
453+
prevSibling: null,
454+
nextSibling: 'X',
455+
}),
456+
X: new ContentBlockNode({
457+
key: 'X',
458+
parent: null,
459+
text: '',
460+
children: Immutable.List(['B']),
461+
prevSibling: 'A',
462+
nextSibling: 'C',
463+
}),
464+
B: new ContentBlockNode({
465+
key: 'B',
466+
parent: 'X',
467+
text: 'beta',
468+
children: Immutable.List([]),
469+
prevSibling: null,
470+
nextSibling: null,
471+
}),
472+
C: new ContentBlockNode({
473+
key: 'C',
474+
parent: null,
475+
text: 'charlie',
476+
children: Immutable.List([]),
477+
prevSibling: 'X',
478+
nextSibling: null,
479+
}),
480+
});
481+
482+
test('test moving only child up deletes parent 1', () => {
483+
expect(DraftTreeOperations.moveChildUp(blockMap8, 'B')).toMatchSnapshot();
484+
});
485+
486+
const blockMap9 = Immutable.OrderedMap({
487+
X: new ContentBlockNode({
488+
key: 'X',
489+
parent: null,
490+
text: '',
491+
children: Immutable.List(['B']),
492+
prevSibling: null,
493+
nextSibling: 'C',
494+
}),
495+
B: new ContentBlockNode({
496+
key: 'B',
497+
parent: 'X',
498+
text: 'beta',
499+
children: Immutable.List([]),
500+
prevSibling: null,
501+
nextSibling: null,
502+
}),
503+
C: new ContentBlockNode({
504+
key: 'C',
505+
parent: null,
506+
text: 'charlie',
507+
children: Immutable.List([]),
508+
prevSibling: 'X',
509+
nextSibling: null,
510+
}),
511+
});
512+
513+
test('test moving only child up deletes parent 2', () => {
514+
expect(DraftTreeOperations.moveChildUp(blockMap9, 'B')).toMatchSnapshot();
515+
});
516+
517+
const blockMap10 = Immutable.OrderedMap({
518+
A: new ContentBlockNode({
519+
key: 'A',
520+
parent: null,
521+
text: 'alpha',
522+
children: Immutable.List([]),
523+
prevSibling: null,
524+
nextSibling: 'X',
525+
}),
526+
X: new ContentBlockNode({
527+
key: 'X',
528+
parent: null,
529+
text: '',
530+
children: Immutable.List(['B']),
531+
prevSibling: 'A',
532+
nextSibling: null,
533+
}),
534+
B: new ContentBlockNode({
535+
key: 'B',
536+
parent: 'X',
537+
text: 'beta',
538+
children: Immutable.List([]),
539+
prevSibling: null,
540+
nextSibling: null,
541+
}),
542+
});
543+
544+
test('test moving only child up deletes parent 3', () => {
545+
expect(DraftTreeOperations.moveChildUp(blockMap10, 'B')).toMatchSnapshot();
546+
});

0 commit comments

Comments
 (0)