Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle Tab key on whole table selection or last cell on Edit Plugin #2536

Merged
merged 12 commits into from
Apr 1, 2024
1 change: 1 addition & 0 deletions packages/roosterjs-content-model-api/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export { toggleCode } from './publicApi/segment/toggleCode';
export { insertEntity } from './publicApi/entity/insertEntity';
export { insertTableRow } from './modelApi/table/insertTableRow';
export { insertTableColumn } from './modelApi/table/insertTableColumn';
export { clearSelectedCells } from './modelApi/table/clearSelectedCells';

export { formatTableWithContentModel } from './publicApi/utils/formatTableWithContentModel';
export { formatImageWithContentModel } from './publicApi/utils/formatImageWithContentModel';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { ContentModelTable, TableSelectionCoordinates } from 'roosterjs-con

/**
* Clear selection of a table.
* @internal
* @param table The table model where the selection is to be cleared
* @param sel The selection coordinates to be cleared
*/
Expand Down
47 changes: 35 additions & 12 deletions packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { getOperationalBlocks, isBlockGroupOfType } from 'roosterjs-content-model-dom';
import { handleTabOnList } from './tabUtils/handleTabOnList';
import { handleTabOnParagraph } from './tabUtils/handleTabOnParagraph';
import { handleTabOnTable } from './tabUtils/handleTabOnTable';
import { handleTabOnTableCell } from './tabUtils/handleTabOnTableCell';
import { setModelIndentation } from 'roosterjs-content-model-api';
import type {
ContentModelDocument,
ContentModelListItem,
ContentModelTableCell,
IEditor,
} from 'roosterjs-content-model-types';

Expand All @@ -14,31 +17,51 @@ import type {
export function keyboardTab(editor: IEditor, rawEvent: KeyboardEvent) {
const selection = editor.getDOMSelection();

if (selection?.type == 'range') {
editor.formatContentModel(
model => {
return handleTab(model, rawEvent);
},
{
apiName: 'handleTabKey',
}
);
switch (selection?.type) {
case 'range':
editor.formatContentModel(
model => {
return handleTab(model, rawEvent);
},
{
apiName: 'handleTabKey',
}
);

return true;
return true;
case 'table':
editor.formatContentModel(
model => {
return handleTabOnTable(model, rawEvent);
},
{
apiName: 'handleTabKey',
}
);
return true;
}
}

/**
* If multiple blocks are selected, indent or outdent the selected blocks with setModelIndentation.
* If only one block is selected, call handleTabOnParagraph or handleTabOnList to handle the tab key.
* If only one block is selected:
* - If it is a table cell, call handleTabOnTableCell to handle the tab key.
* - If it is a paragraph, call handleTabOnParagraph to handle the tab key.
* - If it is a list item, call handleTabOnList to handle the tab key.
*/
function handleTab(model: ContentModelDocument, rawEvent: KeyboardEvent) {
const blocks = getOperationalBlocks<ContentModelListItem>(model, ['ListItem'], ['TableCell']);
const blocks = getOperationalBlocks<ContentModelListItem | ContentModelTableCell>(
model,
['ListItem', 'TableCell'],
[]
);
const block = blocks.length > 0 ? blocks[0].block : undefined;
if (blocks.length > 1) {
setModelIndentation(model, rawEvent.shiftKey ? 'outdent' : 'indent');
rawEvent.preventDefault();
return true;
} else if (isBlockGroupOfType<ContentModelTableCell>(block, 'TableCell')) {
return handleTabOnTableCell(model, block, rawEvent);
} else if (block?.blockType === 'Paragraph') {
return handleTabOnParagraph(model, block, rawEvent);
} else if (isBlockGroupOfType<ContentModelListItem>(block, 'ListItem')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getFirstSelectedTable } from 'roosterjs-content-model-dom';
import { setModelIndentation } from 'roosterjs-content-model-api';
import type { ContentModelDocument, ContentModelTable } from 'roosterjs-content-model-types';

/**
* When the whole table is selected, indent or outdent the whole table with setModelIndentation.
* @internal
*/
export function handleTabOnTable(model: ContentModelDocument, rawEvent: KeyboardEvent) {
const tableModel = getFirstSelectedTable(model)[0];
if (tableModel && isWholeTableSelected(tableModel)) {
setModelIndentation(model, rawEvent.shiftKey ? 'outdent' : 'indent');
rawEvent.preventDefault();
return true;
}
return false;
}

function isWholeTableSelected(tableModel: ContentModelTable) {
return (
tableModel.rows[0]?.cells[0]?.isSelected &&
tableModel.rows[tableModel.rows.length - 1]?.cells[tableModel.widths.length - 1]?.isSelected
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { clearSelectedCells, insertTableRow } from 'roosterjs-content-model-api';
import {
createSelectionMarker,
getFirstSelectedTable,
normalizeTable,
setParagraphNotImplicit,
setSelection,
} from 'roosterjs-content-model-dom';
import type { ContentModelDocument, ContentModelTableCell } from 'roosterjs-content-model-types';

/**
* When the cursor is on the last cell of a table, add new row and focus first new cell.
* @internal
*/
export function handleTabOnTableCell(
model: ContentModelDocument,
cell: ContentModelTableCell,
rawEvent: KeyboardEvent
) {
const tableModel = getFirstSelectedTable(model)[0];
// Check if cursor is on last cell of the table
if (
!rawEvent.shiftKey &&
tableModel &&
tableModel.rows[tableModel.rows.length - 1]?.cells[tableModel.widths.length - 1] === cell
) {
insertTableRow(tableModel, 'insertBelow');

// Clear Table selection
clearSelectedCells(tableModel, {
firstRow: tableModel.rows.length - 1,
firstColumn: 0,
lastRow: tableModel.rows.length - 1,
lastColumn: tableModel.widths.length - 1,
});
normalizeTable(tableModel, model.format);

// Add selection marker to the first cell of the new row
const markerParagraph = tableModel.rows[tableModel.rows.length - 1]?.cells[0]?.blocks[0];
if (markerParagraph.blockType == 'Paragraph') {
const marker = createSelectionMarker(model.format);

markerParagraph.segments.unshift(marker);
setParagraphNotImplicit(markerParagraph);
setSelection(tableModel.rows[tableModel.rows.length - 1].cells[0], marker);
}

rawEvent.preventDefault();
return true;
}

return false;
}
Loading
Loading