Bug 1730442 - part 7: Rewrite HTMLEditor::InsertTableRowsWithTransaction() to collect any cell information before touching the DOM tree r=m_kato

This patch also makes it inserts table rows immediately after the previous
row element of the found reference row element to make it insert new table
rows immediately after there when inserting position is "after".  Typically,
it's the parent `<tr>` element of `aCellElement`.  Therefore, this approach
can preserve text nodes immediately after it which was intentionally inserted
by the web apps.

Differential Revision: https://phabricator.services.mozilla.com/D146365
This commit is contained in:
Masayuki Nakano 2022-05-20 07:39:53 +00:00
parent bc4493c724
commit 5a6a9a0045
5 changed files with 368 additions and 182 deletions

View File

@ -1276,7 +1276,7 @@ EditActionResult HTMLEditor::HandleTabKeyPressInTable(
}
// Find enclosing table cell from selection (cell may be selected element)
Element* cellElement =
const RefPtr<Element> cellElement =
GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
if (!cellElement) {
NS_WARNING(
@ -1343,13 +1343,14 @@ EditActionResult HTMLEditor::HandleTabKeyPressInTable(
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditActionHandled(rv);
}
rv = InsertTableRowsWithTransaction(1, InsertPosition::eAfterSelectedCell);
rv = InsertTableRowsWithTransaction(*cellElement, 1,
InsertPosition::eAfterSelectedCell);
if (NS_WARN_IF(Destroyed())) {
return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::InsertTableRowsWithTransaction(1, "
"HTMLEditor::InsertTableRowsWithTransaction(*cellElement, 1, "
"InsertPosition::eAfterSelectedCell) failed");
return EditActionHandled(rv);
}

View File

@ -12,6 +12,7 @@
#include "mozilla/EditorBase.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/EditorUtils.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/HTMLEditHelpers.h"
#include "mozilla/ManualNAC.h"
#include "mozilla/Result.h"
@ -3052,6 +3053,13 @@ class HTMLEditor final : public EditorBase,
[[nodiscard]] static CellData AtIndexInTableElement(
const HTMLEditor& aHTMLEditor, const Element& aTableElement,
int32_t aRowIndex, int32_t aColumnIndex);
[[nodiscard]] static CellData AtIndexInTableElement(
const HTMLEditor& aHTMLEditor, const Element& aTableElement,
const CellIndexes& aIndexes) {
MOZ_ASSERT(!aIndexes.isErr());
return AtIndexInTableElement(aHTMLEditor, aTableElement, aIndexes.mRow,
aIndexes.mColumn);
}
/**
* Treated as error if fails to compute current index or first index of the
@ -3619,18 +3627,19 @@ class HTMLEditor final : public EditorBase,
/**
* InsertTableRowsWithTransaction() inserts <tr> elements before or after
* a cell element containing first selection range. I.e., if the cell
* spans rows and aInsertPosition is eAfterSelectedCell, new rows will be
* inserted after the most-bottom row which contains the cell. If first
* selection range is not in table cell element, this does nothing but
* does not return error.
* aCellElement. When aCellElement spans rows and aInsertPosition is
* eAfterSelectedCell, new rows will be inserted after the most-bottom row
* which contains the cell.
*
* @param aCellElement The cell element pinting where this will
* insert a row before or after.
* @param aNumberOfRowsToInsert Number of rows to insert.
* @param aInsertPosition Before or after the target cell which
* contains first selection range.
*/
MOZ_CAN_RUN_SCRIPT nsresult InsertTableRowsWithTransaction(
int32_t aNumberOfRowsToInsert, InsertPosition aInsertPosition);
Element& aCellElement, int32_t aNumberOfRowsToInsert,
InsertPosition aInsertPosition);
/**
* Insert a new cell after or before supplied aCell.

View File

@ -292,11 +292,12 @@ nsresult HTMLEditor::DoInlineTableEditingAction(const Element& aElement) {
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
DebugOnly<nsresult> rvIgnored =
InsertTableRowsWithTransaction(1, InsertPosition::eBeforeSelectedCell);
OwningNonNull<Element> targetCellElement(*mInlineEditedCell);
DebugOnly<nsresult> rvIgnored = InsertTableRowsWithTransaction(
targetCellElement, 1, InsertPosition::eBeforeSelectedCell);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::InsertTableRowsWithTransaction(1, "
"HTMLEditor::InsertTableRowsWithTransaction(targetCellElement, 1, "
"InsertPosition::eBeforeSelectedCell) failed, but ignored");
} else if (anonclass.EqualsLiteral("mozTableAddRowAfter")) {
AutoEditActionDataSetter editActionData(*this,
@ -308,11 +309,12 @@ nsresult HTMLEditor::DoInlineTableEditingAction(const Element& aElement) {
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
DebugOnly<nsresult> rvIgnored =
InsertTableRowsWithTransaction(1, InsertPosition::eAfterSelectedCell);
OwningNonNull<Element> targetCellElement(*mInlineEditedCell);
DebugOnly<nsresult> rvIgnored = InsertTableRowsWithTransaction(
targetCellElement, 1, InsertPosition::eAfterSelectedCell);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::InsertTableRowsWithTransaction(1, "
"HTMLEditor::InsertTableRowsWithTransaction(targetCellElement, 1, "
"InsertPosition::eAfterSelectedCell) failed, but ignored");
} else if (anonclass.EqualsLiteral("mozTableRemoveColumn")) {
AutoEditActionDataSetter editActionData(*this,

View File

@ -848,6 +848,10 @@ nsresult HTMLEditor::InsertTableColumnsWithTransaction(
NS_IMETHODIMP HTMLEditor::InsertTableRow(int32_t aNumberOfRowsToInsert,
bool aInsertAfterSelectedCell) {
if (aNumberOfRowsToInsert <= 0) {
return NS_OK;
}
AutoEditActionDataSetter editActionData(*this,
EditAction::eInsertTableRowElement);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
@ -857,48 +861,51 @@ NS_IMETHODIMP HTMLEditor::InsertTableRow(int32_t aNumberOfRowsToInsert,
return EditorBase::ToGenericNSResult(rv);
}
Result<RefPtr<Element>, nsresult> cellElementOrError =
GetFirstSelectedCellElementInTable();
if (cellElementOrError.isErr()) {
NS_WARNING("HTMLEditor::GetFirstSelectedCellElementInTable() failed");
return EditorBase::ToGenericNSResult(cellElementOrError.unwrapErr());
}
if (!cellElementOrError.inspect()) {
return NS_OK;
}
rv = InsertTableRowsWithTransaction(
aNumberOfRowsToInsert, aInsertAfterSelectedCell
? InsertPosition::eAfterSelectedCell
: InsertPosition::eBeforeSelectedCell);
MOZ_KnownLive(*cellElementOrError.inspect()), aNumberOfRowsToInsert,
aInsertAfterSelectedCell ? InsertPosition::eAfterSelectedCell
: InsertPosition::eBeforeSelectedCell);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::InsertTableRowsWithTransaction() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::InsertTableRowsWithTransaction(
int32_t aNumberOfRowsToInsert, InsertPosition aInsertPosition) {
Element& aCellElement, int32_t aNumberOfRowsToInsert,
InsertPosition aInsertPosition) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(HTMLEditUtils::IsTableCell(&aCellElement));
RefPtr<Element> table;
RefPtr<Element> curCell;
int32_t startRowIndex, startColIndex;
nsresult rv =
GetCellContext(getter_AddRefs(table), getter_AddRefs(curCell), nullptr,
nullptr, &startRowIndex, &startColIndex);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::GetCellContext() failed");
return rv;
const RefPtr<PresShell> presShell = GetPresShell();
if (MOZ_UNLIKELY(NS_WARN_IF(!presShell))) {
return NS_ERROR_FAILURE;
}
if (!table || !curCell) {
NS_WARNING(
"HTMLEditor::GetCellContext() didn't return <table> and/or cell");
// Don't fail if no cell found.
if (MOZ_UNLIKELY(
!HTMLEditUtils::IsTableRow(aCellElement.GetParentElement()))) {
NS_WARNING("Tried to insert columns to non-<tr> element");
return NS_ERROR_FAILURE;
}
const RefPtr<Element> tableElement =
HTMLEditUtils::GetClosestAncestorTableElement(aCellElement);
if (MOZ_UNLIKELY(!tableElement)) {
return NS_OK;
}
// Get more data for current cell in row we are inserting at because we need
// colspan.
const auto cellDataAtSelection = CellData::AtIndexInTableElement(
*this, *table, startRowIndex, startColIndex);
if (NS_WARN_IF(cellDataAtSelection.FailedOrNotFound())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(curCell == cellDataAtSelection.mElement);
const Result<TableSize, nsresult> tableSizeOrError =
TableSize::Create(*this, *table);
TableSize::Create(*this, *tableElement);
if (NS_WARN_IF(tableSizeOrError.isErr())) {
return tableSizeOrError.inspectErr();
}
@ -906,6 +913,20 @@ nsresult HTMLEditor::InsertTableRowsWithTransaction(
// Should not be empty since we've already found a cell.
MOZ_ASSERT(!tableSize.IsEmpty());
const CellIndexes cellIndexes(aCellElement, presShell);
if (NS_WARN_IF(cellIndexes.isErr())) {
return NS_ERROR_FAILURE;
}
// Get more data for current cell in row we are inserting at because we need
// rowspan.
const auto cellData =
CellData::AtIndexInTableElement(*this, *tableElement, cellIndexes);
if (NS_WARN_IF(cellData.FailedOrNotFound())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(&aCellElement == cellData.mElement);
AutoPlaceholderBatch treateAsOneTransaction(
*this, ScrollSelectionIntoView::Yes, __FUNCTION__);
// Prevent auto insertion of BR in new cell until we're done
@ -919,186 +940,246 @@ nsresult HTMLEditor::InsertTableRowsWithTransaction(
!error.Failed(),
"HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
switch (aInsertPosition) {
case InsertPosition::eBeforeSelectedCell:
break;
case InsertPosition::eAfterSelectedCell:
// Use row after current cell.
startRowIndex += cellDataAtSelection.mEffectiveRowSpan;
struct ElementWithNewRowSpan final {
const OwningNonNull<Element> mCellElement;
const int32_t mNewRowSpan;
// Detect when user is adding after a rowspan=0 case.
// Assume they want to stop the "0" behavior and really add a new row.
// Thus we set the rowspan to its true value.
if (!cellDataAtSelection.mRowSpan) {
DebugOnly<nsresult> rvIgnored =
SetRowSpan(cellDataAtSelection.mElement,
cellDataAtSelection.mEffectiveRowSpan);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetRowSpan() failed, but ignored");
}
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid InsertPosition");
ElementWithNewRowSpan(Element& aCellElement, int32_t aNewRowSpan)
: mCellElement(aCellElement), mNewRowSpan(aNewRowSpan) {}
};
AutoTArray<ElementWithNewRowSpan, 16> cellElementsToModifyRowSpan;
if (aInsertPosition == InsertPosition::eAfterSelectedCell &&
!cellData.mRowSpan) {
// Detect when user is adding after a rowspan=0 case.
// Assume they want to stop the "0" behavior and really add a new row.
// Thus we set the rowspan to its true value.
cellElementsToModifyRowSpan.AppendElement(
ElementWithNewRowSpan(aCellElement, cellData.mEffectiveRowSpan));
}
// We control selection resetting after the insert.
AutoSelectionSetterAfterTableEdit setCaret(
*this, table, startRowIndex, cellDataAtSelection.mCurrent.mColumn,
ePreviousColumn, false);
// TODO: Remove AutoTransactionsConserveSelection here. It's not necessary
// in normal cases. However, it may be required for nested edit
// actions which may be caused by legacy mutation event listeners or
// chrome script.
AutoTransactionsConserveSelection dontChangeSelection(*this);
RefPtr<Element> cellForRowParent;
int32_t cellsInRow = 0;
if (startRowIndex < tableSize.mRowCount) {
// We are inserting above an existing row. Get each cell in the insert
// row to adjust for colspan effects while we count how many cells are
// needed.
int32_t colIndex = 0;
while (true) {
const auto cellData = CellData::AtIndexInTableElement(
*this, *table, startRowIndex, colIndex);
if (cellData.FailedOrNotFound()) {
break; // Perhaps, we reach end of the row.
}
// XXX So, this is impossible case. Will be removed.
if (!cellData.mElement) {
NS_WARNING("CellData::Update() succeeded, but didn't set mElement");
break;
}
if (cellData.IsSpannedFromOtherRow()) {
// We have a cell spanning this location. Increase its rowspan.
// Note that if rowspan is 0, we do nothing since that cell should
// automatically extend into the new row.
if (cellData.mRowSpan > 0) {
DebugOnly<nsresult> rvIgnored = SetRowSpan(
cellData.mElement, cellData.mRowSpan + aNumberOfRowsToInsert);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetRowSpan() failed, but ignored");
struct MOZ_STACK_CLASS TableRowData {
RefPtr<Element> mElement;
int32_t mNumberOfCellsInStartRow;
int32_t mOffsetInTRElementToPutCaret;
};
const auto referenceRowDataOrError = [&]() -> Result<TableRowData, nsresult> {
const int32_t startRowIndex =
aInsertPosition == InsertPosition::eBeforeSelectedCell
? cellData.mCurrent.mRow
: cellData.mCurrent.mRow + cellData.mEffectiveRowSpan;
if (startRowIndex < tableSize.mRowCount) {
// We are inserting above an existing row. Get each cell in the insert
// row to adjust for rowspan effects while we count how many cells are
// needed.
RefPtr<Element> referenceRowElement;
int32_t numberOfCellsInStartRow = 0;
int32_t offsetInTRElementToPutCaret = 0;
for (int32_t colIndex = 0;;) {
const auto cellDataInStartRow = CellData::AtIndexInTableElement(
*this, *tableElement, startRowIndex, colIndex);
if (cellDataInStartRow.FailedOrNotFound()) {
break; // Perhaps, we reach end of the row.
}
colIndex = cellData.NextColumnIndex();
continue;
}
cellsInRow += cellData.mEffectiveColSpan;
if (!cellForRowParent) {
// FYI: Don't use std::move() here since NextColumnIndex() needs it.
cellForRowParent = cellData.mElement;
}
// XXX So, this is impossible case. Will be removed.
if (!cellDataInStartRow.mElement) {
NS_WARNING("CellData::Update() succeeded, but didn't set mElement");
break;
}
MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
colIndex = cellData.NextColumnIndex();
if (cellDataInStartRow.IsSpannedFromOtherRow()) {
// We have a cell spanning this location. Increase its rowspan.
// Note that if rowspan is 0, we do nothing since that cell should
// automatically extend into the new row.
if (cellDataInStartRow.mRowSpan > 0) {
cellElementsToModifyRowSpan.AppendElement(ElementWithNewRowSpan(
*cellDataInStartRow.mElement,
cellDataInStartRow.mRowSpan + aNumberOfRowsToInsert));
}
colIndex = cellDataInStartRow.NextColumnIndex();
continue;
}
if (colIndex < cellDataInStartRow.mCurrent.mColumn) {
offsetInTRElementToPutCaret++;
}
numberOfCellsInStartRow += cellDataInStartRow.mEffectiveColSpan;
if (!referenceRowElement) {
if (Element* maybeTableRowElement =
cellDataInStartRow.mElement->GetParentElement()) {
if (HTMLEditUtils::IsTableRow(maybeTableRowElement)) {
referenceRowElement = maybeTableRowElement;
}
}
}
MOZ_ASSERT(colIndex < cellDataInStartRow.NextColumnIndex());
colIndex = cellDataInStartRow.NextColumnIndex();
}
if (MOZ_UNLIKELY(!referenceRowElement)) {
NS_WARNING(
"Reference row element to insert new row elements was not found");
return Err(NS_ERROR_FAILURE);
}
return TableRowData{std::move(referenceRowElement),
numberOfCellsInStartRow, offsetInTRElementToPutCaret};
}
} else {
// We are adding a new row after all others. If it weren't for colspan=0
// effect, we could simply use tableSize.mColumnCount for number of new
// cells...
// XXX colspan=0 support has now been removed in table layout so maybe this
// can be cleaned up now? (bug 1243183)
cellsInRow = tableSize.mColumnCount;
int32_t numberOfCellsInStartRow = tableSize.mColumnCount;
int32_t offsetInTRElementToPutCaret = 0;
// but we must compensate for all cells with rowspan = 0 in the last row.
const int32_t kLastRowIndex = tableSize.mRowCount - 1;
const int32_t lastRowIndex = tableSize.mRowCount - 1;
for (int32_t colIndex = 0;;) {
const auto cellData = CellData::AtIndexInTableElement(
*this, *table, kLastRowIndex, colIndex);
if (cellData.FailedOrNotFound()) {
const auto cellDataInLastRow = CellData::AtIndexInTableElement(
*this, *tableElement, lastRowIndex, colIndex);
if (cellDataInLastRow.FailedOrNotFound()) {
break; // Perhaps, we reach end of the row.
}
if (!cellData.mRowSpan) {
MOZ_ASSERT(cellsInRow >= cellData.mEffectiveColSpan);
cellsInRow -= cellData.mEffectiveColSpan;
if (!cellDataInLastRow.mRowSpan) {
MOZ_ASSERT(numberOfCellsInStartRow >=
cellDataInLastRow.mEffectiveColSpan);
numberOfCellsInStartRow -= cellDataInLastRow.mEffectiveColSpan;
} else if (colIndex < cellData.mCurrent.mColumn) {
offsetInTRElementToPutCaret++;
}
// Save cell from the last row that we will use below
if (!cellForRowParent && !cellData.IsSpannedFromOtherRow()) {
// FYI: Don't use std::move() here since NextColumnIndex() needs it.
cellForRowParent = cellData.mElement;
}
MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
MOZ_ASSERT(colIndex < cellDataInLastRow.NextColumnIndex());
colIndex = cellData.NextColumnIndex();
}
return TableRowData{nullptr, numberOfCellsInStartRow,
offsetInTRElementToPutCaret};
}();
if (MOZ_UNLIKELY(referenceRowDataOrError.isErr())) {
return referenceRowDataOrError.inspectErr();
}
if (!cellsInRow) {
NS_WARNING("There was no cell element in the last row");
const TableRowData& referenceRowData = referenceRowDataOrError.inspect();
if (MOZ_UNLIKELY(!referenceRowData.mNumberOfCellsInStartRow)) {
NS_WARNING("There was no cell element in the row");
return NS_OK;
}
if (!cellForRowParent) {
NS_WARNING("There was no cell element for the <tr> element");
return NS_ERROR_FAILURE;
}
Element* parentRow =
GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::tr, *cellForRowParent);
if (!parentRow) {
NS_WARNING(
"HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::tr) "
"failed");
return NS_ERROR_FAILURE;
MOZ_ASSERT_IF(referenceRowData.mElement,
HTMLEditUtils::IsTableRow(referenceRowData.mElement));
if (NS_WARN_IF(!HTMLEditUtils::IsTableRow(aCellElement.GetParentElement()))) {
return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
}
// The row parent and offset where we will insert new row.
EditorDOMPoint pointToInsert(parentRow);
EditorDOMPoint pointToInsert = [&]() {
if (aInsertPosition == InsertPosition::eBeforeSelectedCell) {
MOZ_ASSERT(referenceRowData.mElement);
return EditorDOMPoint(referenceRowData.mElement);
}
// Look for the last row element in the same table section or immediately
// before the reference row element. Then, we can insert new rows
// immediately after the given row element.
Element* lastRowElement = nullptr;
for (Element* rowElement = aCellElement.GetParentElement();
rowElement && rowElement != referenceRowData.mElement;) {
lastRowElement = rowElement;
const Result<RefPtr<Element>, nsresult> nextRowElementOrError =
GetNextTableRowElement(*rowElement);
if (MOZ_UNLIKELY(nextRowElementOrError.isErr())) {
NS_WARNING("HTMLEditor::GetNextTableRowElement() failed");
return EditorDOMPoint();
}
rowElement = nextRowElementOrError.inspect();
}
MOZ_ASSERT(lastRowElement);
return EditorDOMPoint::After(*lastRowElement);
}();
if (NS_WARN_IF(!pointToInsert.IsSet())) {
return NS_ERROR_FAILURE;
}
// Adjust for when adding past the end.
if (aInsertPosition == InsertPosition::eAfterSelectedCell &&
startRowIndex >= tableSize.mRowCount) {
DebugOnly<bool> advanced = pointToInsert.AdvanceOffset();
NS_WARNING_ASSERTION(advanced, "Failed to advance offset");
}
// Note that checking whether the editor destroyed or not should be done
// after inserting all cell elements. Otherwise, the table is left as
// not a rectangle.
auto firstInsertedTRElementOrError =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<RefPtr<Element>, nsresult> {
// Block legacy mutation events for making this job simpler.
nsAutoScriptBlockerSuppressNodeRemoved blockToRunScript;
for (int32_t row = 0; row < aNumberOfRowsToInsert; row++) {
// Create a new row
RefPtr<Element> newRow = CreateElementWithDefaults(*nsGkAtoms::tr);
if (!newRow) {
NS_WARNING("HTMLEditor::CreateElementWithDefaults(nsGkAtoms::tr) failed");
return NS_ERROR_FAILURE;
// Suppress Rules System selection munging.
AutoTransactionsConserveSelection dontChangeSelection(*this);
for (const ElementWithNewRowSpan& cellElementAndNewRowSpan :
cellElementsToModifyRowSpan) {
DebugOnly<nsresult> rvIgnored =
SetRowSpan(MOZ_KnownLive(cellElementAndNewRowSpan.mCellElement),
cellElementAndNewRowSpan.mNewRowSpan);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetRowSpan() failed, but ignored");
}
for (int32_t i = 0; i < cellsInRow; i++) {
RefPtr<Element> newCell = CreateElementWithDefaults(*nsGkAtoms::td);
if (!newCell) {
RefPtr<Element> firstInsertedTRElement;
IgnoredErrorResult error;
for ([[maybe_unused]] const int32_t rowIndex :
Reversed(IntegerRange(aNumberOfRowsToInsert))) {
// Create a new row
RefPtr<Element> newRowElement = CreateElementWithDefaults(*nsGkAtoms::tr);
if (!newRowElement) {
NS_WARNING(
"HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed");
return NS_ERROR_FAILURE;
"HTMLEditor::CreateElementWithDefaults(nsGkAtoms::tr) failed");
return Err(NS_ERROR_FAILURE);
}
newRow->AppendChild(*newCell, error);
if (error.Failed()) {
NS_WARNING("nsINode::AppendChild() failed");
return error.StealNSResult();
}
}
AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
CreateElementResult insertNewRowResult =
InsertNodeWithTransaction<Element>(*newRow, pointToInsert);
if (insertNewRowResult.isErr()) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return insertNewRowResult.unwrapErr();
for ([[maybe_unused]] const int32_t i :
IntegerRange(referenceRowData.mNumberOfCellsInStartRow)) {
const RefPtr<Element> newCellElement =
CreateElementWithDefaults(*nsGkAtoms::td);
if (!newCellElement) {
NS_WARNING(
"HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed");
return Err(NS_ERROR_FAILURE);
}
newRowElement->AppendChild(*newCellElement, error);
if (error.Failed()) {
NS_WARNING("nsINode::AppendChild() failed");
return Err(error.StealNSResult());
}
}
AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
CreateElementResult insertNewRowResult =
InsertNodeWithTransaction<Element>(*newRowElement, pointToInsert);
if (insertNewRowResult.isErr() && !insertNewRowResult.EditorDestroyed()) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return Err(insertNewRowResult.unwrapErr());
}
firstInsertedTRElement = std::move(newRowElement);
// We'll update selection later.
insertNewRowResult.IgnoreCaretPointSuggestion();
}
// Because of dontChangeSelection, we've never allowed to transactions to
// update selection here.
insertNewRowResult.IgnoreCaretPointSuggestion();
return firstInsertedTRElement;
}();
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (MOZ_UNLIKELY(firstInsertedTRElementOrError.isErr())) {
return firstInsertedTRElementOrError.unwrapErr();
}
// SetSelectionAfterTableEdit from AutoSelectionSetterAfterTableEdit will
// access frame selection, so we need reframe.
// Because GetTableCellElementAt() depends on frame.
if (RefPtr<PresShell> presShell = GetPresShell()) {
presShell->FlushPendingNotifications(FlushType::Frames);
}
return NS_OK;
const OwningNonNull<Element> cellElementToPutCaret = [&]() {
if (MOZ_LIKELY(firstInsertedTRElementOrError.inspect())) {
EditorRawDOMPoint point(firstInsertedTRElementOrError.inspect(),
referenceRowData.mOffsetInTRElementToPutCaret);
if (MOZ_LIKELY(point.IsSetAndValid()) &&
MOZ_LIKELY(!point.IsEndOfContainer()) &&
MOZ_LIKELY(HTMLEditUtils::IsTableCell(point.GetChild()))) {
return OwningNonNull<Element>(*point.GetChild()->AsElement());
}
}
return OwningNonNull<Element>(aCellElement);
}();
CollapseSelectionToDeepestNonTableFirstChild(cellElementToPutCaret);
return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
}
nsresult HTMLEditor::DeleteTableElementAndChildrenWithTransaction(

View File

@ -280,6 +280,99 @@ SimpleTest.waitForFocus(() => {
'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (after)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (after)");
(function testInsertBeforeRowFollowingTextNode() {
selection.removeAllRanges();
editor.innerHTML =
"<table>" +
"<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" +
'<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' +
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</table>";
editor.focus();
beforeInputEvents = [];
inputEvents = [];
selection.setBaseAndExtent(
document.getElementById("select").firstChild,
0,
document.getElementById("select").firstChild,
0
);
getTableEditor().insertTableRow(1, false);
is(
editor.innerHTML,
"<table><tbody>" +
"<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" +
'<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
'<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' +
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"testInsertBeforeRowFollowingTextNode: nsITableEditor.insertTableRow(1, false) should insert a row above the second row");
is(
beforeInputEvents.length,
1,
'testInsertBeforeRowFollowingTextNode: Only one "beforeinput" event should be fired'
);
checkInputEvent(
beforeInputEvents[0],
"when selection is collapsed in a cell whose row follows a text node (testInsertBeforeRowFollowingTextNode)"
);
is(
inputEvents.length,
1,
'testInsertBeforeRowFollowingTextNode: Only one "input" event should be fired'
);
checkInputEvent(
inputEvents[0],
"when selection is collapsed in a cell whose row follows a text node (testInsertBeforeRowFollowingTextNode)"
);
})();
(function testInsertAfterRowFollowedTextNode() {
selection.removeAllRanges();
editor.innerHTML =
"<table>" +
"<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" +
'<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' +
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</table>";
editor.focus();
beforeInputEvents = [];
inputEvents = [];
selection.setBaseAndExtent(
document.getElementById("select").firstChild,
0,
document.getElementById("select").firstChild,
0
);
getTableEditor().insertTableRow(1, true);
is(
editor.innerHTML,
"<table><tbody>" +
"<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" +
'<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
'<tr><td valign="top"><br></td><td valign="top"><br></td></tr>\n' +
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"testInsertAfterRowFollowedTextNode: nsITableEditor.insertTableRow(1, true) should insert a row above the second row");
is(
beforeInputEvents.length,
1,
'testInsertAfterRowFollowedTextNode: Only one "beforeinput" event should be fired'
);
checkInputEvent(
beforeInputEvents[0],
"when selection is collapsed in a cell whose row follows a text node (testInsertAfterRowFollowedTextNode)"
);
is(
inputEvents.length,
1,
'testInsertAfterRowFollowedTextNode: Only one "input" event should be fired'
);
checkInputEvent(
inputEvents[0],
"when selection is collapsed in a cell whose row follows a text node (testInsertAfterRowFollowedTextNode)"
);
})();
editor.removeEventListener("beforeinput", onBeforeInput);
editor.removeEventListener("input", onInput);