Source code

Revision control

Copy as Markdown

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 "HTMLEditor.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);
CreateElementResult insertNewCellResult =
InsertNodeWithTransaction<Element>(*newCell, pointToInsert);
if (insertNewCellResult.isErr()) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return insertNewCellResult.unwrapErr();
}
// Because of dontChangeSelection, we've never allowed to transactions to
// update selection here.
insertNewCellResult.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");
}
const CreateElementResult insertCellElementResult =
InsertTableCellsWithTransaction(pointToInsert, aNumberOfCellsToInsert);
if (insertCellElementResult.isErr()) {
NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed");
return EditorBase::ToGenericNSResult(insertCellElementResult.inspectErr());
}
// We don't need to modify selection here.
insertCellElementResult.IgnoreCaretPointSuggestion();
return NS_OK;
}
CreateElementResult 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 CreateElementResult(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 CreateElementResult(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;
}
CreateElementResult insertNewCellResult = InsertNodeWithTransaction(
*newCell,
referenceContent
? EditorDOMPoint(referenceContent)
: EditorDOMPoint::AtEndOf(*aPointToInsert.ContainerAsElement()));
if (insertNewCellResult.isErr()) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return insertNewCellResult.unwrapErr();
}
lastCellElement = insertNewCellResult.UnwrapNewNode();
if (!firstCellElement) {
firstCellElement = lastCellElement;
}
// Because of dontChangeSelection, we've never allowed to transactions
// to update selection here.
insertNewCellResult.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 CreateElementResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
return CreateElementResult(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.ContainerAsElement());
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.ContainerAsElement())
: 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.ContainerAsElement())
: 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);
}
CreateElementResult insertCellElementsResult =
InsertTableCellsWithTransaction(pointToInsert,
aNumberOfColumnsToInsert);
if (insertCellElementsResult.isErr()) {
NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed");
return Err(insertCellElementsResult.unwrapErr());
}
// We'll update selection later into the first inserted cell element in
// the current row.
insertCellElementsResult.IgnoreCaretPointSuggestion();
if (pointToInsert.ContainerAsElement() ==
aPointToInsert.ContainerAsElement()) {
cellElementToPutCaret = insertCellElementsResult.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 < cellData.mCurrent.mColumn) {
offsetInTRElementToPutCaret++;
}
MOZ_ASSERT(colIndex < cellDataInLastRow.NextColumnIndex());
colIndex = cellData.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);
CreateElementResult insertNewRowResult =
InsertNodeWithTransaction<Element>(*newRowElement, pointToInsert);
if (insertNewRowResult.isErr() && !insertNewRowResult.EditorDestroyed()) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return Err(insertNewRowResult.unwrapErr());
}
firstInsertedTRElement = std::move(newRowElement);
// We'll update selection later.
insertNewRowResult.IgnoreCaretPointSuggestion();
}
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;
}