Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export function toggleModelBlockQuote(

const paragraphOfQuote = getOperationalBlocks<
ContentModelFormatContainer | ContentModelListItem
>(model, ['FormatContainer', 'ListItem'], ['TableCell'], true /*deepFirst*/);
>(model, ['FormatContainer', 'ListItem'], ['TableCell'], true /*deepFirst*/, block => {
return block.blockGroupType == 'FormatContainer' ? block.tagName == 'blockquote' : true;
});

if (areAllBlockQuotes(paragraphOfQuote)) {
// All selections are already in quote, we need to unquote them
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,4 +797,180 @@ describe('toggleModelBlockQuote', () => {
expect(splitSelectedParagraphByBrSpy).toHaveBeenCalledTimes(1);
expect(splitSelectedParagraphByBrSpy).toHaveBeenCalledWith(doc);
});

it('Quote only selected segments in a FormatContainer', () => {
const doc = createContentModelDocument();
const para1 = createParagraph();
const text1 = createText('test1');
const para2 = createParagraph();
const text2 = createText('test2');
const container = createFormatContainer('div', {
id: 'TestId',
});

para1.segments.push(text1);
para2.segments.push(text2);
container.blocks.push(para1);
container.blocks.push(para2);
doc.blocks.push(container);

text2.isSelected = true;

toggleModelBlockQuote(doc, {}, {});

expect(doc).toEqual({
blockGroupType: 'Document',
blocks: [
{
tagName: 'div',
blockType: 'BlockGroup',
format: {
id: 'TestId',
},
blockGroupType: 'FormatContainer',
blocks: [
{
segments: [
{
text: 'test1',
segmentType: 'Text',
format: {},
},
],
blockType: 'Paragraph',
format: {},
},
{
tagName: 'blockquote',
blockType: 'BlockGroup',
format: {},
blockGroupType: 'FormatContainer',
blocks: [
{
segments: [
{
text: 'test2',
segmentType: 'Text',
format: {},
isSelected: true,
},
],

blockType: 'Paragraph',
format: {},
},
],
},
],
},
],
});
expect(splitSelectedParagraphByBrSpy).toHaveBeenCalledTimes(1);
expect(splitSelectedParagraphByBrSpy).toHaveBeenCalledWith(doc);
});

it('Quote only selected segments in a FormatContainer - multi selected segments', () => {
const doc = createContentModelDocument();
const para1 = createParagraph();
const text1 = createText('test1');
const para2 = createParagraph();
const text2 = createText('test2');
const para3 = createParagraph();
const text3 = createText('test3');
const para4 = createParagraph();
const text4 = createText('test4');

const container = createFormatContainer('div', {
id: 'TestId',
});

para1.segments.push(text1);
para2.segments.push(text2);
para3.segments.push(text3);
para4.segments.push(text4);
container.blocks.push(para1);
container.blocks.push(para2);
container.blocks.push(para3);
container.blocks.push(para4);
doc.blocks.push(container);

text2.isSelected = true;
text3.isSelected = true;
text4.isSelected = true;

toggleModelBlockQuote(doc, {}, {});

expect(doc).toEqual({
blockGroupType: 'Document',
blocks: [
{
tagName: 'div',
blockType: 'BlockGroup',
format: {
id: 'TestId',
},
blockGroupType: 'FormatContainer',
blocks: [
{
segments: [
{
text: 'test1',
segmentType: 'Text',
format: {},
},
],
blockType: 'Paragraph',
format: {},
},
{
tagName: 'blockquote',
blockType: 'BlockGroup',
format: {},
blockGroupType: 'FormatContainer',
blocks: [
{
segments: [
{
text: 'test2',
segmentType: 'Text',
format: {},
isSelected: true,
},
],
blockType: 'Paragraph',
format: {},
},
{
segments: [
{
text: 'test3',
segmentType: 'Text',
format: {},
isSelected: true,
},
],
blockType: 'Paragraph',
format: {},
},
{
segments: [
{
text: 'test4',
segmentType: 'Text',
format: {},
isSelected: true,
},
],
blockType: 'Paragraph',
format: {},
},
],
},
],
},
],
});
expect(splitSelectedParagraphByBrSpy).toHaveBeenCalledTimes(1);
expect(splitSelectedParagraphByBrSpy).toHaveBeenCalledWith(doc);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ import type {
* @param path The block group path, from the closest one to root
* @param blockGroupTypes The expected block group types
* @param stopTypes @optional Block group types that will cause stop searching
* @param isValidTarget @optional An additional callback to validate whether a matching block group is a valid target
*/
export function getClosestAncestorBlockGroupIndex<T extends ContentModelBlockGroup>(
path: ReadonlyContentModelBlockGroup[],
blockGroupTypes: TypeOfBlockGroup<T>[],
stopTypes: ContentModelBlockGroupType[] = []
stopTypes: ContentModelBlockGroupType[] = [],
isValidTarget?: (block: ReadonlyContentModelBlockGroup) => boolean
): number {
for (let i = 0; i < path.length; i++) {
const group = path[i];

if ((blockGroupTypes as string[]).indexOf(group.blockGroupType) >= 0) {
if (
(blockGroupTypes as string[]).indexOf(group.blockGroupType) >= 0 &&
(!isValidTarget || isValidTarget(group))
) {
return i;
} else if (stopTypes.indexOf(group.blockGroupType) >= 0) {
// Do not go across boundary specified by stopTypes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,14 @@ export function getSelectedParagraphs(
* @param blockGroupTypes The expected block group types
* @param stopTypes Block group types that will stop searching when hit
* @param deepFirst True means search in deep first, otherwise wide first
* @param isValidTarget @optional An additional callback to validate whether a matching block group is a valid target
*/
export function getOperationalBlocks<T extends ContentModelBlockGroup>(
group: ContentModelBlockGroup,
blockGroupTypes: TypeOfBlockGroup<T>[],
stopTypes: ContentModelBlockGroupType[],
deepFirst?: boolean
deepFirst?: boolean,
isValidTarget?: (block: ReadonlyContentModelBlockGroup) => boolean
): OperationalBlocks<T>[];

/**
Expand All @@ -251,19 +253,22 @@ export function getOperationalBlocks<T extends ContentModelBlockGroup>(
* @param blockGroupTypes The expected block group types
* @param stopTypes Block group types that will stop searching when hit
* @param deepFirst True means search in deep first, otherwise wide first
* @param isValidTarget @optional An additional callback to validate whether a matching block group is a valid target
*/
export function getOperationalBlocks<T extends ReadonlyContentModelBlockGroup>(
group: ReadonlyContentModelBlockGroup,
blockGroupTypes: TypeOfBlockGroup<T>[],
stopTypes: ContentModelBlockGroupType[],
deepFirst?: boolean
deepFirst?: boolean,
isValidTarget?: (block: ReadonlyContentModelBlockGroup) => boolean
): ReadonlyOperationalBlocks<T>[];

export function getOperationalBlocks<T extends ContentModelBlockGroup>(
group: ReadonlyContentModelBlockGroup,
blockGroupTypes: TypeOfBlockGroup<T>[],
stopTypes: ContentModelBlockGroupType[],
deepFirst?: boolean
deepFirst?: boolean,
isValidTarget?: (block: ReadonlyContentModelBlockGroup) => boolean
): ReadonlyOperationalBlocks<T>[] {
const result: ReadonlyOperationalBlocks<T>[] = [];
const findSequence = deepFirst ? blockGroupTypes.map(type => [type]) : [blockGroupTypes];
Expand All @@ -276,7 +281,12 @@ export function getOperationalBlocks<T extends ContentModelBlockGroup>(

selections.forEach(({ path, block }) => {
for (let i = 0; i < findSequence.length; i++) {
const groupIndex = getClosestAncestorBlockGroupIndex(path, findSequence[i], stopTypes);
const groupIndex = getClosestAncestorBlockGroupIndex(
path,
findSequence[i],
stopTypes,
isValidTarget
);

if (groupIndex >= 0) {
if (result.filter(x => x.block == path[groupIndex]).length <= 0) {
Expand Down
Loading