summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/HTMLTableEditor.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /editor/libeditor/HTMLTableEditor.cpp
parentInitial commit. (diff)
downloadfirefox-upstream/124.0.1.tar.xz
firefox-upstream/124.0.1.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'editor/libeditor/HTMLTableEditor.cpp')
-rw-r--r--editor/libeditor/HTMLTableEditor.cpp4601
1 files changed, 4601 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLTableEditor.cpp b/editor/libeditor/HTMLTableEditor.cpp
new file mode 100644
index 0000000000..ea470b2203
--- /dev/null
+++ b/editor/libeditor/HTMLTableEditor.cpp
@@ -0,0 +1,4601 @@
+/* -*- 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,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible});
+}
+
+} // namespace mozilla