diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /editor/libeditor/HTMLTableEditor.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | editor/libeditor/HTMLTableEditor.cpp | 4600 |
1 files changed, 4600 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLTableEditor.cpp b/editor/libeditor/HTMLTableEditor.cpp new file mode 100644 index 0000000000..7aee5e0ac6 --- /dev/null +++ b/editor/libeditor/HTMLTableEditor.cpp @@ -0,0 +1,4600 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdio.h> + +#include "HTMLEditor.h" +#include "HTMLEditorInlines.h" + +#include "EditAction.h" +#include "EditorDOMPoint.h" +#include "EditorUtils.h" +#include "HTMLEditUtils.h" + +#include "mozilla/Assertions.h" +#include "mozilla/FlushType.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Element.h" +#include "nsAString.h" +#include "nsAlgorithm.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsFrameSelection.h" +#include "nsGkAtoms.h" +#include "nsAtom.h" +#include "nsIContent.h" +#include "nsIFrame.h" +#include "nsINode.h" +#include "nsISupportsUtils.h" +#include "nsITableCellLayout.h" // For efficient access to table cell +#include "nsLiteralString.h" +#include "nsQueryFrame.h" +#include "nsRange.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsTableCellFrame.h" +#include "nsTableWrapperFrame.h" +#include "nscore.h" +#include <algorithm> + +namespace mozilla { + +using namespace dom; +using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; + +/** + * Stack based helper class for restoring selection after table edit. + */ +class MOZ_STACK_CLASS AutoSelectionSetterAfterTableEdit final { + private: + const RefPtr<HTMLEditor> mHTMLEditor; + const RefPtr<Element> mTable; + int32_t mCol, mRow, mDirection, mSelected; + + public: + AutoSelectionSetterAfterTableEdit(HTMLEditor& aHTMLEditor, Element* aTable, + int32_t aRow, int32_t aCol, + int32_t aDirection, bool aSelected) + : mHTMLEditor(&aHTMLEditor), + mTable(aTable), + mCol(aCol), + mRow(aRow), + mDirection(aDirection), + mSelected(aSelected) {} + + MOZ_CAN_RUN_SCRIPT ~AutoSelectionSetterAfterTableEdit() { + if (mHTMLEditor) { + mHTMLEditor->SetSelectionAfterTableEdit(mTable, mRow, mCol, mDirection, + mSelected); + } + } +}; + +/****************************************************************************** + * HTMLEditor::CellIndexes + ******************************************************************************/ + +void HTMLEditor::CellIndexes::Update(HTMLEditor& aHTMLEditor, + Selection& aSelection) { + // Guarantee the life time of the cell element since Init() will access + // layout methods. + RefPtr<Element> cellElement = + aHTMLEditor.GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td); + if (!cellElement) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) " + "failed"); + return; + } + + RefPtr<PresShell> presShell{aHTMLEditor.GetPresShell()}; + Update(*cellElement, presShell); +} + +void HTMLEditor::CellIndexes::Update(Element& aCellElement, + PresShell* aPresShell) { + // If the table cell is created immediately before this call, e.g., using + // innerHTML, frames have not been created yet. Hence, flush layout to create + // them. + if (NS_WARN_IF(!aPresShell)) { + return; + } + + aPresShell->FlushPendingNotifications(FlushType::Frames); + + nsIFrame* frameOfCell = aCellElement.GetPrimaryFrame(); + if (!frameOfCell) { + NS_WARNING("There was no layout information of aCellElement"); + return; + } + + nsITableCellLayout* tableCellLayout = do_QueryFrame(frameOfCell); + if (!tableCellLayout) { + NS_WARNING("aCellElement was not a table cell"); + return; + } + + if (NS_FAILED(tableCellLayout->GetCellIndexes(mRow, mColumn))) { + NS_WARNING("nsITableCellLayout::GetCellIndexes() failed"); + mRow = mColumn = -1; + return; + } + + MOZ_ASSERT(!isErr()); +} + +/****************************************************************************** + * HTMLEditor::CellData + ******************************************************************************/ + +// static +HTMLEditor::CellData HTMLEditor::CellData::AtIndexInTableElement( + const HTMLEditor& aHTMLEditor, const Element& aTableElement, + int32_t aRowIndex, int32_t aColumnIndex) { + nsTableWrapperFrame* tableFrame = HTMLEditor::GetTableFrame(&aTableElement); + if (!tableFrame) { + NS_WARNING("There was no layout information of the table"); + return CellData::Error(aRowIndex, aColumnIndex); + } + + // If there is no cell at the indexes. Don't set the error state to the new + // instance. + nsTableCellFrame* cellFrame = + tableFrame->GetCellFrameAt(aRowIndex, aColumnIndex); + if (!cellFrame) { + return CellData::NotFound(aRowIndex, aColumnIndex); + } + + Element* cellElement = Element::FromNodeOrNull(cellFrame->GetContent()); + if (!cellElement) { + return CellData::Error(aRowIndex, aColumnIndex); + } + return CellData(*cellElement, aRowIndex, aColumnIndex, *cellFrame, + *tableFrame); +} + +HTMLEditor::CellData::CellData(Element& aElement, int32_t aRowIndex, + int32_t aColumnIndex, + nsTableCellFrame& aTableCellFrame, + nsTableWrapperFrame& aTableWrapperFrame) + : mElement(&aElement), + mCurrent(aRowIndex, aColumnIndex), + mFirst(aTableCellFrame.RowIndex(), aTableCellFrame.ColIndex()), + mRowSpan(aTableCellFrame.GetRowSpan()), + mColSpan(aTableCellFrame.GetColSpan()), + mEffectiveRowSpan( + aTableWrapperFrame.GetEffectiveRowSpanAt(aRowIndex, aColumnIndex)), + mEffectiveColSpan( + aTableWrapperFrame.GetEffectiveColSpanAt(aRowIndex, aColumnIndex)), + mIsSelected(aTableCellFrame.IsSelected()) { + MOZ_ASSERT(!mCurrent.isErr()); +} + +/****************************************************************************** + * HTMLEditor::TableSize + ******************************************************************************/ + +// static +Result<HTMLEditor::TableSize, nsresult> HTMLEditor::TableSize::Create( + HTMLEditor& aHTMLEditor, Element& aTableOrElementInTable) { + // Currently, nsTableWrapperFrame::GetRowCount() and + // nsTableWrapperFrame::GetColCount() are safe to use without grabbing + // <table> element. However, editor developers may not watch layout API + // changes. So, for keeping us safer, we should use RefPtr here. + RefPtr<Element> tableElement = + aHTMLEditor.GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::table, + aTableOrElementInTable); + if (!tableElement) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) " + "failed"); + return Err(NS_ERROR_FAILURE); + } + nsTableWrapperFrame* tableFrame = + do_QueryFrame(tableElement->GetPrimaryFrame()); + if (!tableFrame) { + NS_WARNING("There was no layout information of the <table> element"); + return Err(NS_ERROR_FAILURE); + } + const int32_t rowCount = tableFrame->GetRowCount(); + const int32_t columnCount = tableFrame->GetColCount(); + if (NS_WARN_IF(rowCount < 0) || NS_WARN_IF(columnCount < 0)) { + return Err(NS_ERROR_FAILURE); + } + return TableSize(rowCount, columnCount); +} + +/****************************************************************************** + * HTMLEditor + ******************************************************************************/ + +nsresult HTMLEditor::InsertCell(Element* aCell, int32_t aRowSpan, + int32_t aColSpan, bool aAfter, bool aIsHeader, + Element** aNewCell) { + if (aNewCell) { + *aNewCell = nullptr; + } + + if (NS_WARN_IF(!aCell)) { + return NS_ERROR_INVALID_ARG; + } + + // And the parent and offsets needed to do an insert + EditorDOMPoint pointToInsert(aCell); + if (NS_WARN_IF(!pointToInsert.IsSet())) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr<Element> newCell = + CreateElementWithDefaults(aIsHeader ? *nsGkAtoms::th : *nsGkAtoms::td); + if (!newCell) { + NS_WARNING( + "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::th or td) failed"); + return NS_ERROR_FAILURE; + } + + // Optional: return new cell created + if (aNewCell) { + *aNewCell = do_AddRef(newCell).take(); + } + + if (aRowSpan > 1) { + // Note: Do NOT use editor transaction for this + nsAutoString newRowSpan; + newRowSpan.AppendInt(aRowSpan, 10); + DebugOnly<nsresult> rvIgnored = newCell->SetAttr( + kNameSpaceID_None, nsGkAtoms::rowspan, newRowSpan, true); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "Element::SetAttr(nsGkAtoms::rawspan) failed, but ignored"); + } + if (aColSpan > 1) { + // Note: Do NOT use editor transaction for this + nsAutoString newColSpan; + newColSpan.AppendInt(aColSpan, 10); + DebugOnly<nsresult> rvIgnored = newCell->SetAttr( + kNameSpaceID_None, nsGkAtoms::colspan, newColSpan, true); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "Element::SetAttr(nsGkAtoms::colspan) failed, but ignored"); + } + if (aAfter) { + DebugOnly<bool> advanced = pointToInsert.AdvanceOffset(); + NS_WARNING_ASSERTION(advanced, + "Failed to advance offset to after the old cell"); + } + + // 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); + Result<CreateElementResult, nsresult> insertNewCellResult = + InsertNodeWithTransaction<Element>(*newCell, pointToInsert); + if (MOZ_UNLIKELY(insertNewCellResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertNewCellResult.unwrapErr(); + } + // Because of dontChangeSelection, we've never allowed to transactions to + // update selection here. + insertNewCellResult.inspect().IgnoreCaretPointSuggestion(); + return NS_OK; +} + +nsresult HTMLEditor::SetColSpan(Element* aCell, int32_t aColSpan) { + if (NS_WARN_IF(!aCell)) { + return NS_ERROR_INVALID_ARG; + } + nsAutoString newSpan; + newSpan.AppendInt(aColSpan, 10); + nsresult rv = + SetAttributeWithTransaction(*aCell, *nsGkAtoms::colspan, newSpan); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::SetAttributeWithTransaction(nsGkAtoms::colspan) failed"); + return rv; +} + +nsresult HTMLEditor::SetRowSpan(Element* aCell, int32_t aRowSpan) { + if (NS_WARN_IF(!aCell)) { + return NS_ERROR_INVALID_ARG; + } + nsAutoString newSpan; + newSpan.AppendInt(aRowSpan, 10); + nsresult rv = + SetAttributeWithTransaction(*aCell, *nsGkAtoms::rowspan, newSpan); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::SetAttributeWithTransaction(nsGkAtoms::rowspan) failed"); + return rv; +} + +NS_IMETHODIMP HTMLEditor::InsertTableCell(int32_t aNumberOfCellsToInsert, + bool aInsertAfterSelectedCell) { + if (aNumberOfCellsToInsert <= 0) { + return NS_OK; // Just do nothing. + } + + AutoEditActionDataSetter editActionData(*this, + EditAction::eInsertTableCellElement); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + 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; + } + + EditorDOMPoint pointToInsert(cellElementOrError.inspect()); + if (!pointToInsert.IsSet()) { + NS_WARNING("Found an orphan cell element"); + return NS_ERROR_FAILURE; + } + if (aInsertAfterSelectedCell && !pointToInsert.IsEndOfContainer()) { + DebugOnly<bool> advanced = pointToInsert.AdvanceOffset(); + NS_WARNING_ASSERTION( + advanced, + "Failed to set insertion point after current cell, but ignored"); + } + Result<CreateElementResult, nsresult> insertCellElementResult = + InsertTableCellsWithTransaction(pointToInsert, aNumberOfCellsToInsert); + if (MOZ_UNLIKELY(insertCellElementResult.isErr())) { + NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed"); + return EditorBase::ToGenericNSResult(insertCellElementResult.unwrapErr()); + } + // We don't need to modify selection here. + insertCellElementResult.inspect().IgnoreCaretPointSuggestion(); + return NS_OK; +} + +Result<CreateElementResult, nsresult> +HTMLEditor::InsertTableCellsWithTransaction( + const EditorDOMPoint& aPointToInsert, int32_t aNumberOfCellsToInsert) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aPointToInsert.IsSetAndValid()); + MOZ_ASSERT(aNumberOfCellsToInsert > 0); + + if (!HTMLEditUtils::IsTableRow(aPointToInsert.GetContainer())) { + NS_WARNING("Tried to insert cell elements to non-<tr> element"); + return Err(NS_ERROR_FAILURE); + } + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + // Prevent auto insertion of BR in new cell until we're done + // XXX Why? I think that we should insert <br> element for every cell + // **before** inserting new cell into the <tr> element. + IgnoredErrorResult error; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertNode, nsIEditor::eNext, error); + if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return Err(error.StealNSResult()); + } + NS_WARNING_ASSERTION( + !error.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + error.SuppressException(); + + // Put caret into the cell before the first inserting cell, or the first + // table cell in the row. + RefPtr<Element> cellToPutCaret = + aPointToInsert.IsEndOfContainer() + ? nullptr + : HTMLEditUtils::GetPreviousTableCellElementSibling( + *aPointToInsert.GetChild()); + + RefPtr<Element> firstCellElement, lastCellElement; + nsresult rv = [&]() MOZ_CAN_RUN_SCRIPT { + // 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); + + // Block legacy mutation events for making this job simpler. + nsAutoScriptBlockerSuppressNodeRemoved blockToRunScript; + + // If there is a child to put a cell, we need to put all cell elements + // before it. Therefore, creating `EditorDOMPoint` with the child element + // is safe. Otherwise, we need to try to append cell elements in the row. + // Therefore, using `EditorDOMPoint::AtEndOf()` is safe. Note that it's + // not safe to creat it once because the offset and child relation in the + // point becomes invalid after inserting a cell element. + nsIContent* referenceContent = aPointToInsert.GetChild(); + for ([[maybe_unused]] const auto i : + IntegerRange<uint32_t>(aNumberOfCellsToInsert)) { + RefPtr<Element> newCell = CreateElementWithDefaults(*nsGkAtoms::td); + if (!newCell) { + NS_WARNING( + "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed"); + return NS_ERROR_FAILURE; + } + Result<CreateElementResult, nsresult> insertNewCellResult = + InsertNodeWithTransaction( + *newCell, referenceContent + ? EditorDOMPoint(referenceContent) + : EditorDOMPoint::AtEndOf( + *aPointToInsert.ContainerAs<Element>())); + if (MOZ_UNLIKELY(insertNewCellResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertNewCellResult.unwrapErr(); + } + CreateElementResult unwrappedInsertNewCellResult = + insertNewCellResult.unwrap(); + lastCellElement = unwrappedInsertNewCellResult.UnwrapNewNode(); + if (!firstCellElement) { + firstCellElement = lastCellElement; + } + // Because of dontChangeSelection, we've never allowed to transactions + // to update selection here. + unwrappedInsertNewCellResult.IgnoreCaretPointSuggestion(); + if (!cellToPutCaret) { + cellToPutCaret = std::move(newCell); // This is first cell in the row. + } + } + + // TODO: Stop touching selection here. + MOZ_ASSERT(cellToPutCaret); + MOZ_ASSERT(cellToPutCaret->GetParent()); + CollapseSelectionToDeepestNonTableFirstChild(cellToPutCaret); + return NS_OK; + }(); + if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED || + NS_WARN_IF(Destroyed()))) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + return Err(rv); + } + MOZ_ASSERT(firstCellElement); + MOZ_ASSERT(lastCellElement); + return CreateElementResult(std::move(firstCellElement), + EditorDOMPoint(lastCellElement, 0u)); +} + +NS_IMETHODIMP HTMLEditor::GetFirstRow(Element* aTableOrElementInTable, + Element** aFirstRowElement) { + if (NS_WARN_IF(!aTableOrElementInTable) || NS_WARN_IF(!aFirstRowElement)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eGetFirstRow); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::GetFirstRow() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + Result<RefPtr<Element>, nsresult> firstRowElementOrError = + GetFirstTableRowElement(*aTableOrElementInTable); + NS_WARNING_ASSERTION(!firstRowElementOrError.isErr(), + "HTMLEditor::GetFirstTableRowElement() failed"); + if (firstRowElementOrError.isErr()) { + NS_WARNING("HTMLEditor::GetFirstTableRowElement() failed"); + return EditorBase::ToGenericNSResult(firstRowElementOrError.unwrapErr()); + } + firstRowElementOrError.unwrap().forget(aFirstRowElement); + return NS_OK; +} + +Result<RefPtr<Element>, nsresult> HTMLEditor::GetFirstTableRowElement( + const Element& aTableOrElementInTable) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + Element* tableElement = GetInclusiveAncestorByTagNameInternal( + *nsGkAtoms::table, aTableOrElementInTable); + // If the element is not in <table>, return error. + if (!tableElement) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) " + "failed"); + return Err(NS_ERROR_FAILURE); + } + + for (nsIContent* tableChild = tableElement->GetFirstChild(); tableChild; + tableChild = tableChild->GetNextSibling()) { + if (tableChild->IsHTMLElement(nsGkAtoms::tr)) { + // Found a row directly under <table> + return RefPtr<Element>(tableChild->AsElement()); + } + // <table> can have table section elements like <tbody>. <tr> elements + // may be children of them. + if (tableChild->IsAnyOfHTMLElements(nsGkAtoms::tbody, nsGkAtoms::thead, + nsGkAtoms::tfoot)) { + for (nsIContent* tableSectionChild = tableChild->GetFirstChild(); + tableSectionChild; + tableSectionChild = tableSectionChild->GetNextSibling()) { + if (tableSectionChild->IsHTMLElement(nsGkAtoms::tr)) { + return RefPtr<Element>(tableSectionChild->AsElement()); + } + } + } + } + // Don't return error when there is no <tr> element in the <table>. + return RefPtr<Element>(); +} + +Result<RefPtr<Element>, nsresult> HTMLEditor::GetNextTableRowElement( + const Element& aTableRowElement) const { + if (NS_WARN_IF(!aTableRowElement.IsHTMLElement(nsGkAtoms::tr))) { + return Err(NS_ERROR_INVALID_ARG); + } + + for (nsIContent* maybeNextRow = aTableRowElement.GetNextSibling(); + maybeNextRow; maybeNextRow = maybeNextRow->GetNextSibling()) { + if (maybeNextRow->IsHTMLElement(nsGkAtoms::tr)) { + return RefPtr<Element>(maybeNextRow->AsElement()); + } + } + + // In current table section (e.g., <tbody>), there is no <tr> element. + // Then, check the following table sections. + Element* parentElementOfRow = aTableRowElement.GetParentElement(); + if (!parentElementOfRow) { + NS_WARNING("aTableRowElement was an orphan node"); + return Err(NS_ERROR_FAILURE); + } + + // Basically, <tr> elements should be in table section elements even if + // they are not written in the source explicitly. However, for preventing + // cross table boundary, check it now. + if (parentElementOfRow->IsHTMLElement(nsGkAtoms::table)) { + // Don't return error since this means just not found. + return RefPtr<Element>(); + } + + for (nsIContent* maybeNextTableSection = parentElementOfRow->GetNextSibling(); + maybeNextTableSection; + maybeNextTableSection = maybeNextTableSection->GetNextSibling()) { + // If the sibling of parent of given <tr> is a table section element, + // check its children. + if (maybeNextTableSection->IsAnyOfHTMLElements( + nsGkAtoms::tbody, nsGkAtoms::thead, nsGkAtoms::tfoot)) { + for (nsIContent* maybeNextRow = maybeNextTableSection->GetFirstChild(); + maybeNextRow; maybeNextRow = maybeNextRow->GetNextSibling()) { + if (maybeNextRow->IsHTMLElement(nsGkAtoms::tr)) { + return RefPtr<Element>(maybeNextRow->AsElement()); + } + } + } + // I'm not sure whether this is a possible case since table section + // elements are created automatically. However, DOM API may create + // <tr> elements without table section elements. So, let's check it. + else if (maybeNextTableSection->IsHTMLElement(nsGkAtoms::tr)) { + return RefPtr<Element>(maybeNextTableSection->AsElement()); + } + } + // Don't return error when the given <tr> element is the last <tr> element in + // the <table>. + return RefPtr<Element>(); +} + +NS_IMETHODIMP HTMLEditor::InsertTableColumn(int32_t aNumberOfColumnsToInsert, + bool aInsertAfterSelectedCell) { + if (aNumberOfColumnsToInsert <= 0) { + return NS_OK; // XXX Traditional behavior + } + + AutoEditActionDataSetter editActionData(*this, + EditAction::eInsertTableColumn); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + 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; + } + + EditorDOMPoint pointToInsert(cellElementOrError.inspect()); + if (!pointToInsert.IsSet()) { + NS_WARNING("Found an orphan cell element"); + return NS_ERROR_FAILURE; + } + if (aInsertAfterSelectedCell && !pointToInsert.IsEndOfContainer()) { + DebugOnly<bool> advanced = pointToInsert.AdvanceOffset(); + NS_WARNING_ASSERTION( + advanced, + "Failed to set insertion point after current cell, but ignored"); + } + rv = InsertTableColumnsWithTransaction(pointToInsert, + aNumberOfColumnsToInsert); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::InsertTableColumnsWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::InsertTableColumnsWithTransaction( + const EditorDOMPoint& aPointToInsert, int32_t aNumberOfColumnsToInsert) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aPointToInsert.IsSetAndValid()); + MOZ_ASSERT(aNumberOfColumnsToInsert > 0); + + const RefPtr<PresShell> presShell = GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return NS_ERROR_FAILURE; + } + + if (!HTMLEditUtils::IsTableRow(aPointToInsert.GetContainer())) { + NS_WARNING("Tried to insert columns to non-<tr> element"); + return NS_ERROR_FAILURE; + } + + const RefPtr<Element> tableElement = + HTMLEditUtils::GetClosestAncestorTableElement( + *aPointToInsert.ContainerAs<Element>()); + if (!tableElement) { + NS_WARNING("There was no ancestor <table> element"); + return NS_ERROR_FAILURE; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *tableElement); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.inspectErr(); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + if (NS_WARN_IF(tableSize.IsEmpty())) { + return NS_ERROR_FAILURE; // We cannot handle it in an empty table + } + + // If aPointToInsert points non-cell element or end of the row, it means that + // the caller wants to insert column immediately after the last cell of + // the pointing cell element or in the raw. + const bool insertAfterPreviousCell = [&]() { + if (!aPointToInsert.IsEndOfContainer() && + HTMLEditUtils::IsTableCell(aPointToInsert.GetChild())) { + return false; // Insert before the cell element. + } + // There is a previous cell element, we should add a column after it. + Element* previousCellElement = + aPointToInsert.IsEndOfContainer() + ? HTMLEditUtils::GetLastTableCellElementChild( + *aPointToInsert.ContainerAs<Element>()) + : HTMLEditUtils::GetPreviousTableCellElementSibling( + *aPointToInsert.GetChild()); + return previousCellElement != nullptr; + }(); + + // Consider the column index in the table from given point and direction. + auto referenceColumnIndexOrError = + [&]() MOZ_CAN_RUN_SCRIPT -> Result<int32_t, nsresult> { + if (!insertAfterPreviousCell) { + if (aPointToInsert.IsEndOfContainer()) { + return tableSize.mColumnCount; // Empty row, append columns to the end + } + // Insert columns immediately before current column. + const OwningNonNull<Element> tableCellElement = + *aPointToInsert.GetChild()->AsElement(); + MOZ_ASSERT(HTMLEditUtils::IsTableCell(tableCellElement)); + CellIndexes cellIndexes(*tableCellElement, presShell); + if (NS_WARN_IF(cellIndexes.isErr())) { + return Err(NS_ERROR_FAILURE); + } + return cellIndexes.mColumn; + } + + // Otherwise, insert columns immediately after the previous column. + Element* previousCellElement = + aPointToInsert.IsEndOfContainer() + ? HTMLEditUtils::GetLastTableCellElementChild( + *aPointToInsert.ContainerAs<Element>()) + : HTMLEditUtils::GetPreviousTableCellElementSibling( + *aPointToInsert.GetChild()); + MOZ_ASSERT(previousCellElement); + CellIndexes cellIndexes(*previousCellElement, presShell); + if (NS_WARN_IF(cellIndexes.isErr())) { + return Err(NS_ERROR_FAILURE); + } + return cellIndexes.mColumn; + }(); + if (MOZ_UNLIKELY(referenceColumnIndexOrError.isErr())) { + return referenceColumnIndexOrError.unwrapErr(); + } + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + // Prevent auto insertion of <br> element in new cell until we're done. + // XXX Why? We should put <br> element to every cell element before inserting + // the cells into the tree. + IgnoredErrorResult error; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertNode, nsIEditor::eNext, error); + if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return error.StealNSResult(); + } + NS_WARNING_ASSERTION( + !error.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + error.SuppressException(); + + // Suppress Rules System selection munging. + AutoTransactionsConserveSelection dontChangeSelection(*this); + + // If we are inserting after all existing columns, make sure table is + // "well formed" before appending new column. + // XXX As far as I've tested, NormalizeTableInternal() always fails to + // normalize non-rectangular table. So, the following CellData will + // fail if the table is not rectangle. + if (referenceColumnIndexOrError.inspect() >= tableSize.mColumnCount) { + DebugOnly<nsresult> rv = NormalizeTableInternal(*tableElement); + if (MOZ_UNLIKELY(Destroyed())) { + NS_WARNING( + "HTMLEditor::NormalizeTableInternal() caused destroying the editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::NormalizeTableInternal() failed, but ignored"); + } + + // First, we should collect all reference nodes to insert new table cells. + AutoTArray<CellData, 32> arrayOfCellData; + { + arrayOfCellData.SetCapacity(tableSize.mRowCount); + for (const int32_t rowIndex : IntegerRange(tableSize.mRowCount)) { + const auto cellData = CellData::AtIndexInTableElement( + *this, *tableElement, rowIndex, + referenceColumnIndexOrError.inspect()); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + arrayOfCellData.AppendElement(cellData); + } + } + + // 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 cellElementToPutCaretOrError = + [&]() MOZ_CAN_RUN_SCRIPT -> Result<RefPtr<Element>, nsresult> { + // Block legacy mutation events for making this job simpler. + nsAutoScriptBlockerSuppressNodeRemoved blockToRunScript; + RefPtr<Element> cellElementToPutCaret; + for (const CellData& cellData : arrayOfCellData) { + // Don't fail entire process if we fail to find a cell (may fail just in + // particular rows with < adequate cells per row). + // XXX So, here wants to know whether the CellData actually failed + // above. Fix this later. + if (!cellData.mElement) { + continue; + } + + if ((!insertAfterPreviousCell && cellData.IsSpannedFromOtherColumn()) || + (insertAfterPreviousCell && + cellData.IsNextColumnSpannedFromOtherColumn())) { + // If we have a cell spanning this location, simply increase its + // colspan to keep table rectangular. + if (cellData.mColSpan > 0) { + DebugOnly<nsresult> rvIgnored = SetColSpan( + cellData.mElement, cellData.mColSpan + aNumberOfColumnsToInsert); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "HTMLEditor::SetColSpan() failed, but ignored"); + } + continue; + } + + EditorDOMPoint pointToInsert = [&]() { + if (!insertAfterPreviousCell) { + // Insert before the reference cell. + return EditorDOMPoint(cellData.mElement); + } + if (!cellData.mElement->GetNextSibling()) { + // Insert after the reference cell, but nothing follows it, append + // to the end of the row. + return EditorDOMPoint::AtEndOf(*cellData.mElement->GetParentNode()); + } + // Otherwise, returns immediately before the next sibling. Note that + // the next sibling may not be a table cell element. E.g., it may be + // a text node containing only white-spaces in most cases. + return EditorDOMPoint(cellData.mElement->GetNextSibling()); + }(); + if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + Result<CreateElementResult, nsresult> insertCellElementsResult = + InsertTableCellsWithTransaction(pointToInsert, + aNumberOfColumnsToInsert); + if (MOZ_UNLIKELY(insertCellElementsResult.isErr())) { + NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed"); + return insertCellElementsResult.propagateErr(); + } + CreateElementResult unwrappedInsertCellElementsResult = + insertCellElementsResult.unwrap(); + // We'll update selection later into the first inserted cell element in + // the current row. + unwrappedInsertCellElementsResult.IgnoreCaretPointSuggestion(); + if (pointToInsert.ContainerAs<Element>() == + aPointToInsert.ContainerAs<Element>()) { + cellElementToPutCaret = + unwrappedInsertCellElementsResult.UnwrapNewNode(); + MOZ_ASSERT(cellElementToPutCaret); + MOZ_ASSERT(HTMLEditUtils::IsTableCell(cellElementToPutCaret)); + } + } + return cellElementToPutCaret; + }(); + if (MOZ_UNLIKELY(cellElementToPutCaretOrError.isErr())) { + return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED + : cellElementToPutCaretOrError.unwrapErr(); + } + const RefPtr<Element> cellElementToPutCaret = + cellElementToPutCaretOrError.unwrap(); + NS_WARNING_ASSERTION( + cellElementToPutCaret, + "Didn't find the first inserted cell element in the specified row"); + if (MOZ_LIKELY(cellElementToPutCaret)) { + CollapseSelectionToDeepestNonTableFirstChild(cellElementToPutCaret); + } + return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; +} + +NS_IMETHODIMP HTMLEditor::InsertTableRow(int32_t aNumberOfRowsToInsert, + bool aInsertAfterSelectedCell) { + if (aNumberOfRowsToInsert <= 0) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData(*this, + EditAction::eInsertTableRowElement); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + 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( + 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( + Element& aCellElement, int32_t aNumberOfRowsToInsert, + InsertPosition aInsertPosition) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(HTMLEditUtils::IsTableCell(&aCellElement)); + + const RefPtr<PresShell> presShell = GetPresShell(); + if (MOZ_UNLIKELY(NS_WARN_IF(!presShell))) { + return NS_ERROR_FAILURE; + } + + 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; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *tableElement); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.inspectErr(); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + // 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 + IgnoredErrorResult error; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertNode, nsIEditor::eNext, error); + if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return error.StealNSResult(); + } + NS_WARNING_ASSERTION( + !error.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + struct ElementWithNewRowSpan final { + const OwningNonNull<Element> mCellElement; + const int32_t mNewRowSpan; + + 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)); + } + + 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. + } + + // XXX So, this is impossible case. Will be removed. + if (!cellDataInStartRow.mElement) { + NS_WARNING("CellData::Update() succeeded, but didn't set mElement"); + break; + } + + 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}; + } + + // 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) + 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 lastRowIndex = tableSize.mRowCount - 1; + for (int32_t colIndex = 0;;) { + const auto cellDataInLastRow = CellData::AtIndexInTableElement( + *this, *tableElement, lastRowIndex, colIndex); + if (cellDataInLastRow.FailedOrNotFound()) { + break; // Perhaps, we reach end of the row. + } + + if (!cellDataInLastRow.mRowSpan) { + MOZ_ASSERT(numberOfCellsInStartRow >= + cellDataInLastRow.mEffectiveColSpan); + numberOfCellsInStartRow -= cellDataInLastRow.mEffectiveColSpan; + } else if (colIndex < cellDataInLastRow.mCurrent.mColumn) { + offsetInTRElementToPutCaret++; + } + MOZ_ASSERT(colIndex < cellDataInLastRow.NextColumnIndex()); + colIndex = cellDataInLastRow.NextColumnIndex(); + } + return TableRowData{nullptr, numberOfCellsInStartRow, + offsetInTRElementToPutCaret}; + }(); + if (MOZ_UNLIKELY(referenceRowDataOrError.isErr())) { + return referenceRowDataOrError.inspectErr(); + } + + const TableRowData& referenceRowData = referenceRowDataOrError.inspect(); + if (MOZ_UNLIKELY(!referenceRowData.mNumberOfCellsInStartRow)) { + NS_WARNING("There was no cell element in the row"); + return NS_OK; + } + + 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 = [&]() { + 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; + } + // 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; + + // 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"); + } + + 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::tr) failed"); + return Err(NS_ERROR_FAILURE); + } + + 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); + Result<CreateElementResult, nsresult> insertNewRowResult = + InsertNodeWithTransaction<Element>(*newRowElement, pointToInsert); + if (MOZ_UNLIKELY(insertNewRowResult.isErr())) { + if (insertNewRowResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertNewRowResult.propagateErr(); + } + NS_WARNING( + "EditorBase::InsertNodeWithTransaction() failed, but ignored"); + } + firstInsertedTRElement = std::move(newRowElement); + // We'll update selection later. + insertNewRowResult.inspect().IgnoreCaretPointSuggestion(); + } + return firstInsertedTRElement; + }(); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (MOZ_UNLIKELY(firstInsertedTRElementOrError.isErr())) { + return firstInsertedTRElementOrError.unwrapErr(); + } + + 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( + Element& aTableElement) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // Block selectionchange event. It's enough to dispatch selectionchange + // event immediately after removing the table element. + { + AutoHideSelectionChanges hideSelection(SelectionRef()); + + // Select the <table> element after clear current selection. + if (SelectionRef().RangeCount()) { + ErrorResult error; + SelectionRef().RemoveAllRanges(error); + if (error.Failed()) { + NS_WARNING("Selection::RemoveAllRanges() failed"); + return error.StealNSResult(); + } + } + + RefPtr<nsRange> range = nsRange::Create(&aTableElement); + ErrorResult error; + range->SelectNode(aTableElement, error); + if (error.Failed()) { + NS_WARNING("nsRange::SelectNode() failed"); + return error.StealNSResult(); + } + SelectionRef().AddRangeAndSelectFramesAndNotifyListeners(*range, error); + if (error.Failed()) { + NS_WARNING( + "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed"); + return error.StealNSResult(); + } + +#ifdef DEBUG + range = SelectionRef().GetRangeAt(0); + MOZ_ASSERT(range); + MOZ_ASSERT(range->GetStartContainer() == aTableElement.GetParent()); + MOZ_ASSERT(range->GetEndContainer() == aTableElement.GetParent()); + MOZ_ASSERT(range->GetChildAtStartOffset() == &aTableElement); + MOZ_ASSERT(range->GetChildAtEndOffset() == aTableElement.GetNextSibling()); +#endif // #ifdef DEBUG + } + + nsresult rv = DeleteSelectionAsSubAction(eNext, eStrip); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::DeleteSelectionAsSubAction(eNext, eStrip) failed"); + return rv; +} + +NS_IMETHODIMP HTMLEditor::DeleteTable() { + AutoEditActionDataSetter editActionData(*this, + EditAction::eRemoveTableElement); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<Element> table; + rv = GetCellContext(getter_AddRefs(table), nullptr, nullptr, nullptr, nullptr, + nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return EditorBase::ToGenericNSResult(rv); + } + if (!table) { + NS_WARNING("HTMLEditor::GetCellContext() didn't return <table> element"); + return NS_ERROR_FAILURE; + } + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + rv = DeleteTableElementAndChildrenWithTransaction(*table); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::DeleteTableCell(int32_t aNumberOfCellsToDelete) { + AutoEditActionDataSetter editActionData(*this, + EditAction::eRemoveTableCellElement); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = DeleteTableCellWithTransaction(aNumberOfCellsToDelete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::DeleteTableCellWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::DeleteTableCellWithTransaction( + int32_t aNumberOfCellsToDelete) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + RefPtr<Element> table; + RefPtr<Element> cell; + int32_t startRowIndex, startColIndex; + + nsresult rv = + GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr, + nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return rv; + } + if (!table || !cell) { + NS_WARNING( + "HTMLEditor::GetCellContext() didn't return <table> and/or cell"); + // Don't fail if we didn't find a table or cell. + return NS_OK; + } + + if (NS_WARN_IF(!SelectionRef().RangeCount())) { + return NS_ERROR_FAILURE; // XXX Should we just return NS_OK? + } + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + // Prevent rules testing until we're done + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + MOZ_ASSERT(SelectionRef().RangeCount()); + + SelectedTableCellScanner scanner(SelectionRef()); + + Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *table); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.unwrapErr(); + } + // FYI: Cannot be a const reference because the row count will be updated + TableSize tableSize = tableSizeOrError.unwrap(); + MOZ_ASSERT(!tableSize.IsEmpty()); + + // If only one cell is selected or no cell is selected, remove cells + // starting from the first selected cell or a cell containing first + // selection range. + if (!scanner.IsInTableCellSelectionMode() || + SelectionRef().RangeCount() == 1) { + for (int32_t i = 0; i < aNumberOfCellsToDelete; i++) { + nsresult rv = + GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr, + nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return rv; + } + if (!table || !cell) { + NS_WARNING( + "HTMLEditor::GetCellContext() didn't return <table> and/or cell"); + // Don't fail if no cell found + return NS_OK; + } + + int32_t numberOfCellsInRow = GetNumberOfCellsInRow(*table, startRowIndex); + NS_WARNING_ASSERTION( + numberOfCellsInRow >= 0, + "HTMLEditor::GetNumberOfCellsInRow() failed, but ignored"); + + if (numberOfCellsInRow == 1) { + // Remove <tr> or <table> if we're removing all cells in the row or + // the table. + if (tableSize.mRowCount == 1) { + nsresult rv = DeleteTableElementAndChildrenWithTransaction(*table); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() " + "failed"); + return rv; + } + + // We need to call DeleteSelectedTableRowsWithTransaction() to handle + // cells with rowspan attribute. + rv = DeleteSelectedTableRowsWithTransaction(1); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::DeleteSelectedTableRowsWithTransaction(1) failed"); + return rv; + } + + // Adjust table rows simply. In strictly speaking, we should + // recompute table size with the latest layout information since + // mutation event listener may have changed the DOM tree. However, + // this is not in usual path of Firefox. So, we can assume that + // there are no mutation event listeners. + MOZ_ASSERT(tableSize.mRowCount); + tableSize.mRowCount--; + continue; + } + + // The setCaret object will call AutoSelectionSetterAfterTableEdit in its + // destructor + AutoSelectionSetterAfterTableEdit setCaret( + *this, table, startRowIndex, startColIndex, ePreviousColumn, false); + AutoTransactionsConserveSelection dontChangeSelection(*this); + + // XXX Removing cell element causes not adjusting colspan. + rv = DeleteNodeWithTransaction(*cell); + // If we fail, don't try to delete any more cells??? + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + // Note that we don't refer column number in this loop. So, it must + // be safe not to recompute table size since number of row is synced + // above. + } + return NS_OK; + } + + // When 2 or more cells are selected, ignore aNumberOfCellsToRemove and + // remove all selected cells. + const RefPtr<PresShell> presShell{GetPresShell()}; + // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because scanner grabs + // it until it's destroyed later. + const CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]), + presShell); + if (NS_WARN_IF(firstCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + startRowIndex = firstCellIndexes.mRow; + startColIndex = firstCellIndexes.mColumn; + + // The setCaret object will call AutoSelectionSetterAfterTableEdit in its + // destructor + AutoSelectionSetterAfterTableEdit setCaret( + *this, table, startRowIndex, startColIndex, ePreviousColumn, false); + AutoTransactionsConserveSelection dontChangeSelection(*this); + + bool checkToDeleteRow = true; + bool checkToDeleteColumn = true; + for (RefPtr<Element> selectedCellElement = scanner.GetFirstElement(); + selectedCellElement;) { + if (checkToDeleteRow) { + // Optimize to delete an entire row + // Clear so we don't repeat AllCellsInRowSelected within the same row + checkToDeleteRow = false; + if (AllCellsInRowSelected(table, startRowIndex, tableSize.mColumnCount)) { + // First, find the next cell in a different row to continue after we + // delete this row. + int32_t nextRow = startRowIndex; + while (nextRow == startRowIndex) { + selectedCellElement = scanner.GetNextElement(); + if (!selectedCellElement) { + break; + } + const CellIndexes nextSelectedCellIndexes(*selectedCellElement, + presShell); + if (NS_WARN_IF(nextSelectedCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + nextRow = nextSelectedCellIndexes.mRow; + startColIndex = nextSelectedCellIndexes.mColumn; + } + if (tableSize.mRowCount == 1) { + nsresult rv = DeleteTableElementAndChildrenWithTransaction(*table); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() " + "failed"); + return rv; + } + nsresult rv = DeleteTableRowWithTransaction(*table, startRowIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteTableRowWithTransaction() failed"); + return rv; + } + // Adjust table rows simply. In strictly speaking, we should + // recompute table size with the latest layout information since + // mutation event listener may have changed the DOM tree. However, + // this is not in usual path of Firefox. So, we can assume that + // there are no mutation event listeners. + MOZ_ASSERT(tableSize.mRowCount); + tableSize.mRowCount--; + if (!selectedCellElement) { + break; // XXX Seems like a dead path + } + // For the next cell: Subtract 1 for row we deleted + startRowIndex = nextRow - 1; + // Set true since we know we will look at a new row next + checkToDeleteRow = true; + continue; + } + } + + if (checkToDeleteColumn) { + // Optimize to delete an entire column + // Clear this so we don't repeat AllCellsInColSelected within the same Col + checkToDeleteColumn = false; + if (AllCellsInColumnSelected(table, startColIndex, + tableSize.mColumnCount)) { + // First, find the next cell in a different column to continue after + // we delete this column. + int32_t nextCol = startColIndex; + while (nextCol == startColIndex) { + selectedCellElement = scanner.GetNextElement(); + if (!selectedCellElement) { + break; + } + const CellIndexes nextSelectedCellIndexes(*selectedCellElement, + presShell); + if (NS_WARN_IF(nextSelectedCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + startRowIndex = nextSelectedCellIndexes.mRow; + nextCol = nextSelectedCellIndexes.mColumn; + } + // Delete all cells which belong to the column. + nsresult rv = DeleteTableColumnWithTransaction(*table, startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteTableColumnWithTransaction() failed"); + return rv; + } + // Adjust table columns simply. In strictly speaking, we should + // recompute table size with the latest layout information since + // mutation event listener may have changed the DOM tree. However, + // this is not in usual path of Firefox. So, we can assume that + // there are no mutation event listeners. + MOZ_ASSERT(tableSize.mColumnCount); + tableSize.mColumnCount--; + if (!selectedCellElement) { + break; + } + // For the next cell, subtract 1 for col. deleted + startColIndex = nextCol - 1; + // Set true since we know we will look at a new column next + checkToDeleteColumn = true; + continue; + } + } + + nsresult rv = DeleteNodeWithTransaction(*selectedCellElement); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + + selectedCellElement = scanner.GetNextElement(); + if (!selectedCellElement) { + return NS_OK; + } + + const CellIndexes nextCellIndexes(*selectedCellElement, presShell); + if (NS_WARN_IF(nextCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + startRowIndex = nextCellIndexes.mRow; + startColIndex = nextCellIndexes.mColumn; + // When table cell is removed, table size of column may be changed. + // For example, if there are 2 rows, one has 2 cells, the other has + // 3 cells, tableSize.mColumnCount is 3. When this removes a cell + // in the latter row, mColumnCount should be come 2. However, we + // don't use mColumnCount in this loop, so, this must be okay for now. + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::DeleteTableCellContents() { + AutoEditActionDataSetter editActionData(*this, + EditAction::eDeleteTableCellContents); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = DeleteTableCellContentsWithTransaction(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteTableCellContentsWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::DeleteTableCellContentsWithTransaction() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + RefPtr<Element> table; + RefPtr<Element> cell; + int32_t startRowIndex, startColIndex; + nsresult rv = + GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr, + nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return rv; + } + if (!cell) { + NS_WARNING("HTMLEditor::GetCellContext() didn't return cell element"); + // Don't fail if no cell found. + return NS_OK; + } + + if (NS_WARN_IF(!SelectionRef().RangeCount())) { + return NS_ERROR_FAILURE; // XXX Should we just return NS_OK? + } + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + // Prevent rules testing until we're done + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // Don't let Rules System change the selection + AutoTransactionsConserveSelection dontChangeSelection(*this); + + SelectedTableCellScanner scanner(SelectionRef()); + if (scanner.IsInTableCellSelectionMode()) { + const RefPtr<PresShell> presShell{GetPresShell()}; + // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because scanner + // grabs it until it's destroyed later. + const CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]), + presShell); + if (NS_WARN_IF(firstCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + cell = scanner.ElementsRef()[0]; + startRowIndex = firstCellIndexes.mRow; + startColIndex = firstCellIndexes.mColumn; + } + + AutoSelectionSetterAfterTableEdit setCaret( + *this, table, startRowIndex, startColIndex, ePreviousColumn, false); + + for (RefPtr<Element> selectedCellElement = std::move(cell); + selectedCellElement; selectedCellElement = scanner.GetNextElement()) { + DebugOnly<nsresult> rvIgnored = + DeleteAllChildrenWithTransaction(*selectedCellElement); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteAllChildrenWithTransaction() failed, but ignored"); + if (!scanner.IsInTableCellSelectionMode()) { + break; + } + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::DeleteTableColumn(int32_t aNumberOfColumnsToDelete) { + AutoEditActionDataSetter editActionData(*this, + EditAction::eRemoveTableColumn); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = DeleteSelectedTableColumnsWithTransaction(aNumberOfColumnsToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteSelectedTableColumnsWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::DeleteSelectedTableColumnsWithTransaction( + int32_t aNumberOfColumnsToDelete) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + RefPtr<Element> table; + RefPtr<Element> cell; + int32_t startRowIndex, startColIndex; + nsresult rv = + GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr, + nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return rv; + } + if (!table || !cell) { + NS_WARNING( + "HTMLEditor::GetCellContext() didn't return <table> and/or cell"); + // Don't fail if no cell found. + return NS_OK; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *table); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr()); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + + // Prevent rules testing until we're done + IgnoredErrorResult error; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::eNext, error); + if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return error.StealNSResult(); + } + NS_WARNING_ASSERTION( + !error.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // Shortcut the case of deleting all columns in table + if (!startColIndex && aNumberOfColumnsToDelete >= tableSize.mColumnCount) { + nsresult rv = DeleteTableElementAndChildrenWithTransaction(*table); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() failed"); + return rv; + } + + if (NS_WARN_IF(!SelectionRef().RangeCount())) { + return NS_ERROR_FAILURE; // XXX Should we just return NS_OK? + } + + SelectedTableCellScanner scanner(SelectionRef()); + if (scanner.IsInTableCellSelectionMode() && SelectionRef().RangeCount() > 1) { + const RefPtr<PresShell> presShell{GetPresShell()}; + // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because `scanner` + // grabs it until it's destroyed later. + const CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]), + presShell); + if (NS_WARN_IF(firstCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + startRowIndex = firstCellIndexes.mRow; + startColIndex = firstCellIndexes.mColumn; + } + + // We control selection resetting after the insert... + AutoSelectionSetterAfterTableEdit setCaret( + *this, table, startRowIndex, startColIndex, ePreviousRow, false); + + // If 2 or more cells are not selected, removing columns starting from + // a column which contains first selection range. + if (!scanner.IsInTableCellSelectionMode() || + SelectionRef().RangeCount() == 1) { + int32_t columnCountToRemove = std::min( + aNumberOfColumnsToDelete, tableSize.mColumnCount - startColIndex); + for (int32_t i = 0; i < columnCountToRemove; i++) { + nsresult rv = DeleteTableColumnWithTransaction(*table, startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteTableColumnWithTransaction() failed"); + return rv; + } + } + return NS_OK; + } + + // If 2 or more cells are selected, remove all columns which contain selected + // cells. I.e., we ignore aNumberOfColumnsToDelete in this case. + const RefPtr<PresShell> presShell{GetPresShell()}; + for (RefPtr<Element> selectedCellElement = scanner.GetFirstElement(); + selectedCellElement;) { + if (selectedCellElement != scanner.ElementsRef()[0]) { + const CellIndexes cellIndexes(*selectedCellElement, presShell); + if (NS_WARN_IF(cellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + startRowIndex = cellIndexes.mRow; + startColIndex = cellIndexes.mColumn; + } + // Find the next cell in a different column + // to continue after we delete this column + int32_t nextCol = startColIndex; + while (nextCol == startColIndex) { + selectedCellElement = scanner.GetNextElement(); + if (!selectedCellElement) { + break; + } + const CellIndexes cellIndexes(*selectedCellElement, presShell); + if (NS_WARN_IF(cellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + startRowIndex = cellIndexes.mRow; + nextCol = cellIndexes.mColumn; + } + nsresult rv = DeleteTableColumnWithTransaction(*table, startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteTableColumnWithTransaction() failed"); + return rv; + } + } + return NS_OK; +} + +nsresult HTMLEditor::DeleteTableColumnWithTransaction(Element& aTableElement, + int32_t aColumnIndex) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + for (int32_t rowIndex = 0;; rowIndex++) { + const auto cellData = CellData::AtIndexInTableElement( + *this, aTableElement, rowIndex, aColumnIndex); + // Failure means that there is no more row in the table. In this case, + // we shouldn't return error since we just reach the end of the table. + // XXX Should distinguish whether CellData returns error or just not found + // later. + if (cellData.FailedOrNotFound()) { + return NS_OK; + } + + // Find cells that don't start in column we are deleting. + MOZ_ASSERT(cellData.mColSpan >= 0); + if (cellData.IsSpannedFromOtherColumn() || cellData.mColSpan != 1) { + // If we have a cell spanning this location, decrease its colspan to + // keep table rectangular, but if colspan is 0, it'll be adjusted + // automatically. + if (cellData.mColSpan > 0) { + NS_WARNING_ASSERTION(cellData.mColSpan > 1, + "colspan should be 2 or larger"); + DebugOnly<nsresult> rvIgnored = + SetColSpan(cellData.mElement, cellData.mColSpan - 1); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "HTMLEditor::SetColSpan() failed, but ignored"); + } + if (!cellData.IsSpannedFromOtherColumn()) { + // Cell is in column to be deleted, but must have colspan > 1, + // so delete contents of cell instead of cell itself (We must have + // reset colspan above). + DebugOnly<nsresult> rvIgnored = + DeleteAllChildrenWithTransaction(*cellData.mElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "HTMLEditor::DeleteAllChildrenWithTransaction() " + "failed, but ignored"); + } + // Skip rows which the removed cell spanned. + rowIndex += cellData.NumberOfFollowingRows(); + continue; + } + + // Delete the cell + int32_t numberOfCellsInRow = + GetNumberOfCellsInRow(aTableElement, cellData.mCurrent.mRow); + NS_WARNING_ASSERTION( + numberOfCellsInRow > 0, + "HTMLEditor::GetNumberOfCellsInRow() failed, but ignored"); + if (numberOfCellsInRow != 1) { + // If removing cell is not the last cell of the row, we can just remove + // it. + nsresult rv = DeleteNodeWithTransaction(*cellData.mElement); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + // Skip rows which the removed cell spanned. + rowIndex += cellData.NumberOfFollowingRows(); + continue; + } + + // When the cell is the last cell in the row, remove the row instead. + Element* parentRow = GetInclusiveAncestorByTagNameInternal( + *nsGkAtoms::tr, *cellData.mElement); + if (!parentRow) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::tr) " + "failed"); + return NS_ERROR_FAILURE; + } + + // Check if its the only row left in the table. If so, we can delete + // the table instead. + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, aTableElement); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.inspectErr(); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + if (tableSize.mRowCount == 1) { + // We're deleting the last row. So, let's remove the <table> now. + nsresult rv = DeleteTableElementAndChildrenWithTransaction(aTableElement); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() failed"); + return rv; + } + + // Delete the row by placing caret in cell we were to delete. We need + // to call DeleteTableRowWithTransaction() to handle cells with rowspan. + nsresult rv = + DeleteTableRowWithTransaction(aTableElement, cellData.mFirst.mRow); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteTableRowWithTransaction() failed"); + return rv; + } + + // Note that we decrement rowIndex since a row was deleted. + rowIndex--; + } + + // Not reached because for (;;) loop never breaks. +} + +NS_IMETHODIMP HTMLEditor::DeleteTableRow(int32_t aNumberOfRowsToDelete) { + AutoEditActionDataSetter editActionData(*this, + EditAction::eRemoveTableRowElement); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = DeleteSelectedTableRowsWithTransaction(aNumberOfRowsToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteSelectedTableRowsWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::DeleteSelectedTableRowsWithTransaction( + int32_t aNumberOfRowsToDelete) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + RefPtr<Element> table; + RefPtr<Element> cell; + int32_t startRowIndex, startColIndex; + nsresult rv = + GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr, + nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return rv; + } + if (!table || !cell) { + NS_WARNING( + "HTMLEditor::GetCellContext() didn't return <table> and/or cell"); + // Don't fail if no cell found. + return NS_OK; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *table); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.inspectErr(); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + + // Prevent rules testing until we're done + IgnoredErrorResult error; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::eNext, error); + if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return error.StealNSResult(); + } + NS_WARNING_ASSERTION( + !error.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // Shortcut the case of deleting all rows in table + if (!startRowIndex && aNumberOfRowsToDelete >= tableSize.mRowCount) { + nsresult rv = DeleteTableElementAndChildrenWithTransaction(*table); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() failed"); + return rv; + } + + if (NS_WARN_IF(!SelectionRef().RangeCount())) { + return NS_ERROR_FAILURE; // XXX Should we just return NS_OK? + } + + SelectedTableCellScanner scanner(SelectionRef()); + if (scanner.IsInTableCellSelectionMode() && SelectionRef().RangeCount() > 1) { + // Fetch indexes again - may be different for selected cells + const RefPtr<PresShell> presShell{GetPresShell()}; + // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because `scanner` + // grabs it until it's destroyed later. + const CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]), + presShell); + if (NS_WARN_IF(firstCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + startRowIndex = firstCellIndexes.mRow; + startColIndex = firstCellIndexes.mColumn; + } + + // We control selection resetting after the insert... + AutoSelectionSetterAfterTableEdit setCaret( + *this, table, startRowIndex, startColIndex, ePreviousRow, false); + // Don't change selection during deletions + AutoTransactionsConserveSelection dontChangeSelection(*this); + + // XXX Perhaps, the following loops should collect <tr> elements to remove + // first, then, remove them from the DOM tree since mutation event + // listener may change the DOM tree during the loops. + + // If 2 or more cells are not selected, removing rows starting from + // a row which contains first selection range. + if (!scanner.IsInTableCellSelectionMode() || + SelectionRef().RangeCount() == 1) { + int32_t rowCountToRemove = + std::min(aNumberOfRowsToDelete, tableSize.mRowCount - startRowIndex); + for (int32_t i = 0; i < rowCountToRemove; i++) { + nsresult rv = DeleteTableRowWithTransaction(*table, startRowIndex); + // If failed in current row, try the next + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::DeleteTableRowWithTransaction() failed, but trying " + "next..."); + startRowIndex++; + } + // Check if there's a cell in the "next" row. + cell = GetTableCellElementAt(*table, startRowIndex, startColIndex); + if (!cell) { + return NS_OK; + } + } + return NS_OK; + } + + // If 2 or more cells are selected, remove all rows which contain selected + // cells. I.e., we ignore aNumberOfRowsToDelete in this case. + const RefPtr<PresShell> presShell{GetPresShell()}; + for (RefPtr<Element> selectedCellElement = scanner.GetFirstElement(); + selectedCellElement;) { + if (selectedCellElement != scanner.ElementsRef()[0]) { + const CellIndexes cellIndexes(*selectedCellElement, presShell); + if (NS_WARN_IF(cellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + startRowIndex = cellIndexes.mRow; + startColIndex = cellIndexes.mColumn; + } + // Find the next cell in a different row + // to continue after we delete this row + int32_t nextRow = startRowIndex; + while (nextRow == startRowIndex) { + selectedCellElement = scanner.GetNextElement(); + if (!selectedCellElement) { + break; + } + const CellIndexes cellIndexes(*selectedCellElement, presShell); + if (NS_WARN_IF(cellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + nextRow = cellIndexes.mRow; + startColIndex = cellIndexes.mColumn; + } + // Delete the row containing selected cell(s). + nsresult rv = DeleteTableRowWithTransaction(*table, startRowIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteTableRowWithTransaction() failed"); + return rv; + } + } + return NS_OK; +} + +// Helper that doesn't batch or change the selection +nsresult HTMLEditor::DeleteTableRowWithTransaction(Element& aTableElement, + int32_t aRowIndex) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, aTableElement); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.inspectErr(); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + // Prevent rules testing until we're done + IgnoredErrorResult error; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::eNext, error); + if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return error.StealNSResult(); + } + NS_WARNING_ASSERTION( + !error.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + error.SuppressException(); + + // Scan through cells in row to do rowspan adjustments + // Note that after we delete row, startRowIndex will point to the cells in + // the next row to be deleted. + + // The list of cells we will change rowspan in and the new rowspan values + // for each. + struct MOZ_STACK_CLASS SpanCell final { + RefPtr<Element> mElement; + int32_t mNewRowSpanValue; + + SpanCell(Element* aSpanCellElement, int32_t aNewRowSpanValue) + : mElement(aSpanCellElement), mNewRowSpanValue(aNewRowSpanValue) {} + }; + AutoTArray<SpanCell, 10> spanCellArray; + RefPtr<Element> cellInDeleteRow; + int32_t columnIndex = 0; + while (aRowIndex < tableSize.mRowCount && + columnIndex < tableSize.mColumnCount) { + const auto cellData = CellData::AtIndexInTableElement( + *this, aTableElement, aRowIndex, columnIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // XXX So, we should distinguish if CellDate returns error or just not + // found later. + if (!cellData.mElement) { + break; + } + + // Compensate for cells that don't start or extend below the row we are + // deleting. + if (cellData.IsSpannedFromOtherRow()) { + // If a cell starts in row above us, decrease its rowspan to keep table + // rectangular but we don't need to do this if rowspan=0, since it will + // be automatically adjusted. + if (cellData.mRowSpan > 0) { + // Build list of cells to change rowspan. We can't do it now since + // it upsets cell map, so we will do it after deleting the row. + int32_t newRowSpanValue = std::max(cellData.NumberOfPrecedingRows(), + cellData.NumberOfFollowingRows()); + spanCellArray.AppendElement( + SpanCell(cellData.mElement, newRowSpanValue)); + } + } else { + if (cellData.mRowSpan > 1) { + // Cell spans below row to delete, so we must insert new cells to + // keep rows below. Note that we test "rowSpan" so we don't do this + // if rowSpan = 0 (automatic readjustment). + int32_t aboveRowToInsertNewCellInto = + cellData.NumberOfPrecedingRows() + 1; + nsresult rv = SplitCellIntoRows( + &aTableElement, cellData.mFirst.mRow, cellData.mFirst.mColumn, + aboveRowToInsertNewCellInto, cellData.NumberOfFollowingRows(), + nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SplitCellIntoRows() failed"); + return rv; + } + } + if (!cellInDeleteRow) { + // Reference cell to find row to delete. + cellInDeleteRow = std::move(cellData.mElement); + } + } + // Skip over other columns spanned by this cell + columnIndex += cellData.mEffectiveColSpan; + } + + // Things are messed up if we didn't find a cell in the row! + if (!cellInDeleteRow) { + NS_WARNING("There was no cell in deleting row"); + return NS_ERROR_FAILURE; + } + + // Delete the entire row. + RefPtr<Element> parentRow = + GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::tr, *cellInDeleteRow); + if (parentRow) { + nsresult rv = DeleteNodeWithTransaction(*parentRow); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::tr) " + "failed"); + return rv; + } + } + + // Now we can set new rowspans for cells stored above. + for (SpanCell& spanCell : spanCellArray) { + if (NS_WARN_IF(!spanCell.mElement)) { + continue; + } + nsresult rv = + SetRowSpan(MOZ_KnownLive(spanCell.mElement), spanCell.mNewRowSpanValue); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetRawSpan() failed"); + return rv; + } + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::SelectTable() { + AutoEditActionDataSetter editActionData(*this, EditAction::eSelectTable); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::SelectTable() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<Element> table = + GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table); + if (!table) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::table)" + " failed"); + return NS_OK; // Don't fail if we didn't find a table. + } + + rv = ClearSelection(); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::ClearSelection() failed"); + return EditorBase::ToGenericNSResult(rv); + } + rv = AppendContentToSelectionAsRange(*table); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::AppendContentToSelectionAsRange() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::SelectTableCell() { + AutoEditActionDataSetter editActionData(*this, EditAction::eSelectTableCell); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::SelectTableCell() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<Element> cell = + GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td); + if (!cell) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) " + "failed"); + // Don't fail if we didn't find a cell. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + rv = ClearSelection(); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::ClearSelection() failed"); + return EditorBase::ToGenericNSResult(rv); + } + rv = AppendContentToSelectionAsRange(*cell); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::AppendContentToSelectionAsRange() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::SelectAllTableCells() { + AutoEditActionDataSetter editActionData(*this, + EditAction::eSelectAllTableCells); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::SelectAllTableCells() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<Element> cell = + GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td); + if (!cell) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) " + "failed"); + // Don't fail if we didn't find a cell. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + RefPtr<Element> startCell = cell; + + // Get parent table + RefPtr<Element> table = + GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::table, *cell); + if (!table) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) " + "failed"); + return NS_ERROR_FAILURE; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *table); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr()); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + // Suppress nsISelectionListener notification + // until all selection changes are finished + SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__); + + // It is now safe to clear the selection + // BE SURE TO RESET IT BEFORE LEAVING! + rv = ClearSelection(); + if (rv == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING("HTMLEditor::ClearSelection() caused destroying the editor"); + return EditorBase::ToGenericNSResult(rv); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::ClearSelection() failed, but might be ignored"); + + // Select all cells in the same column as current cell + bool cellSelected = false; + // Safety code to select starting cell if nothing else was selected + auto AppendContentToStartCell = [&]() MOZ_CAN_RUN_SCRIPT { + MOZ_ASSERT(!cellSelected); + // XXX In this case, we ignore `NS_ERROR_FAILURE` set by above inner + // `for` loop. + nsresult rv = AppendContentToSelectionAsRange(*startCell); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::AppendContentToSelectionAsRange() failed"); + return EditorBase::ToGenericNSResult(rv); + }; + for (int32_t row = 0; row < tableSize.mRowCount; row++) { + for (int32_t col = 0; col < tableSize.mColumnCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *table, row, col); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return !cellSelected ? AppendContentToStartCell() : NS_ERROR_FAILURE; + } + + // Skip cells that are spanned from previous rows or columns + // XXX So, we should distinguish whether CellData returns error or just + // not found later. + if (cellData.mElement && !cellData.IsSpannedFromOtherRowOrColumn()) { + nsresult rv = AppendContentToSelectionAsRange(*cellData.mElement); + if (rv == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING( + "HTMLEditor::AppendContentToSelectionAsRange() caused " + "destroying the editor"); + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::AppendContentToSelectionAsRange() failed, but " + "might be ignored"); + return !cellSelected ? AppendContentToStartCell() + : EditorBase::ToGenericNSResult(rv); + } + cellSelected = true; + } + MOZ_ASSERT(col < cellData.NextColumnIndex()); + col = cellData.NextColumnIndex(); + } + } + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::SelectTableRow() { + AutoEditActionDataSetter editActionData(*this, EditAction::eSelectTableRow); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::SelectTableRow() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<Element> cell = + GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td); + if (!cell) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) " + "failed"); + // Don't fail if we didn't find a cell. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + RefPtr<Element> startCell = cell; + + // Get table and location of cell: + RefPtr<Element> table; + int32_t startRowIndex, startColIndex; + + rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr, + nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return EditorBase::ToGenericNSResult(rv); + } + if (!table) { + NS_WARNING("HTMLEditor::GetCellContext() didn't return <table> element"); + return NS_ERROR_FAILURE; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *table); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr()); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + // Note: At this point, we could get first and last cells in row, + // then call SelectBlockOfCells, but that would take just + // a little less code, so the following is more efficient + + // Suppress nsISelectionListener notification + // until all selection changes are finished + SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__); + + // It is now safe to clear the selection + // BE SURE TO RESET IT BEFORE LEAVING! + rv = ClearSelection(); + if (rv == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING("HTMLEditor::ClearSelection() caused destroying the editor"); + return EditorBase::ToGenericNSResult(rv); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::ClearSelection() failed, but might be ignored"); + + // Select all cells in the same row as current cell + bool cellSelected = false; + for (int32_t col = 0; col < tableSize.mColumnCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *table, startRowIndex, col); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + if (cellSelected) { + return NS_ERROR_FAILURE; + } + // Safety code to select starting cell if nothing else was selected + nsresult rv = AppendContentToSelectionAsRange(*startCell); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::AppendContentToSelectionAsRange() failed"); + NS_WARNING_ASSERTION( + cellData.isOk() || NS_SUCCEEDED(rv) || + NS_FAILED(EditorBase::ToGenericNSResult(rv)), + "CellData::AtIndexInTableElement() failed, but ignored"); + return EditorBase::ToGenericNSResult(rv); + } + + // Skip cells that are spanned from previous rows or columns + // XXX So, we should distinguish whether CellData returns error or just + // not found later. + if (cellData.mElement && !cellData.IsSpannedFromOtherRowOrColumn()) { + nsresult rv = AppendContentToSelectionAsRange(*cellData.mElement); + if (rv == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING( + "HTMLEditor::AppendContentToSelectionAsRange() caused destroying " + "the editor"); + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + if (cellSelected) { + NS_WARNING("HTMLEditor::AppendContentToSelectionAsRange() failed"); + return EditorBase::ToGenericNSResult(rv); + } + // Safety code to select starting cell if nothing else was selected + nsresult rvTryAgain = AppendContentToSelectionAsRange(*startCell); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::AppendContentToSelectionAsRange() failed"); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(EditorBase::ToGenericNSResult(rv)) || + NS_SUCCEEDED(rvTryAgain) || + NS_FAILED(EditorBase::ToGenericNSResult(rvTryAgain)), + "HTMLEditor::AppendContentToSelectionAsRange(*cellData.mElement) " + "failed, but ignored"); + return EditorBase::ToGenericNSResult(rvTryAgain); + } + cellSelected = true; + } + MOZ_ASSERT(col < cellData.NextColumnIndex()); + col = cellData.NextColumnIndex(); + } + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::SelectTableColumn() { + AutoEditActionDataSetter editActionData(*this, + EditAction::eSelectTableColumn); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::SelectTableColumn() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<Element> cell = + GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td); + if (!cell) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) " + "failed"); + // Don't fail if we didn't find a cell. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + RefPtr<Element> startCell = cell; + + // Get location of cell: + RefPtr<Element> table; + int32_t startRowIndex, startColIndex; + + rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr, + nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return EditorBase::ToGenericNSResult(rv); + } + if (!table) { + NS_WARNING("HTMLEditor::GetCellContext() didn't return <table> element"); + return NS_ERROR_FAILURE; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *table); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr()); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + // Suppress nsISelectionListener notification + // until all selection changes are finished + SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__); + + // It is now safe to clear the selection + // BE SURE TO RESET IT BEFORE LEAVING! + rv = ClearSelection(); + if (rv == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING("HTMLEditor::ClearSelection() caused destroying the editor"); + return EditorBase::ToGenericNSResult(rv); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::ClearSelection() failed, but might be ignored"); + + // Select all cells in the same column as current cell + bool cellSelected = false; + for (int32_t row = 0; row < tableSize.mRowCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *table, row, startColIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + if (cellSelected) { + return NS_ERROR_FAILURE; + } + // Safety code to select starting cell if nothing else was selected + nsresult rv = AppendContentToSelectionAsRange(*startCell); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::AppendContentToSelectionAsRange() failed"); + NS_WARNING_ASSERTION( + cellData.isOk() || NS_SUCCEEDED(rv) || + NS_FAILED(EditorBase::ToGenericNSResult(rv)), + "CellData::AtIndexInTableElement() failed, but ignored"); + return EditorBase::ToGenericNSResult(rv); + } + + // Skip cells that are spanned from previous rows or columns + // XXX So, we should distinguish whether CellData returns error or just + // not found later. + if (cellData.mElement && !cellData.IsSpannedFromOtherRowOrColumn()) { + nsresult rv = AppendContentToSelectionAsRange(*cellData.mElement); + if (rv == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING( + "HTMLEditor::AppendContentToSelectionAsRange() caused destroying " + "the editor"); + return EditorBase::ToGenericNSResult(rv); + } + if (NS_FAILED(rv)) { + if (cellSelected) { + NS_WARNING("HTMLEditor::AppendContentToSelectionAsRange() failed"); + return EditorBase::ToGenericNSResult(rv); + } + // Safety code to select starting cell if nothing else was selected + nsresult rvTryAgain = AppendContentToSelectionAsRange(*startCell); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::AppendContentToSelectionAsRange() failed"); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(EditorBase::ToGenericNSResult(rv)) || + NS_SUCCEEDED(rvTryAgain) || + NS_FAILED(EditorBase::ToGenericNSResult(rvTryAgain)), + "HTMLEditor::AppendContentToSelectionAsRange(*cellData.mElement) " + "failed, but ignored"); + return EditorBase::ToGenericNSResult(rvTryAgain); + } + cellSelected = true; + } + MOZ_ASSERT(row < cellData.NextRowIndex()); + row = cellData.NextRowIndex(); + } + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::SplitTableCell() { + AutoEditActionDataSetter editActionData(*this, + EditAction::eSplitTableCellElement); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<Element> table; + RefPtr<Element> cell; + int32_t startRowIndex, startColIndex, actualRowSpan, actualColSpan; + // Get cell, table, etc. at selection anchor node + rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr, + nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return EditorBase::ToGenericNSResult(rv); + } + if (!table || !cell) { + NS_WARNING( + "HTMLEditor::GetCellContext() didn't return <table> and/or cell"); + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + // We need rowspan and colspan data + rv = GetCellSpansAt(table, startRowIndex, startColIndex, actualRowSpan, + actualColSpan); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellSpansAt() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // Must have some span to split + if (actualRowSpan <= 1 && actualColSpan <= 1) { + return NS_OK; + } + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + // Prevent auto insertion of BR in new cell until we're done + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertNode, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return EditorBase::ToGenericNSResult(ignoredError.StealNSResult()); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // We reset selection + AutoSelectionSetterAfterTableEdit setCaret( + *this, table, startRowIndex, startColIndex, ePreviousColumn, false); + //...so suppress Rules System selection munging + AutoTransactionsConserveSelection dontChangeSelection(*this); + + RefPtr<Element> newCell; + int32_t rowIndex = startRowIndex; + int32_t rowSpanBelow, colSpanAfter; + + // Split up cell row-wise first into rowspan=1 above, and the rest below, + // whittling away at the cell below until no more extra span + for (rowSpanBelow = actualRowSpan - 1; rowSpanBelow >= 0; rowSpanBelow--) { + // We really split row-wise only if we had rowspan > 1 + if (rowSpanBelow > 0) { + nsresult rv = SplitCellIntoRows(table, rowIndex, startColIndex, 1, + rowSpanBelow, getter_AddRefs(newCell)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SplitCellIntoRows() failed"); + return EditorBase::ToGenericNSResult(rv); + } + DebugOnly<nsresult> rvIgnored = CopyCellBackgroundColor(newCell, cell); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "HTMLEditor::CopyCellBackgroundColor() failed, but ignored"); + } + int32_t colIndex = startColIndex; + // Now split the cell with rowspan = 1 into cells if it has colSpan > 1 + for (colSpanAfter = actualColSpan - 1; colSpanAfter > 0; colSpanAfter--) { + nsresult rv = SplitCellIntoColumns(table, rowIndex, colIndex, 1, + colSpanAfter, getter_AddRefs(newCell)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SplitCellIntoColumns() failed"); + return EditorBase::ToGenericNSResult(rv); + } + DebugOnly<nsresult> rvIgnored = CopyCellBackgroundColor(newCell, cell); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::CopyCellBackgroundColor() failed, but ignored"); + colIndex++; + } + // Point to the new cell and repeat + rowIndex++; + } + return NS_OK; +} + +nsresult HTMLEditor::CopyCellBackgroundColor(Element* aDestCell, + Element* aSourceCell) { + if (NS_WARN_IF(!aDestCell) || NS_WARN_IF(!aSourceCell)) { + return NS_ERROR_INVALID_ARG; + } + + if (!aSourceCell->HasAttr(nsGkAtoms::bgcolor)) { + return NS_OK; + } + + // Copy backgournd color to new cell. + nsString backgroundColor; + aSourceCell->GetAttr(nsGkAtoms::bgcolor, backgroundColor); + nsresult rv = SetAttributeWithTransaction(*aDestCell, *nsGkAtoms::bgcolor, + backgroundColor); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::SetAttributeWithTransaction(nsGkAtoms::bgcolor) failed"); + return rv; +} + +nsresult HTMLEditor::SplitCellIntoColumns(Element* aTable, int32_t aRowIndex, + int32_t aColIndex, + int32_t aColSpanLeft, + int32_t aColSpanRight, + Element** aNewCell) { + if (NS_WARN_IF(!aTable)) { + return NS_ERROR_INVALID_ARG; + } + if (aNewCell) { + *aNewCell = nullptr; + } + + const auto cellData = + CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, aColIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // We can't split! + if (cellData.mEffectiveColSpan <= 1 || + aColSpanLeft + aColSpanRight > cellData.mEffectiveColSpan) { + return NS_OK; + } + + // Reduce colspan of cell to split + nsresult rv = SetColSpan(cellData.mElement, aColSpanLeft); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetColSpan() failed"); + return rv; + } + + // Insert new cell after using the remaining span + // and always get the new cell so we can copy the background color; + RefPtr<Element> newCellElement; + rv = InsertCell(cellData.mElement, cellData.mEffectiveRowSpan, aColSpanRight, + true, false, getter_AddRefs(newCellElement)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::InsertCell() failed"); + return rv; + } + if (!newCellElement) { + return NS_OK; + } + if (aNewCell) { + *aNewCell = do_AddRef(newCellElement).take(); + } + rv = CopyCellBackgroundColor(newCellElement, cellData.mElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::CopyCellBackgroundColor() failed"); + return rv; +} + +nsresult HTMLEditor::SplitCellIntoRows(Element* aTable, int32_t aRowIndex, + int32_t aColIndex, int32_t aRowSpanAbove, + int32_t aRowSpanBelow, + Element** aNewCell) { + if (NS_WARN_IF(!aTable)) { + return NS_ERROR_INVALID_ARG; + } + + if (aNewCell) { + *aNewCell = nullptr; + } + + const auto cellData = + CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, aColIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // We can't split! + if (cellData.mEffectiveRowSpan <= 1 || + aRowSpanAbove + aRowSpanBelow > cellData.mEffectiveRowSpan) { + return NS_OK; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *aTable); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.inspectErr(); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + // Find a cell to insert before or after + RefPtr<Element> cellElementAtInsertionPoint; + RefPtr<Element> lastCellFound; + bool insertAfter = (cellData.mFirst.mColumn > 0); + for (int32_t colIndex = 0, + rowBelowIndex = cellData.mFirst.mRow + aRowSpanAbove; + colIndex <= tableSize.mColumnCount;) { + const auto cellDataAtInsertionPoint = CellData::AtIndexInTableElement( + *this, *aTable, rowBelowIndex, colIndex); + // If we fail here, it could be because row has bad rowspan values, + // such as all cells having rowspan > 1 (Call FixRowSpan first!). + // XXX According to the comment, this does not assume that + // FixRowSpan() doesn't work well and user can create non-rectangular + // table. So, we should not return error when CellData cannot find + // a cell. + if (NS_WARN_IF(cellDataAtInsertionPoint.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // FYI: Don't use std::move() here since the following checks will use + // utility methods of cellDataAtInsertionPoint, but some of them + // check whether its mElement is not nullptr. + cellElementAtInsertionPoint = cellDataAtInsertionPoint.mElement; + + // Skip over cells spanned from above (like the one we are splitting!) + if (cellDataAtInsertionPoint.mElement && + !cellDataAtInsertionPoint.IsSpannedFromOtherRow()) { + if (!insertAfter) { + // Inserting before, so stop at first cell in row we want to insert + // into. + break; + } + // New cell isn't first in row, + // so stop after we find the cell just before new cell's column + if (cellDataAtInsertionPoint.NextColumnIndex() == + cellData.mFirst.mColumn) { + break; + } + // If cell found is AFTER desired new cell colum, + // we have multiple cells with rowspan > 1 that + // prevented us from finding a cell to insert after... + if (cellDataAtInsertionPoint.mFirst.mColumn > cellData.mFirst.mColumn) { + // ... so instead insert before the cell we found + insertAfter = false; + break; + } + // FYI: Don't use std::move() here since + // cellDataAtInsertionPoint.NextColumnIndex() needs it. + lastCellFound = cellDataAtInsertionPoint.mElement; + } + MOZ_ASSERT(colIndex < cellDataAtInsertionPoint.NextColumnIndex()); + colIndex = cellDataAtInsertionPoint.NextColumnIndex(); + } + + if (!cellElementAtInsertionPoint && lastCellFound) { + // Edge case where we didn't find a cell to insert after + // or before because column(s) before desired column + // and all columns after it are spanned from above. + // We can insert after the last cell we found + cellElementAtInsertionPoint = std::move(lastCellFound); + insertAfter = true; // Should always be true, but let's be sure + } + + // Reduce rowspan of cell to split + nsresult rv = SetRowSpan(cellData.mElement, aRowSpanAbove); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetRowSpan() failed"); + return rv; + } + + // Insert new cell after using the remaining span + // and always get the new cell so we can copy the background color; + RefPtr<Element> newCell; + rv = InsertCell(cellElementAtInsertionPoint, aRowSpanBelow, + cellData.mEffectiveColSpan, insertAfter, false, + getter_AddRefs(newCell)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::InsertCell() failed"); + return rv; + } + if (!newCell) { + return NS_OK; + } + if (aNewCell) { + *aNewCell = do_AddRef(newCell).take(); + } + rv = CopyCellBackgroundColor(newCell, cellElementAtInsertionPoint); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::CopyCellBackgroundColor() failed"); + return rv; +} + +NS_IMETHODIMP HTMLEditor::SwitchTableCellHeaderType(Element* aSourceCell, + Element** aNewCell) { + if (NS_WARN_IF(!aSourceCell)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, + EditAction::eSetTableCellElementType); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + // Prevent auto insertion of BR in new cell created by + // ReplaceContainerAndCloneAttributesWithTransaction(). + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertNode, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return EditorBase::ToGenericNSResult(ignoredError.StealNSResult()); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // Save current selection to restore when done. + // This is needed so ReplaceContainerAndCloneAttributesWithTransaction() + // can monitor selection when replacing nodes. + AutoSelectionRestorer restoreSelectionLater(*this); + + // Set to the opposite of current type + nsAtom* newCellName = + aSourceCell->IsHTMLElement(nsGkAtoms::td) ? nsGkAtoms::th : nsGkAtoms::td; + + // This creates new node, moves children, copies attributes (true) + // and manages the selection! + Result<CreateElementResult, nsresult> newCellElementOrError = + ReplaceContainerAndCloneAttributesWithTransaction( + *aSourceCell, MOZ_KnownLive(*newCellName)); + if (MOZ_UNLIKELY(newCellElementOrError.isErr())) { + NS_WARNING( + "EditorBase::ReplaceContainerAndCloneAttributesWithTransaction() " + "failed"); + return newCellElementOrError.unwrapErr(); + } + // restoreSelectionLater will change selection + newCellElementOrError.inspect().IgnoreCaretPointSuggestion(); + + // Return the new cell + if (aNewCell) { + newCellElementOrError.unwrap().UnwrapNewNode().forget(aNewCell); + } + + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::JoinTableCells(bool aMergeNonContiguousContents) { + AutoEditActionDataSetter editActionData(*this, + EditAction::eJoinTableCellElements); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<Element> table; + RefPtr<Element> targetCell; + int32_t startRowIndex, startColIndex; + + // Get cell, table, etc. at selection anchor node + rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(targetCell), + nullptr, nullptr, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellContext() failed"); + return EditorBase::ToGenericNSResult(rv); + } + if (!table || !targetCell) { + NS_WARNING( + "HTMLEditor::GetCellContext() didn't return <table> and/or cell"); + return NS_OK; + } + + if (NS_WARN_IF(!SelectionRef().RangeCount())) { + return NS_ERROR_FAILURE; // XXX Should we just return NS_OK? + } + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + // Don't let Rules System change the selection + AutoTransactionsConserveSelection dontChangeSelection(*this); + + // Note: We dont' use AutoSelectionSetterAfterTableEdit here so the selection + // is retained after joining. This leaves the target cell selected + // as well as the "non-contiguous" cells, so user can see what happened. + + SelectedTableCellScanner scanner(SelectionRef()); + + // If only one cell is selected, join with cell to the right + if (scanner.ElementsRef().Length() > 1) { + // We have selected cells: Join just contiguous cells + // and just merge contents if not contiguous + Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *table); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return EditorBase::ToGenericNSResult(tableSizeOrError.unwrapErr()); + } + // FYI: Cannot be const because the row count will be updated + TableSize tableSize = tableSizeOrError.unwrap(); + + RefPtr<PresShell> presShell = GetPresShell(); + // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because `scanner` + // grabs it until it's destroyed later. + const CellIndexes firstSelectedCellIndexes( + MOZ_KnownLive(scanner.ElementsRef()[0]), presShell); + if (NS_WARN_IF(firstSelectedCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + + // Get spans for cell we will merge into + int32_t firstRowSpan, firstColSpan; + nsresult rv = GetCellSpansAt(table, firstSelectedCellIndexes.mRow, + firstSelectedCellIndexes.mColumn, firstRowSpan, + firstColSpan); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetCellSpansAt() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // This defines the last indexes along the "edges" + // of the contiguous block of cells, telling us + // that we can join adjacent cells to the block + // Start with same as the first values, + // then expand as we find adjacent selected cells + int32_t lastRowIndex = firstSelectedCellIndexes.mRow; + int32_t lastColIndex = firstSelectedCellIndexes.mColumn; + + // First pass: Determine boundaries of contiguous rectangular block that + // we will join into one cell, favoring adjacent cells in the same row. + for (int32_t rowIndex = firstSelectedCellIndexes.mRow; + rowIndex <= lastRowIndex; rowIndex++) { + int32_t currentRowCount = tableSize.mRowCount; + // Be sure each row doesn't have rowspan errors + rv = FixBadRowSpan(table, rowIndex, tableSize.mRowCount); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::FixBadRowSpan() failed"); + return EditorBase::ToGenericNSResult(rv); + } + // Adjust rowcount by number of rows we removed + lastRowIndex -= currentRowCount - tableSize.mRowCount; + + bool cellFoundInRow = false; + bool lastRowIsSet = false; + int32_t lastColInRow = 0; + int32_t firstColInRow = firstSelectedCellIndexes.mColumn; + int32_t colIndex = firstSelectedCellIndexes.mColumn; + for (; colIndex < tableSize.mColumnCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *table, rowIndex, colIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + if (cellData.mIsSelected) { + if (!cellFoundInRow) { + // We've just found the first selected cell in this row + firstColInRow = cellData.mCurrent.mColumn; + } + if (cellData.mCurrent.mRow > firstSelectedCellIndexes.mRow && + firstColInRow != firstSelectedCellIndexes.mColumn) { + // We're in at least the second row, + // but left boundary is "ragged" (not the same as 1st row's start) + // Let's just end block on previous row + // and keep previous lastColIndex + // TODO: We could try to find the Maximum firstColInRow + // so our block can still extend down more rows? + lastRowIndex = std::max(0, cellData.mCurrent.mRow - 1); + lastRowIsSet = true; + break; + } + // Save max selected column in this row, including extra colspan + lastColInRow = cellData.LastColumnIndex(); + cellFoundInRow = true; + } else if (cellFoundInRow) { + // No cell or not selected, but at least one cell in row was found + if (cellData.mCurrent.mRow > firstSelectedCellIndexes.mRow + 1 && + cellData.mCurrent.mColumn <= lastColIndex) { + // Cell is in a column less than current right border in + // the third or higher selected row, so stop block at the previous + // row + lastRowIndex = std::max(0, cellData.mCurrent.mRow - 1); + lastRowIsSet = true; + } + // We're done with this row + break; + } + MOZ_ASSERT(colIndex < cellData.NextColumnIndex()); + colIndex = cellData.NextColumnIndex(); + } // End of column loop + + // Done with this row + if (cellFoundInRow) { + if (rowIndex == firstSelectedCellIndexes.mRow) { + // First row always initializes the right boundary + lastColIndex = lastColInRow; + } + + // If we didn't determine last row above... + if (!lastRowIsSet) { + if (colIndex < lastColIndex) { + // (don't think we ever get here?) + // Cell is in a column less than current right boundary, + // so stop block at the previous row + lastRowIndex = std::max(0, rowIndex - 1); + } else { + // Go on to examine next row + lastRowIndex = rowIndex + 1; + } + } + // Use the minimum col we found so far for right boundary + lastColIndex = std::min(lastColIndex, lastColInRow); + } else { + // No selected cells in this row -- stop at row above + // and leave last column at its previous value + lastRowIndex = std::max(0, rowIndex - 1); + } + } + + // The list of cells we will delete after joining + nsTArray<RefPtr<Element>> deleteList; + + // 2nd pass: Do the joining and merging + for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) { + for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *table, rowIndex, colIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // If this is 0, we are past last cell in row, so exit the loop + if (!cellData.mEffectiveColSpan) { + break; + } + + // Merge only selected cells (skip cell we're merging into, of course) + if (cellData.mIsSelected && + cellData.mElement != scanner.ElementsRef()[0]) { + if (cellData.mCurrent.mRow >= firstSelectedCellIndexes.mRow && + cellData.mCurrent.mRow <= lastRowIndex && + cellData.mCurrent.mColumn >= firstSelectedCellIndexes.mColumn && + cellData.mCurrent.mColumn <= lastColIndex) { + // We are within the join region + // Problem: It is very tricky to delete cells as we merge, + // since that will upset the cellmap + // Instead, build a list of cells to delete and do it later + NS_ASSERTION(!cellData.IsSpannedFromOtherRow(), + "JoinTableCells: StartRowIndex is in row above"); + + if (cellData.mEffectiveColSpan > 1) { + // Check if cell "hangs" off the boundary because of colspan > 1 + // Use split methods to chop off excess + int32_t extraColSpan = cellData.mFirst.mColumn + + cellData.mEffectiveColSpan - + (lastColIndex + 1); + if (extraColSpan > 0) { + nsresult rv = SplitCellIntoColumns( + table, cellData.mFirst.mRow, cellData.mFirst.mColumn, + cellData.mEffectiveColSpan - extraColSpan, extraColSpan, + nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SplitCellIntoColumns() failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + } + + nsresult rv = + MergeCells(scanner.ElementsRef()[0], cellData.mElement, false); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::MergeCells() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // Add cell to list to delete + deleteList.AppendElement(cellData.mElement.get()); + } else if (aMergeNonContiguousContents) { + // Cell is outside join region -- just merge the contents + nsresult rv = + MergeCells(scanner.ElementsRef()[0], cellData.mElement, false); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::MergeCells() failed"); + return rv; + } + } + } + MOZ_ASSERT(colIndex < cellData.NextColumnIndex()); + colIndex = cellData.NextColumnIndex(); + } + } + + // All cell contents are merged. Delete the empty cells we accumulated + // Prevent rules testing until we're done + IgnoredErrorResult error; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::eNext, error); + if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return EditorBase::ToGenericNSResult(error.StealNSResult()); + } + NS_WARNING_ASSERTION(!error.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() " + "failed, but ignored"); + + for (uint32_t i = 0, n = deleteList.Length(); i < n; i++) { + RefPtr<Element> nodeToBeRemoved = deleteList[i]; + if (nodeToBeRemoved) { + nsresult rv = DeleteNodeWithTransaction(*nodeToBeRemoved); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + } + // Cleanup selection: remove ranges where cells were deleted + uint32_t rangeCount = SelectionRef().RangeCount(); + + // TODO: Rewriting this with reversed ranged-loop may make it simpler. + RefPtr<nsRange> range; + for (uint32_t i = 0; i < rangeCount; i++) { + range = SelectionRef().GetRangeAt(i); + if (NS_WARN_IF(!range)) { + return NS_ERROR_FAILURE; + } + + Element* deletedCell = + HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(*range); + if (!deletedCell) { + SelectionRef().RemoveRangeAndUnselectFramesAndNotifyListeners(*range, + error); + NS_WARNING_ASSERTION( + !error.Failed(), + "Selection::RemoveRangeAndUnselectFramesAndNotifyListeners() " + "failed, but ignored"); + rangeCount--; + i--; + } + } + + // Set spans for the cell everything merged into + rv = SetRowSpan(MOZ_KnownLive(scanner.ElementsRef()[0]), + lastRowIndex - firstSelectedCellIndexes.mRow + 1); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetRowSpan() failed"); + return EditorBase::ToGenericNSResult(rv); + } + rv = SetColSpan(MOZ_KnownLive(scanner.ElementsRef()[0]), + lastColIndex - firstSelectedCellIndexes.mColumn + 1); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetColSpan() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // Fixup disturbances in table layout + DebugOnly<nsresult> rvIgnored = NormalizeTableInternal(*table); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "HTMLEditor::NormalizeTableInternal() failed, but ignored"); + } else { + // Joining with cell to the right -- get rowspan and colspan data of target + // cell. + const auto leftCellData = CellData::AtIndexInTableElement( + *this, *table, startRowIndex, startColIndex); + if (NS_WARN_IF(leftCellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // Get data for cell to the right. + const auto rightCellData = CellData::AtIndexInTableElement( + *this, *table, leftCellData.mFirst.mRow, + leftCellData.mFirst.mColumn + leftCellData.mEffectiveColSpan); + if (NS_WARN_IF(rightCellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // XXX So, this does not assume that CellData returns error when just not + // found. We need to fix this later. + if (!rightCellData.mElement) { + return NS_OK; // Don't fail if there's no cell + } + + // sanity check + NS_ASSERTION( + rightCellData.mCurrent.mRow >= rightCellData.mFirst.mRow, + "JoinCells: rightCellData.mCurrent.mRow < rightCellData.mFirst.mRow"); + + // Figure out span of merged cell starting from target's starting row + // to handle case of merged cell starting in a row above + int32_t spanAboveMergedCell = rightCellData.NumberOfPrecedingRows(); + int32_t effectiveRowSpan2 = + rightCellData.mEffectiveRowSpan - spanAboveMergedCell; + if (effectiveRowSpan2 > leftCellData.mEffectiveRowSpan) { + // Cell to the right spans into row below target + // Split off portion below target cell's bottom-most row + nsresult rv = SplitCellIntoRows( + table, rightCellData.mFirst.mRow, rightCellData.mFirst.mColumn, + spanAboveMergedCell + leftCellData.mEffectiveRowSpan, + effectiveRowSpan2 - leftCellData.mEffectiveRowSpan, nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SplitCellIntoRows() failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + + // Move contents from cell to the right + // Delete the cell now only if it starts in the same row + // and has enough row "height" + nsresult rv = + MergeCells(leftCellData.mElement, rightCellData.mElement, + !rightCellData.IsSpannedFromOtherRow() && + effectiveRowSpan2 >= leftCellData.mEffectiveRowSpan); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::MergeCells() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + if (effectiveRowSpan2 < leftCellData.mEffectiveRowSpan) { + // Merged cell is "shorter" + // (there are cells(s) below it that are row-spanned by target cell) + // We could try splitting those cells, but that's REAL messy, + // so the safest thing to do is NOT really join the cells + return NS_OK; + } + + if (spanAboveMergedCell > 0) { + // Cell we merged started in a row above the target cell + // Reduce rowspan to give room where target cell will extend its colspan + nsresult rv = SetRowSpan(rightCellData.mElement, spanAboveMergedCell); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetRowSpan() failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + + // Reset target cell's colspan to encompass cell to the right + rv = SetColSpan(leftCellData.mElement, leftCellData.mEffectiveColSpan + + rightCellData.mEffectiveColSpan); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetColSpan() failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + return NS_OK; +} + +nsresult HTMLEditor::MergeCells(RefPtr<Element> aTargetCell, + RefPtr<Element> aCellToMerge, + bool aDeleteCellToMerge) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(!aTargetCell) || NS_WARN_IF(!aCellToMerge)) { + return NS_ERROR_INVALID_ARG; + } + + // Prevent rules testing until we're done + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // Don't need to merge if cell is empty + if (!IsEmptyCell(aCellToMerge)) { + // Get index of last child in target cell + // If we fail or don't have children, + // we insert at index 0 + int32_t insertIndex = 0; + + // Start inserting just after last child + uint32_t len = aTargetCell->GetChildCount(); + if (len == 1 && IsEmptyCell(aTargetCell)) { + // Delete the empty node + nsCOMPtr<nsIContent> cellChild = aTargetCell->GetFirstChild(); + if (NS_WARN_IF(!cellChild)) { + return NS_ERROR_FAILURE; + } + nsresult rv = DeleteNodeWithTransaction(*cellChild); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + insertIndex = 0; + } else { + insertIndex = (int32_t)len; + } + + // Move the contents + EditorDOMPoint pointToPutCaret; + while (aCellToMerge->HasChildren()) { + nsCOMPtr<nsIContent> cellChild = aCellToMerge->GetLastChild(); + if (NS_WARN_IF(!cellChild)) { + return NS_ERROR_FAILURE; + } + nsresult rv = DeleteNodeWithTransaction(*cellChild); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + Result<CreateContentResult, nsresult> insertChildContentResult = + InsertNodeWithTransaction(*cellChild, + EditorDOMPoint(aTargetCell, insertIndex)); + if (MOZ_UNLIKELY(insertChildContentResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertChildContentResult.unwrapErr(); + } + CreateContentResult unwrappedInsertChildContentResult = + insertChildContentResult.unwrap(); + unwrappedInsertChildContentResult.MoveCaretPointTo( + pointToPutCaret, *this, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + } + if (pointToPutCaret.IsSet()) { + nsresult rv = CollapseSelectionTo(pointToPutCaret); + if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { + NS_WARNING( + "EditorBase::CollapseSelectionTo() caused destroying the editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed, but ignored"); + } + } + + if (!aDeleteCellToMerge) { + return NS_OK; + } + + // Delete cells whose contents were moved. + nsresult rv = DeleteNodeWithTransaction(*aCellToMerge); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return rv; +} + +nsresult HTMLEditor::FixBadRowSpan(Element* aTable, int32_t aRowIndex, + int32_t& aNewRowCount) { + if (NS_WARN_IF(!aTable)) { + return NS_ERROR_INVALID_ARG; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *aTable); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.inspectErr(); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + int32_t minRowSpan = -1; + for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, colIndex); + // NOTE: This is a *real* failure. + // CellData passes if cell is missing from cellmap + // XXX If <table> has large rowspan value or colspan value than actual + // cells, we may hit error. So, this method is always failed to + // "fix" the rowspan... + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // XXX So, this does not assume that CellData returns error when just not + // found. We need to fix this later. + if (!cellData.mElement) { + break; + } + + if (cellData.mRowSpan > 0 && !cellData.IsSpannedFromOtherRow() && + (cellData.mRowSpan < minRowSpan || minRowSpan == -1)) { + minRowSpan = cellData.mRowSpan; + } + MOZ_ASSERT(colIndex < cellData.NextColumnIndex()); + colIndex = cellData.NextColumnIndex(); + } + + if (minRowSpan > 1) { + // The amount to reduce everyone's rowspan + // so at least one cell has rowspan = 1 + int32_t rowsReduced = minRowSpan - 1; + for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, colIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // Fixup rowspans only for cells starting in current row + // XXX So, this does not assume that CellData returns error when just + // not found a cell. Fix this later. + if (cellData.mElement && cellData.mRowSpan > 0 && + !cellData.IsSpannedFromOtherRowOrColumn()) { + nsresult rv = + SetRowSpan(cellData.mElement, cellData.mRowSpan - rowsReduced); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetRawSpan() failed"); + return rv; + } + } + MOZ_ASSERT(colIndex < cellData.NextColumnIndex()); + colIndex = cellData.NextColumnIndex(); + } + } + const Result<TableSize, nsresult> newTableSizeOrError = + TableSize::Create(*this, *aTable); + if (NS_WARN_IF(newTableSizeOrError.isErr())) { + return newTableSizeOrError.inspectErr(); + } + aNewRowCount = newTableSizeOrError.inspect().mRowCount; + return NS_OK; +} + +nsresult HTMLEditor::FixBadColSpan(Element* aTable, int32_t aColIndex, + int32_t& aNewColCount) { + if (NS_WARN_IF(!aTable)) { + return NS_ERROR_INVALID_ARG; + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *aTable); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.inspectErr(); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + int32_t minColSpan = -1; + for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *aTable, rowIndex, aColIndex); + // NOTE: This is a *real* failure. + // CellData passes if cell is missing from cellmap + // XXX If <table> has large rowspan value or colspan value than actual + // cells, we may hit error. So, this method is always failed to + // "fix" the colspan... + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // XXX So, this does not assume that CellData returns error when just + // not found a cell. Fix this later. + if (!cellData.mElement) { + break; + } + if (cellData.mColSpan > 0 && !cellData.IsSpannedFromOtherColumn() && + (cellData.mColSpan < minColSpan || minColSpan == -1)) { + minColSpan = cellData.mColSpan; + } + MOZ_ASSERT(rowIndex < cellData.NextRowIndex()); + rowIndex = cellData.NextRowIndex(); + } + + if (minColSpan > 1) { + // The amount to reduce everyone's colspan + // so at least one cell has colspan = 1 + int32_t colsReduced = minColSpan - 1; + for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *aTable, rowIndex, aColIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + // Fixup colspans only for cells starting in current column + // XXX So, this does not assume that CellData returns error when just + // not found a cell. Fix this later. + if (cellData.mElement && cellData.mColSpan > 0 && + !cellData.IsSpannedFromOtherRowOrColumn()) { + nsresult rv = + SetColSpan(cellData.mElement, cellData.mColSpan - colsReduced); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetColSpan() failed"); + return rv; + } + } + MOZ_ASSERT(rowIndex < cellData.NextRowIndex()); + rowIndex = cellData.NextRowIndex(); + } + } + const Result<TableSize, nsresult> newTableSizeOrError = + TableSize::Create(*this, *aTable); + if (NS_WARN_IF(newTableSizeOrError.isErr())) { + return newTableSizeOrError.inspectErr(); + } + aNewColCount = newTableSizeOrError.inspect().mColumnCount; + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::NormalizeTable(Element* aTableOrElementInTable) { + AutoEditActionDataSetter editActionData(*this, EditAction::eNormalizeTable); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + if (!aTableOrElementInTable) { + aTableOrElementInTable = + GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table); + if (!aTableOrElementInTable) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::" + "table) failed"); + return NS_OK; // Don't throw error even if the element is not in <table>. + } + } + rv = NormalizeTableInternal(*aTableOrElementInTable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::NormalizeTableInternal() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::NormalizeTableInternal(Element& aTableOrElementInTable) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + RefPtr<Element> tableElement; + if (aTableOrElementInTable.NodeInfo()->NameAtom() == nsGkAtoms::table) { + tableElement = &aTableOrElementInTable; + } else { + tableElement = GetInclusiveAncestorByTagNameInternal( + *nsGkAtoms::table, aTableOrElementInTable); + if (!tableElement) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) " + "failed"); + return NS_OK; // Don't throw error even if the element is not in <table>. + } + } + + Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *tableElement); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return tableSizeOrError.unwrapErr(); + } + // FYI: Cannot be const because the row/column count will be updated + TableSize tableSize = tableSizeOrError.unwrap(); + + // Save current selection + AutoSelectionRestorer restoreSelectionLater(*this); + + AutoPlaceholderBatch treateAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + // Prevent auto insertion of BR in new cell until we're done + IgnoredErrorResult error; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertNode, nsIEditor::eNext, error); + if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return error.StealNSResult(); + } + NS_WARNING_ASSERTION( + !error.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // XXX If there is a cell which has bigger or smaller "rowspan" or "colspan" + // values, FixBadRowSpan() will return error. So, we can do nothing + // if the table needs normalization... + // Scan all cells in each row to detect bad rowspan values + for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) { + nsresult rv = FixBadRowSpan(tableElement, rowIndex, tableSize.mRowCount); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::FixBadRowSpan() failed"); + return rv; + } + } + // and same for colspans + for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount; colIndex++) { + nsresult rv = FixBadColSpan(tableElement, colIndex, tableSize.mColumnCount); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::FixBadColSpan() failed"); + return rv; + } + } + + // Fill in missing cellmap locations with empty cells + for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) { + RefPtr<Element> previousCellElementInRow; + for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount; colIndex++) { + const auto cellData = CellData::AtIndexInTableElement( + *this, *tableElement, rowIndex, colIndex); + // NOTE: This is a *real* failure. + // CellData passes if cell is missing from cellmap + // XXX So, this method assumes that CellData won't return error when + // just not found. Fix this later. + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + + if (cellData.mElement) { + // Save the last cell found in the same row we are scanning + if (!cellData.IsSpannedFromOtherRow()) { + previousCellElementInRow = std::move(cellData.mElement); + } + continue; + } + + // We are missing a cell at a cellmap location. + // Add a cell after the previous cell element in the current row. + if (NS_WARN_IF(!previousCellElementInRow)) { + // We don't have any cells in this row -- We are really messed up! + return NS_ERROR_FAILURE; + } + + // Insert a new cell after (true), and return the new cell to us + RefPtr<Element> newCellElement; + nsresult rv = InsertCell(previousCellElementInRow, 1, 1, true, false, + getter_AddRefs(newCellElement)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::InsertCell() failed"); + return rv; + } + + if (newCellElement) { + previousCellElementInRow = std::move(newCellElement); + } + } + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetCellIndexes(Element* aCellElement, + int32_t* aRowIndex, + int32_t* aColumnIndex) { + if (NS_WARN_IF(!aRowIndex) || NS_WARN_IF(!aColumnIndex)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eGetCellIndexes); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::GetCellIndexes() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + *aRowIndex = 0; + *aColumnIndex = 0; + + if (!aCellElement) { + // Use cell element which contains anchor of Selection when aCellElement is + // nullptr. + const CellIndexes cellIndexes(*this, SelectionRef()); + if (NS_WARN_IF(cellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + *aRowIndex = cellIndexes.mRow; + *aColumnIndex = cellIndexes.mColumn; + return NS_OK; + } + + const RefPtr<PresShell> presShell{GetPresShell()}; + const CellIndexes cellIndexes(*aCellElement, presShell); + if (NS_WARN_IF(cellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + *aRowIndex = cellIndexes.mRow; + *aColumnIndex = cellIndexes.mColumn; + return NS_OK; +} + +// static +nsTableWrapperFrame* HTMLEditor::GetTableFrame(const Element* aTableElement) { + if (NS_WARN_IF(!aTableElement)) { + return nullptr; + } + return do_QueryFrame(aTableElement->GetPrimaryFrame()); +} + +// Return actual number of cells (a cell with colspan > 1 counts as just 1) +int32_t HTMLEditor::GetNumberOfCellsInRow(Element& aTableElement, + int32_t aRowIndex) { + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, aTableElement); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return -1; + } + + int32_t numberOfCells = 0; + for (int32_t columnIndex = 0; + columnIndex < tableSizeOrError.inspect().mColumnCount;) { + const auto cellData = CellData::AtIndexInTableElement( + *this, aTableElement, aRowIndex, columnIndex); + // Failure means that there is no more cell in the row. In this case, + // we shouldn't return error since we just reach the end of the row. + // XXX So, this method assumes that CellData won't return error when + // just not found. Fix this later. + if (cellData.FailedOrNotFound()) { + break; + } + + // Only count cells that start in row we are working with + if (cellData.mElement && !cellData.IsSpannedFromOtherRow()) { + numberOfCells++; + } + MOZ_ASSERT(columnIndex < cellData.NextColumnIndex()); + columnIndex = cellData.NextColumnIndex(); + } + return numberOfCells; +} + +NS_IMETHODIMP HTMLEditor::GetTableSize(Element* aTableOrElementInTable, + int32_t* aRowCount, + int32_t* aColumnCount) { + if (NS_WARN_IF(!aRowCount) || NS_WARN_IF(!aColumnCount)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eGetTableSize); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::GetTableSize() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + *aRowCount = 0; + *aColumnCount = 0; + + Element* tableOrElementInTable = aTableOrElementInTable; + if (!tableOrElementInTable) { + tableOrElementInTable = + GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table); + if (!tableOrElementInTable) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::" + "table) failed"); + return NS_ERROR_FAILURE; + } + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *tableOrElementInTable); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr()); + } + *aRowCount = tableSizeOrError.inspect().mRowCount; + *aColumnCount = tableSizeOrError.inspect().mColumnCount; + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetCellDataAt( + Element* aTableElement, int32_t aRowIndex, int32_t aColumnIndex, + Element** aCellElement, int32_t* aStartRowIndex, int32_t* aStartColumnIndex, + int32_t* aRowSpan, int32_t* aColSpan, int32_t* aEffectiveRowSpan, + int32_t* aEffectiveColSpan, bool* aIsSelected) { + if (NS_WARN_IF(!aCellElement) || NS_WARN_IF(!aStartRowIndex) || + NS_WARN_IF(!aStartColumnIndex) || NS_WARN_IF(!aRowSpan) || + NS_WARN_IF(!aColSpan) || NS_WARN_IF(!aEffectiveRowSpan) || + NS_WARN_IF(!aEffectiveColSpan) || NS_WARN_IF(!aIsSelected)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eGetCellDataAt); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::GetCellDataAt() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + *aStartRowIndex = 0; + *aStartColumnIndex = 0; + *aRowSpan = 0; + *aColSpan = 0; + *aEffectiveRowSpan = 0; + *aEffectiveColSpan = 0; + *aIsSelected = false; + *aCellElement = nullptr; + + // Let's keep the table element with strong pointer since editor developers + // may not handle layout code of <table>, however, this method depends on + // them. + RefPtr<Element> table = aTableElement; + if (!table) { + // Get the selected table or the table enclosing the selection anchor. + table = GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table); + if (!table) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::" + "table) failed"); + return NS_ERROR_FAILURE; + } + } + + const CellData cellData = + CellData::AtIndexInTableElement(*this, *table, aRowIndex, aColumnIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return NS_ERROR_FAILURE; + } + NS_ADDREF(*aCellElement = cellData.mElement.get()); + *aIsSelected = cellData.mIsSelected; + *aStartRowIndex = cellData.mFirst.mRow; + *aStartColumnIndex = cellData.mFirst.mColumn; + *aRowSpan = cellData.mRowSpan; + *aColSpan = cellData.mColSpan; + *aEffectiveRowSpan = cellData.mEffectiveRowSpan; + *aEffectiveColSpan = cellData.mEffectiveColSpan; + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetCellAt(Element* aTableElement, int32_t aRowIndex, + int32_t aColumnIndex, + Element** aCellElement) { + if (NS_WARN_IF(!aCellElement)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eGetCellAt); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::GetCellAt() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + *aCellElement = nullptr; + + Element* tableElement = aTableElement; + if (!tableElement) { + // Get the selected table or the table enclosing the selection anchor. + tableElement = GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table); + if (!tableElement) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::" + "table) failed"); + return NS_ERROR_FAILURE; + } + } + + RefPtr<Element> cellElement = + GetTableCellElementAt(*tableElement, aRowIndex, aColumnIndex); + cellElement.forget(aCellElement); + return NS_OK; +} + +Element* HTMLEditor::GetTableCellElementAt(Element& aTableElement, + int32_t aRowIndex, + int32_t aColumnIndex) const { + // Let's grab the <table> element while we're retrieving layout API since + // editor developers do not watch all layout API changes. So, it may + // become unsafe. + OwningNonNull<Element> tableElement(aTableElement); + nsTableWrapperFrame* tableFrame = HTMLEditor::GetTableFrame(tableElement); + if (!tableFrame) { + NS_WARNING("There was no table layout information"); + return nullptr; + } + nsIContent* cell = tableFrame->GetCellAt(aRowIndex, aColumnIndex); + return Element::FromNodeOrNull(cell); +} + +// When all you want are the rowspan and colspan (not exposed in nsITableEditor) +nsresult HTMLEditor::GetCellSpansAt(Element* aTable, int32_t aRowIndex, + int32_t aColIndex, int32_t& aActualRowSpan, + int32_t& aActualColSpan) { + nsTableWrapperFrame* tableFrame = HTMLEditor::GetTableFrame(aTable); + if (!tableFrame) { + NS_WARNING("There was no table layout information"); + return NS_ERROR_FAILURE; + } + aActualRowSpan = tableFrame->GetEffectiveRowSpanAt(aRowIndex, aColIndex); + aActualColSpan = tableFrame->GetEffectiveColSpanAt(aRowIndex, aColIndex); + + return NS_OK; +} + +nsresult HTMLEditor::GetCellContext(Element** aTable, Element** aCell, + nsINode** aCellParent, int32_t* aCellOffset, + int32_t* aRowIndex, int32_t* aColumnIndex) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // Initialize return pointers + if (aTable) { + *aTable = nullptr; + } + if (aCell) { + *aCell = nullptr; + } + if (aCellParent) { + *aCellParent = nullptr; + } + if (aCellOffset) { + *aCellOffset = 0; + } + if (aRowIndex) { + *aRowIndex = 0; + } + if (aColumnIndex) { + *aColumnIndex = 0; + } + + RefPtr<Element> table; + RefPtr<Element> cell; + + // Caller may supply the cell... + if (aCell && *aCell) { + cell = *aCell; + } + + // ...but if not supplied, + // get cell if it's the child of selection anchor node, + // or get the enclosing by a cell + if (!cell) { + // Find a selected or enclosing table element + Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError = + GetSelectedOrParentTableElement(); + if (cellOrRowOrTableElementOrError.isErr()) { + NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed"); + return cellOrRowOrTableElementOrError.unwrapErr(); + } + if (!cellOrRowOrTableElementOrError.inspect()) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + if (HTMLEditUtils::IsTable(cellOrRowOrTableElementOrError.inspect())) { + // We have a selected table, not a cell + if (aTable) { + cellOrRowOrTableElementOrError.unwrap().forget(aTable); + } + return NS_OK; + } + if (!HTMLEditUtils::IsTableCell(cellOrRowOrTableElementOrError.inspect())) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + // We found a cell + cell = cellOrRowOrTableElementOrError.unwrap(); + } + if (aCell) { + // we don't want to cell.forget() here, because we use it below. + *aCell = do_AddRef(cell).take(); + } + + // Get containing table + table = GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::table, *cell); + if (!table) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) " + "failed"); + // Cell must be in a table, so fail if not found + return NS_ERROR_FAILURE; + } + if (aTable) { + table.forget(aTable); + } + + // Get the rest of the related data only if requested + if (aRowIndex || aColumnIndex) { + const RefPtr<PresShell> presShell{GetPresShell()}; + const CellIndexes cellIndexes(*cell, presShell); + if (NS_WARN_IF(cellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + if (aRowIndex) { + *aRowIndex = cellIndexes.mRow; + } + if (aColumnIndex) { + *aColumnIndex = cellIndexes.mColumn; + } + } + if (aCellParent) { + // Get the immediate parent of the cell + EditorRawDOMPoint atCellElement(cell); + if (NS_WARN_IF(!atCellElement.IsSet())) { + return NS_ERROR_FAILURE; + } + + if (aCellOffset) { + *aCellOffset = atCellElement.Offset(); + } + + // Now it's safe to hand over the reference to cellParent, since + // we don't need it anymore. + *aCellParent = do_AddRef(atCellElement.GetContainer()).take(); + } + + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetSelectedCells( + nsTArray<RefPtr<Element>>& aOutSelectedCellElements) { + MOZ_ASSERT(aOutSelectedCellElements.IsEmpty()); + + AutoEditActionDataSetter editActionData(*this, EditAction::eGetSelectedCells); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::GetSelectedCells() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + SelectedTableCellScanner scanner(SelectionRef()); + if (!scanner.IsInTableCellSelectionMode()) { + return NS_OK; + } + + aOutSelectedCellElements.SetCapacity(scanner.ElementsRef().Length()); + for (const OwningNonNull<Element>& cellElement : scanner.ElementsRef()) { + aOutSelectedCellElements.AppendElement(cellElement); + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetFirstSelectedCellInTable(int32_t* aRowIndex, + int32_t* aColumnIndex, + Element** aCellElement) { + if (NS_WARN_IF(!aRowIndex) || NS_WARN_IF(!aColumnIndex) || + NS_WARN_IF(!aCellElement)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData( + *this, EditAction::eGetFirstSelectedCellInTable); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING( + "HTMLEditor::GetFirstSelectedCellInTable() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + if (NS_WARN_IF(!SelectionRef().RangeCount())) { + return NS_ERROR_FAILURE; // XXX Should return NS_OK? + } + + *aRowIndex = 0; + *aColumnIndex = 0; + *aCellElement = nullptr; + RefPtr<Element> firstSelectedCellElement = + HTMLEditUtils::GetFirstSelectedTableCellElement(SelectionRef()); + if (!firstSelectedCellElement) { + return NS_OK; + } + + RefPtr<PresShell> presShell = GetPresShell(); + const CellIndexes indexes(*firstSelectedCellElement, presShell); + if (NS_WARN_IF(indexes.isErr())) { + return NS_ERROR_FAILURE; + } + + firstSelectedCellElement.forget(aCellElement); + *aRowIndex = indexes.mRow; + *aColumnIndex = indexes.mColumn; + return NS_OK; +} + +void HTMLEditor::SetSelectionAfterTableEdit(Element* aTable, int32_t aRow, + int32_t aCol, int32_t aDirection, + bool aSelected) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(!aTable) || NS_WARN_IF(Destroyed())) { + return; + } + + RefPtr<Element> cell; + bool done = false; + do { + cell = GetTableCellElementAt(*aTable, aRow, aCol); + if (cell) { + if (aSelected) { + // Reselect the cell + DebugOnly<nsresult> rv = SelectContentInternal(*cell); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::SelectContentInternal() failed, but ignored"); + return; + } + + // Set the caret to deepest first child + // but don't go into nested tables + // TODO: Should we really be placing the caret at the END + // of the cell content? + CollapseSelectionToDeepestNonTableFirstChild(cell); + return; + } + + // Setup index to find another cell in the + // direction requested, but move in other direction if already at + // beginning of row or column + switch (aDirection) { + case ePreviousColumn: + if (!aCol) { + if (aRow > 0) { + aRow--; + } else { + done = true; + } + } else { + aCol--; + } + break; + case ePreviousRow: + if (!aRow) { + if (aCol > 0) { + aCol--; + } else { + done = true; + } + } else { + aRow--; + } + break; + default: + done = true; + } + } while (!done); + + // We didn't find a cell + // Set selection to just before the table + if (aTable->GetParentNode()) { + EditorRawDOMPoint atTable(aTable); + if (NS_WARN_IF(!atTable.IsSetAndValid())) { + return; + } + DebugOnly<nsresult> rvIgnored = CollapseSelectionTo(atTable); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::CollapseSelectionTo() failed, but ignored"); + return; + } + // Last resort: Set selection to start of doc + // (it's very bad to not have a valid selection!) + DebugOnly<nsresult> rvIgnored = SetSelectionAtDocumentStart(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "HTMLEditor::SetSelectionAtDocumentStart() failed, but ignored"); +} + +NS_IMETHODIMP HTMLEditor::GetSelectedOrParentTableElement( + nsAString& aTagName, int32_t* aSelectedCount, + Element** aCellOrRowOrTableElement) { + if (NS_WARN_IF(!aSelectedCount) || NS_WARN_IF(!aCellOrRowOrTableElement)) { + return NS_ERROR_INVALID_ARG; + } + + aTagName.Truncate(); + *aCellOrRowOrTableElement = nullptr; + *aSelectedCount = 0; + + AutoEditActionDataSetter editActionData( + *this, EditAction::eGetSelectedOrParentTableElement); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING( + "HTMLEditor::GetSelectedOrParentTableElement() couldn't handle the " + "job"); + return EditorBase::ToGenericNSResult(rv); + } + + bool isCellSelected = false; + Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError = + GetSelectedOrParentTableElement(&isCellSelected); + if (cellOrRowOrTableElementOrError.isErr()) { + NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed"); + return EditorBase::ToGenericNSResult( + cellOrRowOrTableElementOrError.unwrapErr()); + } + if (!cellOrRowOrTableElementOrError.inspect()) { + return NS_OK; + } + RefPtr<Element> cellOrRowOrTableElement = + cellOrRowOrTableElementOrError.unwrap(); + + if (isCellSelected) { + aTagName.AssignLiteral("td"); + *aSelectedCount = SelectionRef().RangeCount(); + cellOrRowOrTableElement.forget(aCellOrRowOrTableElement); + return NS_OK; + } + + if (HTMLEditUtils::IsTableCell(cellOrRowOrTableElement)) { + aTagName.AssignLiteral("td"); + // Keep *aSelectedCount as 0. + cellOrRowOrTableElement.forget(aCellOrRowOrTableElement); + return NS_OK; + } + + if (HTMLEditUtils::IsTable(cellOrRowOrTableElement)) { + aTagName.AssignLiteral("table"); + *aSelectedCount = 1; + cellOrRowOrTableElement.forget(aCellOrRowOrTableElement); + return NS_OK; + } + + if (HTMLEditUtils::IsTableRow(cellOrRowOrTableElement)) { + aTagName.AssignLiteral("tr"); + *aSelectedCount = 1; + cellOrRowOrTableElement.forget(aCellOrRowOrTableElement); + return NS_OK; + } + + MOZ_ASSERT_UNREACHABLE("Which element was returned?"); + return NS_ERROR_UNEXPECTED; +} + +Result<RefPtr<Element>, nsresult> HTMLEditor::GetSelectedOrParentTableElement( + bool* aIsCellSelected /* = nullptr */) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (aIsCellSelected) { + *aIsCellSelected = false; + } + + if (NS_WARN_IF(!SelectionRef().RangeCount())) { + return Err(NS_ERROR_FAILURE); // XXX Shouldn't throw an exception? + } + + // Try to get the first selected cell, first. + RefPtr<Element> cellElement = + HTMLEditUtils::GetFirstSelectedTableCellElement(SelectionRef()); + if (cellElement) { + if (aIsCellSelected) { + *aIsCellSelected = true; + } + return cellElement; + } + + const RangeBoundary& anchorRef = SelectionRef().AnchorRef(); + if (NS_WARN_IF(!anchorRef.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + // If anchor selects a <td>, <table> or <tr>, return it. + if (anchorRef.Container()->HasChildNodes()) { + nsIContent* selectedContent = anchorRef.GetChildAtOffset(); + if (selectedContent) { + // XXX Why do we ignore <th> element in this case? + if (selectedContent->IsHTMLElement(nsGkAtoms::td)) { + // FYI: If first range selects a <tr> element, but the other selects + // a <td> element, you can reach here. + // Each cell is in its own selection range in this case. + // XXX Although, other ranges may not select cells, though. + if (aIsCellSelected) { + *aIsCellSelected = true; + } + return RefPtr<Element>(selectedContent->AsElement()); + } + if (selectedContent->IsAnyOfHTMLElements(nsGkAtoms::table, + nsGkAtoms::tr)) { + return RefPtr<Element>(selectedContent->AsElement()); + } + } + } + + if (NS_WARN_IF(!anchorRef.Container()->IsContent())) { + return RefPtr<Element>(); + } + + // Then, look for a cell element (either <td> or <th>) which contains + // the anchor container. + cellElement = GetInclusiveAncestorByTagNameInternal( + *nsGkAtoms::td, *anchorRef.Container()->AsContent()); + if (!cellElement) { + return RefPtr<Element>(); // Not in table. + } + // Don't set *aIsCellSelected to true in this case because it does NOT + // select a cell, just in a cell. + return cellElement; +} + +Result<RefPtr<Element>, nsresult> +HTMLEditor::GetFirstSelectedCellElementInTable() const { + Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError = + GetSelectedOrParentTableElement(); + if (cellOrRowOrTableElementOrError.isErr()) { + NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed"); + return cellOrRowOrTableElementOrError; + } + + if (!cellOrRowOrTableElementOrError.inspect()) { + return cellOrRowOrTableElementOrError; + } + + const RefPtr<Element>& element = cellOrRowOrTableElementOrError.inspect(); + if (!HTMLEditUtils::IsTableCell(element)) { + return RefPtr<Element>(); + } + + if (!HTMLEditUtils::IsTableRow(element->GetParentNode())) { + NS_WARNING("There was no parent <tr> element for the found cell"); + return RefPtr<Element>(); + } + + if (!HTMLEditUtils::GetClosestAncestorTableElement(*element)) { + NS_WARNING("There was no ancestor <table> element for the found cell"); + return Err(NS_ERROR_FAILURE); + } + return cellOrRowOrTableElementOrError; +} + +NS_IMETHODIMP HTMLEditor::GetSelectedCellsType(Element* aElement, + uint32_t* aSelectionType) { + if (NS_WARN_IF(!aSelectionType)) { + return NS_ERROR_INVALID_ARG; + } + *aSelectionType = 0; + + AutoEditActionDataSetter editActionData(*this, + EditAction::eGetSelectedCellsType); + nsresult rv = editActionData.CanHandleAndFlushPendingNotifications(); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("HTMLEditor::GetSelectedCellsType() couldn't handle the job"); + return EditorBase::ToGenericNSResult(rv); + } + + if (NS_WARN_IF(!SelectionRef().RangeCount())) { + return NS_ERROR_FAILURE; // XXX Should we just return NS_OK? + } + + // Be sure we have a table element + // (if aElement is null, this uses selection's anchor node) + RefPtr<Element> table; + if (aElement) { + table = GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::table, *aElement); + if (!table) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) " + "failed"); + return NS_ERROR_FAILURE; + } + } else { + table = GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table); + if (!table) { + NS_WARNING( + "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::" + "table) failed"); + return NS_ERROR_FAILURE; + } + } + + const Result<TableSize, nsresult> tableSizeOrError = + TableSize::Create(*this, *table); + if (NS_WARN_IF(tableSizeOrError.isErr())) { + return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr()); + } + const TableSize& tableSize = tableSizeOrError.inspect(); + + // Traverse all selected cells + SelectedTableCellScanner scanner(SelectionRef()); + if (!scanner.IsInTableCellSelectionMode()) { + return NS_OK; + } + + // We have at least one selected cell, so set return value + *aSelectionType = static_cast<uint32_t>(TableSelectionMode::Cell); + + // Store indexes of each row/col to avoid duplication of searches + nsTArray<int32_t> indexArray; + + const RefPtr<PresShell> presShell{GetPresShell()}; + bool allCellsInRowAreSelected = false; + for (const OwningNonNull<Element>& selectedCellElement : + scanner.ElementsRef()) { + // `MOZ_KnownLive(selectedCellElement)` is safe because `scanner` grabs + // it until it's destroyed later. + const CellIndexes selectedCellIndexes(MOZ_KnownLive(selectedCellElement), + presShell); + if (NS_WARN_IF(selectedCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + if (!indexArray.Contains(selectedCellIndexes.mColumn)) { + indexArray.AppendElement(selectedCellIndexes.mColumn); + allCellsInRowAreSelected = AllCellsInRowSelected( + table, selectedCellIndexes.mRow, tableSize.mColumnCount); + // We're done as soon as we fail for any row + if (!allCellsInRowAreSelected) { + break; + } + } + } + + if (allCellsInRowAreSelected) { + *aSelectionType = static_cast<uint32_t>(TableSelectionMode::Row); + return NS_OK; + } + // Test for columns + + // Empty the indexArray + indexArray.Clear(); + + // Start at first cell again + bool allCellsInColAreSelected = false; + for (const OwningNonNull<Element>& selectedCellElement : + scanner.ElementsRef()) { + // `MOZ_KnownLive(selectedCellElement)` is safe because `scanner` grabs + // it until it's destroyed later. + const CellIndexes selectedCellIndexes(MOZ_KnownLive(selectedCellElement), + presShell); + if (NS_WARN_IF(selectedCellIndexes.isErr())) { + return NS_ERROR_FAILURE; + } + + if (!indexArray.Contains(selectedCellIndexes.mRow)) { + indexArray.AppendElement(selectedCellIndexes.mColumn); + allCellsInColAreSelected = AllCellsInColumnSelected( + table, selectedCellIndexes.mColumn, tableSize.mRowCount); + // We're done as soon as we fail for any column + if (!allCellsInRowAreSelected) { + break; + } + } + } + if (allCellsInColAreSelected) { + *aSelectionType = static_cast<uint32_t>(TableSelectionMode::Column); + } + + return NS_OK; +} + +bool HTMLEditor::AllCellsInRowSelected(Element* aTable, int32_t aRowIndex, + int32_t aNumberOfColumns) { + if (NS_WARN_IF(!aTable)) { + return false; + } + + for (int32_t col = 0; col < aNumberOfColumns;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, col); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return false; + } + + // If no cell, we may have a "ragged" right edge, so return TRUE only if + // we already found a cell in the row. + // XXX So, this does not assume that CellData returns error when just + // not found a cell. Fix this later. + if (!cellData.mElement) { + NS_WARNING("CellData didn't set mElement"); + return cellData.mCurrent.mColumn > 0; + } + + // Return as soon as a non-selected cell is found. + // XXX Odd, this is testing if each cell element is selected. Why do + // we need to warn if it's false?? + if (!cellData.mIsSelected) { + NS_WARNING("CellData didn't set mIsSelected"); + return false; + } + + MOZ_ASSERT(col < cellData.NextColumnIndex()); + col = cellData.NextColumnIndex(); + } + return true; +} + +bool HTMLEditor::AllCellsInColumnSelected(Element* aTable, int32_t aColIndex, + int32_t aNumberOfRows) { + if (NS_WARN_IF(!aTable)) { + return false; + } + + for (int32_t row = 0; row < aNumberOfRows;) { + const auto cellData = + CellData::AtIndexInTableElement(*this, *aTable, row, aColIndex); + if (NS_WARN_IF(cellData.FailedOrNotFound())) { + return false; + } + + // If no cell, we must have a "ragged" right edge on the last column so + // return TRUE only if we already found a cell in the row. + // XXX So, this does not assume that CellData returns error when just + // not found a cell. Fix this later. + if (!cellData.mElement) { + NS_WARNING("CellData didn't set mElement"); + return cellData.mCurrent.mRow > 0; + } + + // Return as soon as a non-selected cell is found. + // XXX Odd, this is testing if each cell element is selected. Why do + // we need to warn if it's false?? + if (!cellData.mIsSelected) { + NS_WARNING("CellData didn't set mIsSelected"); + return false; + } + + MOZ_ASSERT(row < cellData.NextRowIndex()); + row = cellData.NextRowIndex(); + } + return true; +} + +bool HTMLEditor::IsEmptyCell(dom::Element* aCell) { + MOZ_ASSERT(aCell); + + // Check if target only contains empty text node or <br> + nsCOMPtr<nsINode> cellChild = aCell->GetFirstChild(); + if (!cellChild) { + return false; + } + + nsCOMPtr<nsINode> nextChild = cellChild->GetNextSibling(); + if (nextChild) { + return false; + } + + // We insert a single break into a cell by default + // to have some place to locate a cursor -- it is dispensable + if (cellChild->IsHTMLElement(nsGkAtoms::br)) { + return true; + } + + // Or check if no real content + return HTMLEditUtils::IsEmptyNode( + *cellChild, {EmptyCheckOption::TreatSingleBRElementAsVisible}); +} + +} // namespace mozilla |