/* -*- 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/. */
usingnamespace 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;
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;
}
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;
}
// 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);
}
// 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
if (NS_WARN_IF(!aCell)) { return NS_ERROR_INVALID_ARG;
}
// And the parent and offsets needed to do an insert
EditorDOMPoint pointToInsert(aCell); if (NS_WARN_IF(!pointToInsert.IsSet())) { return NS_ERROR_INVALID_ARG;
}
RefPtr<Element> newCell =
CreateElementWithDefaults(aIsHeader ? *nsGkAtoms::th : *nsGkAtoms::td); if (!newCell) {
NS_WARNING( "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::th or td) failed"); return NS_ERROR_FAILURE;
}
// Optional: return new cell created if (aNewCell) {
*aNewCell = do_AddRef(newCell).take();
}
if (aRowSpan > 1) { // Note: Do NOT use editor transaction for this
nsAutoString newRowSpan;
newRowSpan.AppendInt(aRowSpan, 10);
DebugOnly<nsresult> rvIgnored = newCell->SetAttr(
kNameSpaceID_None, nsGkAtoms::rowspan, newRowSpan, true);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored), "Element::SetAttr(nsGkAtoms::rawspan) failed, but ignored");
} if (aColSpan > 1) { // Note: Do NOT use editor transaction for this
nsAutoString newColSpan;
newColSpan.AppendInt(aColSpan, 10);
DebugOnly<nsresult> rvIgnored = newCell->SetAttr(
kNameSpaceID_None, nsGkAtoms::colspan, newColSpan, true);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored), "Element::SetAttr(nsGkAtoms::colspan) failed, but ignored");
} if (aAfter) {
DebugOnly<bool> advanced = pointToInsert.AdvanceOffset();
NS_WARNING_ASSERTION(advanced, "Failed to advance offset to after the old cell");
}
// TODO: Remove AutoTransactionsConserveSelection here. It's not necessary // in normal cases. However, it may be required for nested edit // actions which may be caused by legacy mutation event listeners or // chrome script.
AutoTransactionsConserveSelection dontChangeSelection(*this);
Result<CreateElementResult, nsresult> insertNewCellResult =
InsertNodeWithTransaction<Element>(*newCell, pointToInsert); if (MOZ_UNLIKELY(insertNewCellResult.isErr())) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertNewCellResult.unwrapErr();
} // Because of dontChangeSelection, we've never allowed to transactions to // update selection here.
insertNewCellResult.inspect().IgnoreCaretPointSuggestion(); return NS_OK;
}
if (!cellElementOrError.inspect()) { return NS_OK;
}
EditorDOMPoint pointToInsert(cellElementOrError.inspect()); if (!pointToInsert.IsSet()) {
NS_WARNING("Found an orphan cell element"); return NS_ERROR_FAILURE;
} if (aInsertAfterSelectedCell && !pointToInsert.IsEndOfContainer()) {
DebugOnly<bool> advanced = pointToInsert.AdvanceOffset();
NS_WARNING_ASSERTION(
advanced, "Failed to set insertion point after current cell, but ignored");
}
Result<CreateElementResult, nsresult> insertCellElementResult =
InsertTableCellsWithTransaction(pointToInsert, aNumberOfCellsToInsert); if (MOZ_UNLIKELY(insertCellElementResult.isErr())) {
NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed"); return EditorBase::ToGenericNSResult(insertCellElementResult.unwrapErr());
} // We don't need to modify selection here.
insertCellElementResult.inspect().IgnoreCaretPointSuggestion(); return NS_OK;
}
if (!HTMLEditUtils::IsTableRow(aPointToInsert.GetContainer())) {
NS_WARNING("Tried to insert cell elements to non-
element"); return Err(NS_ERROR_FAILURE);
}
AutoPlaceholderBatch treateAsOneTransaction(
*this, ScrollSelectionIntoView::Yes, __FUNCTION__); // Prevent auto insertion of BR in new cell until we're done // XXX Why? I think that we should insert <br> element for every cell // **before** inserting new cell into the <tr> element.
IgnoredErrorResult error;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eInsertNode, nsIEditor::eNext, error); if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return Err(error.StealNSResult());
}
NS_WARNING_ASSERTION(
!error.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
error.SuppressException();
// Put caret into the cell before the first inserting cell, or the first // table cell in the row.
RefPtr<Element> cellToPutCaret =
aPointToInsert.IsEndOfContainer()
? nullptr
: HTMLEditUtils::GetPreviousTableCellElementSibling(
*aPointToInsert.GetChild());
RefPtr<Element> firstCellElement, lastCellElement;
nsresult rv = [&]() MOZ_CAN_RUN_SCRIPT { // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary // in normal cases. However, it may be required for nested edit // actions which may be caused by legacy mutation event listeners or // chrome script.
AutoTransactionsConserveSelection dontChangeSelection(*this);
// Block legacy mutation events for making this job simpler.
nsAutoScriptBlockerSuppressNodeRemoved blockToRunScript;
// If there is a child to put a cell, we need to put all cell elements // before it. Therefore, creating `EditorDOMPoint` with the child element // is safe. Otherwise, we need to try to append cell elements in the row. // Therefore, using `EditorDOMPoint::AtEndOf()` is safe. Note that it's // not safe to creat it once because the offset and child relation in the // point becomes invalid after inserting a cell element.
nsIContent* referenceContent = aPointToInsert.GetChild(); for ([[maybe_unused]] constauto i :
IntegerRange<uint32_t>(aNumberOfCellsToInsert)) {
RefPtr<Element> newCell = CreateElementWithDefaults(*nsGkAtoms::td); if (!newCell) {
NS_WARNING( "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed"); return NS_ERROR_FAILURE;
}
Result<CreateElementResult, nsresult> insertNewCellResult =
InsertNodeWithTransaction(
*newCell, referenceContent
? EditorDOMPoint(referenceContent)
: EditorDOMPoint::AtEndOf(
*aPointToInsert.ContainerAs<Element>())); if (MOZ_UNLIKELY(insertNewCellResult.isErr())) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertNewCellResult.unwrapErr();
}
CreateElementResult unwrappedInsertNewCellResult =
insertNewCellResult.unwrap();
lastCellElement = unwrappedInsertNewCellResult.UnwrapNewNode(); if (!firstCellElement) {
firstCellElement = lastCellElement;
} // Because of dontChangeSelection, we've never allowed to transactions // to update selection here.
unwrappedInsertNewCellResult.IgnoreCaretPointSuggestion(); if (!cellToPutCaret) {
cellToPutCaret = std::move(newCell); // This is first cell in the row.
}
}
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>();
}
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. elseif (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
}
if (!HTMLEditUtils::IsTableRow(aPointToInsert.GetContainer())) {
NS_WARNING("Tried to insert columns to non-
element"); return NS_ERROR_FAILURE;
}
const RefPtr<Element> tableElement =
HTMLEditUtils::GetClosestAncestorTableElement(
*aPointToInsert.ContainerAs<Element>()); if (!tableElement) {
NS_WARNING("There was no ancestor
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. constbool insertAfterPreviousCell = [&]() { if (!aPointToInsert.IsEndOfContainer() &&
HTMLEditUtils::IsTableCell(aPointToInsert.GetChild())) { returnfalse; // Insert before the cell element.
} // There is a previous cell element, we should add a column after it.
Element* previousCellElement =
aPointToInsert.IsEndOfContainer()
? HTMLEditUtils::GetLastTableCellElementChild(
*aPointToInsert.ContainerAs<Element>())
: HTMLEditUtils::GetPreviousTableCellElementSibling(
*aPointToInsert.GetChild()); return previousCellElement != nullptr;
}();
// Consider the column index in the table from given point and direction. auto referenceColumnIndexOrError =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<int32_t, nsresult> { if (!insertAfterPreviousCell) { if (aPointToInsert.IsEndOfContainer()) { return tableSize.mColumnCount; // Empty row, append columns to the end
} // Insert columns immediately before current column. const OwningNonNull<Element> tableCellElement =
*aPointToInsert.GetChild()->AsElement();
MOZ_ASSERT(HTMLEditUtils::IsTableCell(tableCellElement));
CellIndexes cellIndexes(*tableCellElement, presShell); if (NS_WARN_IF(cellIndexes.isErr())) { return Err(NS_ERROR_FAILURE);
} return cellIndexes.mColumn;
}
// Otherwise, insert columns immediately after the previous column.
Element* previousCellElement =
aPointToInsert.IsEndOfContainer()
? HTMLEditUtils::GetLastTableCellElementChild(
*aPointToInsert.ContainerAs<Element>())
: HTMLEditUtils::GetPreviousTableCellElementSibling(
*aPointToInsert.GetChild());
MOZ_ASSERT(previousCellElement);
CellIndexes cellIndexes(*previousCellElement, presShell); if (NS_WARN_IF(cellIndexes.isErr())) { return Err(NS_ERROR_FAILURE);
} return cellIndexes.mColumn;
}(); if (MOZ_UNLIKELY(referenceColumnIndexOrError.isErr())) { return referenceColumnIndexOrError.unwrapErr();
}
AutoPlaceholderBatch treateAsOneTransaction(
*this, ScrollSelectionIntoView::Yes, __FUNCTION__); // Prevent auto insertion of <br> element in new cell until we're done. // XXX Why? We should put <br> element to every cell element before inserting // the cells into the tree.
IgnoredErrorResult error;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eInsertNode, nsIEditor::eNext, error); if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return error.StealNSResult();
}
NS_WARNING_ASSERTION(
!error.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
error.SuppressException();
// Suppress Rules System selection munging.
AutoTransactionsConserveSelection dontChangeSelection(*this);
// If we are inserting after all existing columns, make sure table is // "well formed" before appending new column. // XXX As far as I've tested, NormalizeTableInternal() always fails to // normalize non-rectangular table. So, the following CellData will // fail if the table is not rectangle. if (referenceColumnIndexOrError.inspect() >= tableSize.mColumnCount) {
DebugOnly<nsresult> rv = NormalizeTableInternal(*tableElement); if (MOZ_UNLIKELY(Destroyed())) {
NS_WARNING( "HTMLEditor::NormalizeTableInternal() caused destroying the editor"); return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv), "HTMLEditor::NormalizeTableInternal() failed, but ignored");
}
// First, we should collect all reference nodes to insert new table cells.
AutoTArray<CellData, 32> arrayOfCellData;
{
arrayOfCellData.SetCapacity(tableSize.mRowCount); for (const int32_t rowIndex : IntegerRange(tableSize.mRowCount)) { constauto cellData = CellData::AtIndexInTableElement(
*this, *tableElement, rowIndex,
referenceColumnIndexOrError.inspect()); if (NS_WARN_IF(cellData.FailedOrNotFound())) { return NS_ERROR_FAILURE;
}
arrayOfCellData.AppendElement(cellData);
}
}
// Note that checking whether the editor destroyed or not should be done // after inserting all cell elements. Otherwise, the table is left as // not a rectangle. auto cellElementToPutCaretOrError =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<RefPtr<Element>, nsresult> { // Block legacy mutation events for making this job simpler.
nsAutoScriptBlockerSuppressNodeRemoved blockToRunScript;
RefPtr<Element> cellElementToPutCaret; for (const CellData& cellData : arrayOfCellData) { // Don't fail entire process if we fail to find a cell (may fail just in // particular rows with < adequate cells per row). // XXX So, here wants to know whether the CellData actually failed // above. Fix this later. if (!cellData.mElement) { continue;
}
if ((!insertAfterPreviousCell && cellData.IsSpannedFromOtherColumn()) ||
(insertAfterPreviousCell &&
cellData.IsNextColumnSpannedFromOtherColumn())) { // If we have a cell spanning this location, simply increase its // colspan to keep table rectangular. if (cellData.mColSpan > 0) {
DebugOnly<nsresult> rvIgnored = SetColSpan(
cellData.mElement, cellData.mColSpan + aNumberOfColumnsToInsert);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "HTMLEditor::SetColSpan() failed, but ignored");
} continue;
}
EditorDOMPoint pointToInsert = [&]() { if (!insertAfterPreviousCell) { // Insert before the reference cell. return EditorDOMPoint(cellData.mElement);
} if (!cellData.mElement->GetNextSibling()) { // Insert after the reference cell, but nothing follows it, append // to the end of the row. return EditorDOMPoint::AtEndOf(*cellData.mElement->GetParentNode());
} // Otherwise, returns immediately before the next sibling. Note that // the next sibling may not be a table cell element. E.g., it may be // a text node containing only white-spaces in most cases. return EditorDOMPoint(cellData.mElement->GetNextSibling());
}(); if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
Result<CreateElementResult, nsresult> insertCellElementsResult =
InsertTableCellsWithTransaction(pointToInsert,
aNumberOfColumnsToInsert); if (MOZ_UNLIKELY(insertCellElementsResult.isErr())) {
NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed"); return insertCellElementsResult.propagateErr();
}
CreateElementResult unwrappedInsertCellElementsResult =
insertCellElementsResult.unwrap(); // We'll update selection later into the first inserted cell element in // the current row.
unwrappedInsertCellElementsResult.IgnoreCaretPointSuggestion(); if (pointToInsert.ContainerAs<Element>() ==
aPointToInsert.ContainerAs<Element>()) {
cellElementToPutCaret =
unwrappedInsertCellElementsResult.UnwrapNewNode();
MOZ_ASSERT(cellElementToPutCaret);
MOZ_ASSERT(HTMLEditUtils::IsTableCell(cellElementToPutCaret));
}
} return cellElementToPutCaret;
}(); if (MOZ_UNLIKELY(cellElementToPutCaretOrError.isErr())) { return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED
: cellElementToPutCaretOrError.unwrapErr();
} const RefPtr<Element> cellElementToPutCaret =
cellElementToPutCaretOrError.unwrap();
NS_WARNING_ASSERTION(
cellElementToPutCaret, "Didn't find the first inserted cell element in the specified row"); if (MOZ_LIKELY(cellElementToPutCaret)) {
CollapseSelectionToDeepestNonTableFirstChild(cellElementToPutCaret);
} return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
}
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. constauto 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;
}; constauto 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;;) { constauto 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;;) { constauto cellDataInLastRow = CellData::AtIndexInTableElement(
*this, *tableElement, lastRowIndex, colIndex); if (cellDataInLastRow.FailedOrNotFound()) { break; // Perhaps, we reach end of the row.
}
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());
}
}
// 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();
}
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
and/or cell"); // Don't fail if no cell found return NS_OK;
}
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;
}
}
selectedCellElement = scanner.GetNextElement(); if (!selectedCellElement) { return NS_OK;
}
const CellIndexes nextCellIndexes(*selectedCellElement, presShell); if (NS_WARN_IF(nextCellIndexes.isErr())) { return NS_ERROR_FAILURE;
}
startRowIndex = nextCellIndexes.mRow;
startColIndex = nextCellIndexes.mColumn; // When table cell is removed, table size of column may be changed. // For example, if there are 2 rows, one has 2 cells, the other has // 3 cells, tableSize.mColumnCount is 3. When this removes a cell // in the latter row, mColumnCount should be come 2. However, we // don't use mColumnCount in this loop, so, this must be okay for now.
} return NS_OK;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.