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

Commit 77e6844

Browse files
niveditcfacebook-github-bot
authored andcommitted
Implement untab operations
Summary: This diff implements the `unTab` operation for nested lists. It covers the following four cases (refer to the diagrams below for a better explanation): 1. Block is not nested => do nothing 2. Block is first child => move as previous sibling of parent 3. Block is last child => move as next sibling of parent 4. Block is neither the first nor the last child => split the block at the child, keep previous children on original parent, move block as next sibling of parent & add the new block with the next children as its next sibling. 1 - 3 => {F138170258} 4 => {F138170261} **TODO:** Still figuring out the correct paradigm for dealing with the block's children when the first nested block is un-nested & will add behaviors + test cases for that in the next diff. Reviewed By: vdurmont Differential Revision: D9757713 fbshipit-source-id: 01796b003acbfcfcc6fa8d5f6f4576d4f2a13ec6
1 parent 8bb9c6c commit 77e6844

File tree

3 files changed

+1588
-6
lines changed

3 files changed

+1588
-6
lines changed

src/model/modifier/exploration/NestedRichTextEditorUtil.js

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import type {DataObjectForLink, RichTextUtils} from 'RichTextUtils';
2020
import type SelectionState from 'SelectionState';
2121
import type URI from 'URI';
2222

23+
const ContentBlockNode = require('ContentBlockNode');
2324
const DraftModifier = require('DraftModifier');
2425
const DraftTreeOperations = require('DraftTreeOperations');
2526
const EditorState = require('EditorState');
2627
const RichTextEditorUtil = require('RichTextEditorUtil');
2728

2829
const adjustBlockDepthForContentState = require('adjustBlockDepthForContentState');
30+
const generateRandomKey = require('generateRandomKey');
31+
const invariant = require('invariant');
2932

3033
// Eventually we could allow to control this list by either allowing user configuration
3134
// and/or a schema in conjunction to DraftBlockRenderMap
@@ -307,10 +310,10 @@ const NestedRichTextEditorUtil: RichTextUtils = {
307310
}
308311

309312
// implement nested tree behaviour for onTab
313+
let blockMap = editorState.getCurrentContent().getBlockMap();
314+
const prevSiblingKey = block.getPrevSiblingKey();
315+
const nextSiblingKey = block.getNextSiblingKey();
310316
if (!event.shiftKey) {
311-
let blockMap = editorState.getCurrentContent().getBlockMap();
312-
const prevSiblingKey = block.getPrevSiblingKey();
313-
const nextSiblingKey = block.getNextSiblingKey();
314317
// if there is no previous sibling, we do nothing
315318
if (prevSiblingKey == null) {
316319
return editorState;
@@ -344,10 +347,82 @@ const NestedRichTextEditorUtil: RichTextUtils = {
344347
} else {
345348
blockMap = DraftTreeOperations.createNewParent(blockMap, key);
346349
}
347-
content = editorState.getCurrentContent().merge({
348-
blockMap: blockMap,
349-
});
350+
// on un-tab
351+
} else {
352+
// if the block isn't nested, do nothing
353+
const parentKey = block.getParentKey();
354+
if (parentKey == null) {
355+
return editorState;
356+
}
357+
const parent = blockMap.get(parentKey);
358+
const existingChildren = parent.getChildKeys();
359+
const blockIndex = existingChildren.indexOf(key);
360+
if (blockIndex === 0 || blockIndex === existingChildren.count() - 1) {
361+
blockMap = DraftTreeOperations.moveChildUp(blockMap, key);
362+
} else {
363+
// split the block into [0, blockIndex] in parent & the rest in a new block
364+
const prevChildren = existingChildren.slice(0, blockIndex + 1);
365+
const nextChildren = existingChildren.slice(blockIndex + 1);
366+
blockMap = blockMap.set(
367+
parentKey,
368+
parent.merge({children: prevChildren}),
369+
);
370+
const newBlock = new ContentBlockNode({
371+
key: generateRandomKey(),
372+
text: '',
373+
depth: parent.getDepth(),
374+
type: parent.getType(),
375+
children: nextChildren,
376+
parent: parent.getParentKey(),
377+
});
378+
// add new block just before its the original next sibling in the block map
379+
// TODO(T33894878): Remove the map reordering code & fix converter after launch
380+
invariant(
381+
nextSiblingKey != null,
382+
'block must have a next sibling here',
383+
);
384+
const blocks = blockMap.toSeq();
385+
blockMap = blocks
386+
.takeUntil(block => block.getKey() === nextSiblingKey)
387+
.concat(
388+
[[newBlock.getKey(), newBlock]],
389+
blocks.skipUntil(block => block.getKey() === nextSiblingKey),
390+
)
391+
.toOrderedMap();
392+
393+
// set the nextChildren's parent to the new block
394+
blockMap = blockMap.map(
395+
block =>
396+
nextChildren.includes(block.getKey())
397+
? block.merge({parent: newBlock.getKey()})
398+
: block,
399+
);
400+
// update the next/previous pointers for the children at the split
401+
blockMap = blockMap
402+
.set(key, block.merge({nextSibling: null}))
403+
.set(
404+
nextSiblingKey,
405+
blockMap.get(nextSiblingKey).merge({prevSibling: null}),
406+
);
407+
const parentNextSiblingKey = parent.getNextSiblingKey();
408+
if (parentNextSiblingKey != null) {
409+
blockMap = DraftTreeOperations.updateSibling(
410+
blockMap,
411+
newBlock.getKey(),
412+
parentNextSiblingKey,
413+
);
414+
}
415+
blockMap = DraftTreeOperations.updateSibling(
416+
blockMap,
417+
parentKey,
418+
newBlock.getKey(),
419+
);
420+
blockMap = DraftTreeOperations.moveChildUp(blockMap, key);
421+
}
350422
}
423+
content = editorState.getCurrentContent().merge({
424+
blockMap: blockMap,
425+
});
351426

352427
const withAdjustment = adjustBlockDepthForContentState(
353428
content,

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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,125 @@ test('onTab when siblings are at the same depth creates a new parent', () => {
466466
);
467467
});
468468

469+
test('onTab (untab) on a block with no parent does nothing', () => {
470+
assertNestedUtilOperation(
471+
editorState =>
472+
onTab({preventDefault: () => {}, shiftKey: true}, editorState, 1),
473+
{
474+
anchorKey: 'B',
475+
focusKey: 'B',
476+
},
477+
contentBlockNodes2,
478+
);
479+
});
480+
481+
test('onTab (untab) on a first child moves block as previous sibling of parent', () => {
482+
assertNestedUtilOperation(
483+
editorState =>
484+
onTab({preventDefault: () => {}, shiftKey: true}, editorState, 2),
485+
{
486+
anchorKey: 'D',
487+
focusKey: 'D',
488+
},
489+
contentBlockNodes2,
490+
);
491+
});
492+
493+
test('onTab (untab) on a last child moves block as next sibling of parent', () => {
494+
assertNestedUtilOperation(
495+
editorState =>
496+
onTab({preventDefault: () => {}, shiftKey: true}, editorState, 2),
497+
{
498+
anchorKey: 'H',
499+
focusKey: 'H',
500+
},
501+
contentBlockNodes2,
502+
);
503+
});
504+
505+
const contentBlockNodes3 = [
506+
new ContentBlockNode({
507+
key: 'A',
508+
nextSibling: 'X',
509+
text: 'alpha',
510+
type: 'ordered-list-item',
511+
children: List([]),
512+
}),
513+
new ContentBlockNode({
514+
key: 'X',
515+
prevSibling: 'A',
516+
nextSibling: 'G',
517+
text: '',
518+
type: 'ordered-list-item',
519+
children: List(['B', 'C', 'D', 'E', 'F']),
520+
}),
521+
new ContentBlockNode({
522+
key: 'B',
523+
parent: 'X',
524+
prevSibling: null,
525+
nextSibling: 'C',
526+
text: 'beta',
527+
type: 'ordered-list-item',
528+
children: List([]),
529+
}),
530+
new ContentBlockNode({
531+
key: 'C',
532+
parent: 'X',
533+
prevSibling: 'B',
534+
nextSibling: 'D',
535+
text: 'charlie',
536+
type: 'ordered-list-item',
537+
children: List([]),
538+
}),
539+
new ContentBlockNode({
540+
key: 'D',
541+
parent: 'X',
542+
prevSibling: 'C',
543+
nextSibling: 'E',
544+
text: 'delta',
545+
type: 'ordered-list-item',
546+
children: List([]),
547+
}),
548+
new ContentBlockNode({
549+
key: 'E',
550+
parent: 'X',
551+
prevSibling: 'D',
552+
nextSibling: 'F',
553+
text: 'epsilon',
554+
type: 'ordered-list-item',
555+
children: List([]),
556+
}),
557+
new ContentBlockNode({
558+
key: 'F',
559+
parent: 'X',
560+
prevSibling: 'E',
561+
nextSibling: null,
562+
text: 'foo',
563+
type: 'ordered-list-item',
564+
children: List([]),
565+
}),
566+
new ContentBlockNode({
567+
key: 'G',
568+
prevSibling: 'X',
569+
nextSibling: null,
570+
text: 'gamma',
571+
type: 'ordered-list-item',
572+
children: List([]),
573+
}),
574+
];
575+
576+
test('onTab (untab) on a middle child splits the block at that child', () => {
577+
assertNestedUtilOperation(
578+
editorState =>
579+
onTab({preventDefault: () => {}, shiftKey: true}, editorState, 2),
580+
{
581+
anchorKey: 'E',
582+
focusKey: 'E',
583+
},
584+
contentBlockNodes3,
585+
);
586+
});
587+
469588
// TODO (T32099101)
470589
test('onSplitParent must split a nested block retaining parent', () => {
471590
expect(true).toBe(true);

0 commit comments

Comments
 (0)