Source code

Revision control

Other Tools

/* -*- 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 "mozilla/HTMLEditor.h"
#include "HTMLEditUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/EditAction.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorUtils.h"
#include "mozilla/FlushType.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:
RefPtr<HTMLEditor> mHTMLEditor;
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) {
MOZ_KnownLive(mHTMLEditor)
->SetSelectionAfterTableEdit(MOZ_KnownLive(mTable), mRow, mCol,
mDirection, mSelected);
}
}
// This is needed to abort the caret reset in the destructor
// when one method yields control to another
void CancelSetCaret() {
mHTMLEditor = nullptr;
mTable = nullptr;
}
};
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");
}
// Don't let Rules System change the selection.
AutoTransactionsConserveSelection dontChangeSelection(*this);
nsresult rv = InsertNodeWithTransaction(*newCell, pointToInsert);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertNodeWithTransaction() failed");
return rv;
}
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) {
AutoEditActionDataSetter editActionData(*this,
EditAction::eInsertTableCellElement);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
rv = InsertTableCellsWithTransaction(
aNumberOfCellsToInsert, aInsertAfterSelectedCell
? InsertPosition::eAfterSelectedCell
: InsertPosition::eBeforeSelectedCell);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::InsertTableCellsWithTransaction() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::InsertTableCellsWithTransaction(
int32_t aNumberOfCellsToInsert, InsertPosition aInsertPosition) {
MOZ_ASSERT(IsEditActionDataAvailable());
RefPtr<Element> table;
RefPtr<Element> curCell;
nsCOMPtr<nsINode> cellParent;
int32_t cellOffset, startRowIndex, startColIndex;
nsresult rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(curCell),
getter_AddRefs(cellParent), &cellOffset,
&startRowIndex, &startColIndex);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!table || !curCell) {
NS_WARNING(
"HTMLEditor::GetCellContext() didn't return <table> and/or cell");
// Don't fail if no cell found.
return NS_OK;
}
// Get more data for current cell in row we are inserting at since we need
// colspan value.
IgnoredErrorResult ignoredError;
CellData cellDataAtSelection(*this, *table, startRowIndex, startColIndex,
ignoredError);
if (cellDataAtSelection.FailedOrNotFound()) {
NS_WARNING("CellData couldn't find selected cell");
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(!ignoredError.Failed());
MOZ_ASSERT(curCell == cellDataAtSelection.mElement);
int32_t newCellIndex;
switch (aInsertPosition) {
case InsertPosition::eBeforeSelectedCell:
newCellIndex = cellDataAtSelection.mCurrent.mColumn;
break;
case InsertPosition::eAfterSelectedCell:
MOZ_ASSERT(!cellDataAtSelection.IsSpannedFromOtherRowOrColumn());
newCellIndex = cellDataAtSelection.NextColumnIndex();
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid InsertPosition");
}
AutoPlaceholderBatch treateAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
// Prevent auto insertion of BR in new cell until we're done
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eInsertNode, 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");
ignoredError.SuppressException();
// We control selection resetting after the insert.
AutoSelectionSetterAfterTableEdit setCaret(
*this, table, cellDataAtSelection.mCurrent.mRow, newCellIndex,
ePreviousColumn, false);
// So, suppress Rules System selection munging.
AutoTransactionsConserveSelection dontChangeSelection(*this);
EditorDOMPoint pointToInsert(cellParent, cellOffset);
if (NS_WARN_IF(!pointToInsert.IsSet())) {
return NS_ERROR_FAILURE;
}
if (aInsertPosition == InsertPosition::eAfterSelectedCell) {
DebugOnly<bool> advanced = pointToInsert.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to move insertion point after the cell");
}
for (int32_t i = 0; i < aNumberOfCellsToInsert; i++) {
RefPtr<Element> newCell = CreateElementWithDefaults(*nsGkAtoms::td);
if (!newCell) {
NS_WARNING("HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed");
return NS_ERROR_FAILURE;
}
AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
nsresult rv = InsertNodeWithTransaction(*newCell, pointToInsert);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return rv;
}
}
return NS_OK;
}
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::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
ErrorResult error;
RefPtr<Element> firstRowElement =
GetFirstTableRowElement(*aTableOrElementInTable, error);
NS_WARNING_ASSERTION(!error.Failed(),
"HTMLEditor::GetFirstTableRowElement() failed");
firstRowElement.forget(aFirstRowElement);
return EditorBase::ToGenericNSResult(error.StealNSResult());
}
Element* HTMLEditor::GetFirstTableRowElement(Element& aTableOrElementInTable,
ErrorResult& aRv) const {
MOZ_ASSERT(!aRv.Failed());
Element* tableElement = GetInclusiveAncestorByTagNameInternal(
*nsGkAtoms::table, aTableOrElementInTable);
// If the element is not in <table>, return error.
if (!tableElement) {
NS_WARNING(
"HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) "
"failed");
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
for (nsIContent* tableChild = tableElement->GetFirstChild(); tableChild;
tableChild = tableChild->GetNextSibling()) {
if (tableChild->IsHTMLElement(nsGkAtoms::tr)) {
// Found a row directly under <table>
return 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 tableSectionChild->AsElement();
}
}
}
}
// Don't return error when there is no <tr> element in the <table>.
return nullptr;
}
Element* HTMLEditor::GetNextTableRowElement(Element& aTableRowElement,
ErrorResult& aRv) const {
MOZ_ASSERT(!aRv.Failed());
if (NS_WARN_IF(!aTableRowElement.IsHTMLElement(nsGkAtoms::tr))) {
aRv.Throw(NS_ERROR_INVALID_ARG);
return nullptr;
}
for (nsIContent* maybeNextRow = aTableRowElement.GetNextSibling();
maybeNextRow; maybeNextRow = maybeNextRow->GetNextSibling()) {
if (maybeNextRow->IsHTMLElement(nsGkAtoms::tr)) {
return 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");
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
// 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 nullptr;
}
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 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 maybeNextTableSection->AsElement();
}
}
// Don't return error when the given <tr> element is the last <tr> element in
// the <table>.
return nullptr;
}
nsresult HTMLEditor::GetLastCellInRow(nsINode* aRowNode, nsINode** aCellNode) {
if (NS_WARN_IF(!aCellNode)) {
return NS_ERROR_INVALID_ARG;
}
*aCellNode = nullptr;
if (NS_WARN_IF(!aRowNode)) {
return NS_ERROR_INVALID_ARG;
}
nsCOMPtr<nsINode> rowChild = aRowNode->GetLastChild();
while (rowChild && !HTMLEditUtils::IsTableCell(rowChild)) {
// Skip over textnodes
rowChild = rowChild->GetPreviousSibling();
}
if (rowChild) {
rowChild.forget(aCellNode);
return NS_OK;
}
// If here, cell was not found
return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
}
NS_IMETHODIMP HTMLEditor::InsertTableColumn(int32_t aNumberOfColumnsToInsert,
bool aInsertAfterSelectedCell) {
AutoEditActionDataSetter editActionData(*this,
EditAction::eInsertTableColumn);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
rv = InsertTableColumnsWithTransaction(
aNumberOfColumnsToInsert, aInsertAfterSelectedCell
? InsertPosition::eAfterSelectedCell
: InsertPosition::eBeforeSelectedCell);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::InsertTableColumnsWithTransaction() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::InsertTableColumnsWithTransaction(
int32_t aNumberOfColumnsToInsert, InsertPosition aInsertPosition) {
MOZ_ASSERT(IsEditActionDataAvailable());
RefPtr<Element> table;
RefPtr<Element> curCell;
int32_t startRowIndex, startColIndex;
nsresult rv =
GetCellContext(getter_AddRefs(table), getter_AddRefs(curCell), nullptr,
nullptr, &startRowIndex, &startColIndex);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::GetCellContext() failed");
return rv;
}
if (!table || !curCell) {
NS_WARNING(
"HTMLEditor::GetCellContext() didn't return <table> and/or cell");
// Don't fail if no cell found.
return NS_OK;
}
// Get more data for current cell, we need rowspan value.
IgnoredErrorResult ignoredError;
CellData cellDataAtSelection(*this, *table, startRowIndex, startColIndex,
ignoredError);
if (cellDataAtSelection.FailedOrNotFound()) {
NS_WARNING("CellData couldn't find selected cell");
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(!ignoredError.Failed());
MOZ_ASSERT(curCell == cellDataAtSelection.mElement);
ErrorResult error;
TableSize tableSize(*this, *table, error);
if (error.Failed()) {
NS_WARNING("TableSize failed");
return error.StealNSResult();
}
// Should not be empty since we've already found a cell.
MOZ_ASSERT(!tableSize.IsEmpty());
AutoPlaceholderBatch treateAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
// Prevent auto insertion of <br> element in new cell until we're done.
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eInsertNode, 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");
ignoredError.SuppressException();
switch (aInsertPosition) {
case InsertPosition::eBeforeSelectedCell:
break;
case InsertPosition::eAfterSelectedCell:
// Use column after current cell.
startColIndex += cellDataAtSelection.mEffectiveColSpan;
// Detect when user is adding after a colspan=0 case.
// Assume they want to stop the "0" behavior and really add a new column.
// Thus we set the colspan to its true value.
if (!cellDataAtSelection.mColSpan) {
DebugOnly<nsresult> rvIgnored =
SetColSpan(MOZ_KnownLive(cellDataAtSelection.mElement),
cellDataAtSelection.mEffectiveColSpan);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetColSpan() failed, but ignored");
}
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid InsertPosition");
}
// We control selection resetting after the insert.
AutoSelectionSetterAfterTableEdit setCaret(
*this, table, cellDataAtSelection.mCurrent.mRow, startColIndex,
ePreviousRow, false);
// 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 (startColIndex >= tableSize.mColumnCount) {
DebugOnly<nsresult> rv = NormalizeTableInternal(*table);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::NormalizeTableInternal() failed, but ignored");
}
RefPtr<Element> rowElement;
for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) {
if (startColIndex < tableSize.mColumnCount) {
// We are inserting before an existing column.
CellData cellData(*this, *table, rowIndex, startColIndex, ignoredError);
if (cellData.FailedOrNotFound()) {
NS_WARNING("CellData failed");
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(!ignoredError.Failed());
// 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 (cellData.IsSpannedFromOtherColumn()) {
// If we have a cell spanning this location, simply increase its
// colspan to keep table rectangular.
// Note: we do nothing if colsspan=0, since it should automatically
// span the new column.
if (cellData.mColSpan > 0) {
DebugOnly<nsresult> rvIgnored =
SetColSpan(MOZ_KnownLive(cellData.mElement),
cellData.mColSpan + aNumberOfColumnsToInsert);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetColSpan() failed, but ignored");
}
continue;
}
// Simply set selection to the current cell. So, we can let
// InsertTableCellsWithTransaction() do the work. Insert a new cell
// before current one.
CollapseSelectionToStartOf(MOZ_KnownLive(*cellData.mElement),
ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::CollapseSelectionToStartOf() failed, but ignored");
ignoredError.SuppressException();
rv = InsertTableCellsWithTransaction(aNumberOfColumnsToInsert,
InsertPosition::eBeforeSelectedCell);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::InsertTableCellsWithTransaction() "
"failed, but might be ignored");
continue;
}
// Get current row and append new cells after last cell in row
if (!rowIndex) {
rowElement = GetFirstTableRowElement(*table, error);
if (error.Failed()) {
NS_WARNING("HTMLEditor::GetFirstTableRowElement() failed");
return error.StealNSResult();
}
if (!rowElement) {
NS_WARNING("There was no table row");
continue;
}
} else {
if (!rowElement) {
NS_WARNING("Have not found table row yet");
// XXX Looks like that when rowIndex is 0, startColIndex is always
// same as or larger than tableSize.mColumnCount. Is it true?
return NS_ERROR_FAILURE;
}
rowElement = GetNextTableRowElement(*rowElement, error);
if (error.Failed()) {
NS_WARNING("HTMLEditor::GetNextTableRowElement() failed");
return error.StealNSResult();
}
if (!rowElement) {
NS_WARNING(
"HTMLEditor::GetNextTableRowElement() didn't return <tr> element");
continue;
}
}
nsCOMPtr<nsINode> lastCellNode;
rv = GetLastCellInRow(rowElement, getter_AddRefs(lastCellNode));
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::GetLastCellInRow() failed");
return rv;
}
if (!lastCellNode) {
NS_WARNING("HTMLEditor::GetLastCellInRow() didn't return cell");
return NS_ERROR_FAILURE;
}
// Simply add same number of cells to each row. Although tempted to check
// cell indexes for current cell, the effects of colspan > 1 in some cells
// makes this futile. We must use NormalizeTableInternal() first to assure
// that there are cells in each cellmap location.
CollapseSelectionToStartOf(*lastCellNode, ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::CollapseSelectionToStartOf() failed, but ignored");
ignoredError.SuppressException();
rv = InsertTableCellsWithTransaction(aNumberOfColumnsToInsert,
InsertPosition::eAfterSelectedCell);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::InsertTableCellsWithTransaction() "
"failed, but might be ignored");
}
// XXX This is perhaps the result of the last call of
// InsertTableCellsWithTransaction().
return rv;
}
NS_IMETHODIMP HTMLEditor::InsertTableRow(int32_t aNumberOfRowsToInsert,
bool aInsertAfterSelectedCell) {
AutoEditActionDataSetter editActionData(*this,
EditAction::eInsertTableRowElement);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
rv = InsertTableRowsWithTransaction(
aNumberOfRowsToInsert, aInsertAfterSelectedCell
? InsertPosition::eAfterSelectedCell
: InsertPosition::eBeforeSelectedCell);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::InsertTableRowsWithTransaction() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::InsertTableRowsWithTransaction(
int32_t aNumberOfRowsToInsert, InsertPosition aInsertPosition) {
MOZ_ASSERT(IsEditActionDataAvailable());
RefPtr<Element> table;
RefPtr<Element> curCell;
int32_t startRowIndex, startColIndex;
nsresult rv =
GetCellContext(getter_AddRefs(table), getter_AddRefs(curCell), nullptr,
nullptr, &startRowIndex, &startColIndex);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::GetCellContext() failed");
return rv;
}
if (!table || !curCell) {
NS_WARNING(
"HTMLEditor::GetCellContext() didn't return <table> and/or cell");
// Don't fail if no cell found.
return NS_OK;
}
// Get more data for current cell in row we are inserting at because we need
// colspan.
IgnoredErrorResult ignoredError;
CellData cellDataAtSelection(*this, *table, startRowIndex, startColIndex,
ignoredError);
ignoredError.SuppressException();
if (cellDataAtSelection.FailedOrNotFound()) {
NS_WARNING("CellData couldn't find selected cell");
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(curCell == cellDataAtSelection.mElement);
ErrorResult error;
TableSize tableSize(*this, *table, error);
if (error.Failed()) {
NS_WARNING("TableSize failed");
return error.StealNSResult();
}
// Should not be empty since we've already found a cell.
MOZ_ASSERT(!tableSize.IsEmpty());
AutoPlaceholderBatch treateAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
// Prevent auto insertion of BR in new cell until we're done
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eInsertNode, 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");
switch (aInsertPosition) {
case InsertPosition::eBeforeSelectedCell:
break;
case InsertPosition::eAfterSelectedCell:
// Use row after current cell.
startRowIndex += cellDataAtSelection.mEffectiveRowSpan;
// Detect when user is adding after a rowspan=0 case.
// Assume they want to stop the "0" behavior and really add a new row.
// Thus we set the rowspan to its true value.
if (!cellDataAtSelection.mRowSpan) {
DebugOnly<nsresult> rvIgnored =
SetRowSpan(MOZ_KnownLive(cellDataAtSelection.mElement),
cellDataAtSelection.mEffectiveRowSpan);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetRowSpan() failed, but ignored");
}
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid InsertPosition");
}
// We control selection resetting after the insert.
AutoSelectionSetterAfterTableEdit setCaret(
*this, table, startRowIndex, cellDataAtSelection.mCurrent.mColumn,
ePreviousColumn, false);
// Suppress Rules System selection munging.
AutoTransactionsConserveSelection dontChangeSelection(*this);
RefPtr<Element> cellForRowParent;
int32_t cellsInRow = 0;
if (startRowIndex < tableSize.mRowCount) {
// We are inserting above an existing row. Get each cell in the insert
// row to adjust for colspan effects while we count how many cells are
// needed.
CellData cellData;
for (int32_t colIndex = 0;; colIndex = cellData.NextColumnIndex()) {
cellData.Update(*this, *table, startRowIndex, colIndex, ignoredError);
if (cellData.FailedOrNotFound()) {
break; // Perhaps, we reach end of the row.
}
// XXX So, this is impossible case. Will be removed.
if (!cellData.mElement) {
NS_WARNING("CellData::Update() succeeded, but didn't set mElement");
break;
}
if (cellData.IsSpannedFromOtherRow()) {
// We have a cell spanning this location. Increase its rowspan.
// Note that if rowspan is 0, we do nothing since that cell should
// automatically extend into the new row.
if (cellData.mRowSpan > 0) {
DebugOnly<nsresult> rvIgnored =
SetRowSpan(MOZ_KnownLive(cellData.mElement),
cellData.mRowSpan + aNumberOfRowsToInsert);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetRowSpan() failed, but ignored");
}
continue;
}
cellsInRow += cellData.mEffectiveColSpan;
if (!cellForRowParent) {
// FYI: Don't use std::move() here since NextColumnIndex() needs it.
cellForRowParent = cellData.mElement;
}
MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
}
} else {
// We are adding a new row after all others. If it weren't for colspan=0
// effect, we could simply use tableSize.mColumnCount for number of new
// cells...
// XXX colspan=0 support has now been removed in table layout so maybe this
// can be cleaned up now? (bug 1243183)
cellsInRow = tableSize.mColumnCount;
// but we must compensate for all cells with rowspan = 0 in the last row.
const int32_t kLastRowIndex = tableSize.mRowCount - 1;
CellData cellData;
for (int32_t colIndex = 0;; colIndex = cellData.NextColumnIndex()) {
cellData.Update(*this, *table, kLastRowIndex, colIndex, ignoredError);
if (cellData.FailedOrNotFound()) {
break; // Perhaps, we reach end of the row.
}
if (!cellData.mRowSpan) {
MOZ_ASSERT(cellsInRow >= cellData.mEffectiveColSpan);
cellsInRow -= cellData.mEffectiveColSpan;
}
// Save cell from the last row that we will use below
if (!cellForRowParent && !cellData.IsSpannedFromOtherRow()) {
// FYI: Don't use std::move() here since NextColumnIndex() needs it.
cellForRowParent = cellData.mElement;
}
MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
}
}
if (!cellsInRow) {
NS_WARNING("There was no cell element in the last row");
return NS_OK;
}
if (!cellForRowParent) {
NS_WARNING("There was no cell element for the <tr> element");
return NS_ERROR_FAILURE;
}
Element* parentRow =
GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::tr, *cellForRowParent);
if (!parentRow) {
NS_WARNING(
"HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::tr) "
"failed");
return NS_ERROR_FAILURE;
}
// The row parent and offset where we will insert new row.
EditorDOMPoint pointToInsert(parentRow);
if (NS_WARN_IF(!pointToInsert.IsSet())) {
return NS_ERROR_FAILURE;
}
// Adjust for when adding past the end.
if (aInsertPosition == InsertPosition::eAfterSelectedCell &&
startRowIndex >= tableSize.mRowCount) {
DebugOnly<bool> advanced = pointToInsert.AdvanceOffset();
NS_WARNING_ASSERTION(advanced, "Failed to advance offset");
}
for (int32_t row = 0; row < aNumberOfRowsToInsert; row++) {
// Create a new row
RefPtr<Element> newRow = CreateElementWithDefaults(*nsGkAtoms::tr);
if (!newRow) {
NS_WARNING("HTMLEditor::CreateElementWithDefaults(nsGkAtoms::tr) failed");
return NS_ERROR_FAILURE;
}
for (int32_t i = 0; i < cellsInRow; i++) {
RefPtr<Element> newCell = CreateElementWithDefaults(*nsGkAtoms::td);
if (!newCell) {
NS_WARNING(
"HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed");
return NS_ERROR_FAILURE;
}
newRow->AppendChild(*newCell, error);
if (error.Failed()) {
NS_WARNING("nsINode::AppendChild() failed");
return error.StealNSResult();
}
}
AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
nsresult rv = InsertNodeWithTransaction(*newRow, pointToInsert);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return rv;
}
}
// SetSelectionAfterTableEdit from AutoSelectionSetterAfterTableEdit will
// access frame selection, so we need reframe.
// Because GetTableCellElementAt() depends on frame.
if (RefPtr<PresShell> presShell = GetPresShell()) {
presShell->FlushPendingNotifications(FlushType::Frames);
}
return NS_OK;
}
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 (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);
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 (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);
// 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());
ErrorResult error;
TableSize tableSize(*this, *table, error);
if (error.Failed()) {
NS_WARNING("TableSize failed");
return error.StealNSResult();
}
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("HTMLEditor::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.
CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]),
presShell, error);
if (error.Failed()) {
NS_WARNING("CellIndexes failed");
return error.StealNSResult();
}
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;
}
CellIndexes nextSelectedCellIndexes(*selectedCellElement, presShell,
error);
if (error.Failed()) {
NS_WARNING("CellIndexes failed");
return error.StealNSResult();
}
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;
}
CellIndexes nextSelectedCellIndexes(*selectedCellElement, presShell,
error);
if (error.Failed()) {
NS_WARNING("CellIndexes failed");
return error.StealNSResult();
}
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("HTMLEditor::DeleteNodeWithTransaction() failed");
return rv;
}
selectedCellElement = scanner.GetNextElement();
if (!selectedCellElement) {
return NS_OK;
}
CellIndexes nextCellIndexes(*selectedCellElement, presShell, error);
if (error.Failed()) {
NS_WARNING("CellIndexes failed");
return error.StealNSResult();
}
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 (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);
// 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.
ErrorResult error;
CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]),
presShell, error);
if (error.Failed()) {
NS_WARNING("CellIndexes failed");
return error.StealNSResult();
}
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 (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;
}
ErrorResult error;
TableSize tableSize(*this, *table, error);
if (error.Failed()) {
NS_WARNING("TableSize failed");
return error.StealNSResult();
}
AutoPlaceholderBatch treateAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
// 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");
// 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.
CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]),
presShell, error);
if (error.Failed()) {
NS_WARNING("CellIndexes failed");
return error.StealNSResult();
}
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]) {
CellIndexes cellIndexes(*selectedCellElement, presShell, error);
if (error.Failed()) {
NS_WARNING("CellIndexes failed");
return error.StealNSResult();
}
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;
}
CellIndexes cellIndexes(*selectedCellElement, presShell, error);
if (error.Failed()) {
NS_WARNING("CellIndexes failed");
return error.StealNSResult();
}
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());
// XXX Why don't this method remove proper <col> (and <colgroup>)?
ErrorResult error;
IgnoredErrorResult ignoredError;
for (int32_t rowIndex = 0;; rowIndex++) {
CellData cellData(*this, aTableElement, rowIndex, aColumnIndex,
ignoredError);
// 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(MOZ_KnownLive(cellData.mElement), cellData.mColSpan - 1);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"HTMLEdit