Commit 757378b4 authored by Masayuki Nakano's avatar Masayuki Nakano
Browse files

Bug 1730442 - part 3: Make `HTMLEditor::InsertTableCellsWithTransaction()`...

Bug 1730442 - part 3: Make `HTMLEditor::InsertTableCellsWithTransaction()` work without layout information r=m_kato

It refers the layout information for getting row/column index of selected cell
element or a cell element which contains selection.  However, it can stop
referring it if all callers can specify insertion point of new cells, and it's
possible.  Therefore, we can make it free from layout information.

Note that this blocks legacy mutation events during inserting table cell
elements, but perhaps this does not cause problems in the wild.
`DOMNodeInserted` will be fired after all cell elements are inserted, so
from point of view of the event listeners, editor content is different from
traditional behavior, but this works only when user uses inline table editor
which is disabled by default.  I.e., in the wild, this path is rarely run.

Note that the changes are tested by `test_nsITableEditor_insertTableColumn.html`
and `test_nsITableEditor_insertTableCell.html`.

Depends on D146360

Differential Revision: https://phabricator.services.mozilla.com/D146361
parent 4db95885
Loading
Loading
Loading
Loading
+9 −13
Original line number Diff line number Diff line
@@ -3461,8 +3461,6 @@ class HTMLEditor final : public EditorBase,
      bool* aMixed, nsAString& aOutColor, bool aBlockLevel);
  nsresult GetHTMLBackgroundColorState(bool* aMixed, nsAString& outColor);

  nsresult GetLastCellInRow(nsINode* aRowNode, nsINode** aCellNode);

  /**
   * This sets background on the appropriate container element (table, cell,)
   * or calls to set the page background.
@@ -3581,10 +3579,7 @@ class HTMLEditor final : public EditorBase,
  };

  /**
   * InsertTableCellsWithTransaction() inserts <td> elements before or after
   * a cell element containing first selection range.  I.e., if the cell
   * spans columns and aInsertPosition is eAfterSelectedCell, new columns
   * will be inserted after the right-most column which contains the cell.
   * InsertTableCellsWithTransaction() inserts <td> elements at aPointToInsert.
   * Note that this simply inserts <td> elements, i.e., colspan and rowspan
   * around the cell containing selection are not modified.  So, for example,
   * adding a cell to rectangular table changes non-rectangular table.
@@ -3592,15 +3587,16 @@ class HTMLEditor final : public EditorBase,
   * it may be moved to right side of the row-spanning cell after inserting
   * some cell elements before it.  Similarly, colspan won't be adjusted
   * for keeping table rectangle.
   * If first selection range is not in table cell element, this does nothing
   * but does not return error.
   * Finally, puts caret into previous cell of the insertion point or the
   * first inserted cell if aPointToInsert is start of the row.
   *
   * @param aNumberOfCellssToInsert     Number of cells to insert.
   * @param aInsertPosition             Before or after the target cell which
   *                                    contains first selection range.
   * @param aPointToInsert              The place to insert one or more cell
   *                                    elements.  The container must be a
   *                                    <tr> element.
   * @param aNumberOfCellsToInsert      Number of cells to insert.
   */
  MOZ_CAN_RUN_SCRIPT nsresult InsertTableCellsWithTransaction(
      int32_t aNumberOfCellsToInsert, InsertPosition aInsertPosition);
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertTableCellsWithTransaction(
      const EditorDOMPoint& aPointToInsert, int32_t aNumberOfCellsToInsert);

  /**
   * InsertTableColumnsWithTransaction() inserts columns before or after
+125 −151
Original line number Diff line number Diff line
@@ -322,6 +322,10 @@ nsresult HTMLEditor::SetRowSpan(Element* aCell, int32_t aRowSpan) {

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();
@@ -331,61 +335,57 @@ NS_IMETHODIMP HTMLEditor::InsertTableCell(int32_t aNumberOfCellsToInsert,
    return EditorBase::ToGenericNSResult(rv);
  }

  rv = InsertTableCellsWithTransaction(
      aNumberOfCellsToInsert, aInsertAfterSelectedCell
                                  ? InsertPosition::eAfterSelectedCell
                                  : InsertPosition::eBeforeSelectedCell);
  Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError =
      GetSelectedOrParentTableElement();
  if (cellOrRowOrTableElementOrError.isErr()) {
    NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed");
    return EditorBase::ToGenericNSResult(
        cellOrRowOrTableElementOrError.unwrapErr());
  }

  if (!cellOrRowOrTableElementOrError.inspect()) {
    return NS_OK;
  }

  if (!HTMLEditUtils::GetClosestAncestorTableElement(
          *cellOrRowOrTableElementOrError.inspect())) {
    NS_WARNING("There was no ancestor <table> element for the found cell");
    return NS_ERROR_FAILURE;
  }

  EditorDOMPoint pointToInsert(cellOrRowOrTableElementOrError.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 = InsertTableCellsWithTransaction(pointToInsert, aNumberOfCellsToInsert);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::InsertTableCellsWithTransaction() failed");
  return EditorBase::ToGenericNSResult(rv);
}

nsresult HTMLEditor::InsertTableCellsWithTransaction(
    int32_t aNumberOfCellsToInsert, InsertPosition aInsertPosition) {
    const EditorDOMPoint& aPointToInsert, int32_t aNumberOfCellsToInsert) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aPointToInsert.IsSetAndValid());
  MOZ_ASSERT(aNumberOfCellsToInsert > 0);

  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.
  const auto cellDataAtSelection = CellData::AtIndexInTableElement(
      *this, *table, startRowIndex, startColIndex);
  if (NS_WARN_IF(cellDataAtSelection.FailedOrNotFound())) {
  if (!HTMLEditUtils::IsTableRow(aPointToInsert.GetContainer())) {
    NS_WARNING("Tried to insert cell elements to non-<tr> element");
    return NS_ERROR_FAILURE;
  }
  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, __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);
@@ -397,44 +397,69 @@ nsresult HTMLEditor::InsertTableCellsWithTransaction(
      "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
  error.SuppressException();

  // We control selection resetting after the insert.
  AutoSelectionSetterAfterTableEdit setCaret(
      *this, table, cellDataAtSelection.mCurrent.mRow, newCellIndex,
      ePreviousColumn, false);
  // Put caret into the cell before the first inserting cell, or the first
  // table cell in the row.
  RefPtr<Element> cellToPutCaret;
  for (nsIContent* maybeCellToPutCaret =
           aPointToInsert.GetPreviousSiblingOfChild();
       maybeCellToPutCaret;
       maybeCellToPutCaret = maybeCellToPutCaret->GetPreviousSibling()) {
    if (HTMLEditUtils::IsTableCell(maybeCellToPutCaret)) {
      cellToPutCaret = maybeCellToPutCaret->AsElement();
      break;
    }
  }

  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);

  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");
  }
    // 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* nextCellElement = aPointToInsert.GetChild();
    for ([[maybe_unused]] const auto i :
         IntegerRange<uint32_t>(aNumberOfCellsToInsert)) {
      RefPtr<Element> newCell = CreateElementWithDefaults(*nsGkAtoms::td);
    if (MOZ_UNLIKELY(!newCell)) {
      NS_WARNING("HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed");
      if (!newCell) {
        NS_WARNING(
            "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed");
        return NS_ERROR_FAILURE;
      }
    AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
    CreateElementResult insertNewCellResult =
        InsertNodeWithTransaction<Element>(*newCell, pointToInsert);
      CreateElementResult insertNewCellResult = InsertNodeWithTransaction(
          *newCell,
          nextCellElement
              ? EditorDOMPoint(nextCellElement)
              : EditorDOMPoint::AtEndOf(*aPointToInsert.ContainerAsElement()));
      if (insertNewCellResult.isErr()) {
        NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
        return insertNewCellResult.unwrapErr();
      }
    // Because of dontChangeSelection, we've never allowed to transactions to
    // update selection here.
      // 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.
      }
    }

    MOZ_ASSERT(cellToPutCaret);
    MOZ_ASSERT(cellToPutCaret->GetParent());
    CollapseSelectionToDeepestNonTableFirstChild(cellToPutCaret);
    return NS_OK;
  }();
  return rv == NS_ERROR_EDITOR_DESTROYED || NS_WARN_IF(Destroyed())
             ? NS_ERROR_EDITOR_DESTROYED
             : rv;
}

NS_IMETHODIMP HTMLEditor::GetFirstRow(Element* aTableOrElementInTable,
@@ -556,32 +581,12 @@ Element* HTMLEditor::GetNextTableRowElement(Element& aTableRowElement,
  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) {
  if (aNumberOfColumnsToInsert <= 0) {
    return NS_OK;  // XXX Traditional behavior
  }

  AutoEditActionDataSetter editActionData(*this,
                                          EditAction::eInsertTableColumn);
  nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
@@ -604,6 +609,7 @@ NS_IMETHODIMP HTMLEditor::InsertTableColumn(int32_t aNumberOfColumnsToInsert,
nsresult HTMLEditor::InsertTableColumnsWithTransaction(
    int32_t aNumberOfColumnsToInsert, InsertPosition aInsertPosition) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aNumberOfColumnsToInsert >= 0);

  RefPtr<Element> table;
  RefPtr<Element> curCell;
@@ -726,25 +732,16 @@ nsresult HTMLEditor::InsertTableColumnsWithTransaction(
        continue;
      }

      // Simply set selection to the current cell. So, we can let
      // InsertTableCellsWithTransaction() do the work.  Insert a new cell
      // before current one.
      CollapseSelectionToStartOf(*cellData.mElement, error);
      if (MOZ_UNLIKELY(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
        NS_WARNING(
            "EditorBase::CollapseSelectionToStartOf() caused destroying the "
            "editor");
        return NS_ERROR_EDITOR_DESTROYED;
      EditorDOMPoint pointToInsert(cellData.mElement);
      if (MOZ_UNLIKELY(NS_WARN_IF(!pointToInsert.IsSet()))) {
        return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
      }
      nsresult rv = InsertTableCellsWithTransaction(pointToInsert,
                                                    aNumberOfColumnsToInsert);
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed");
        return rv;
      }
      NS_WARNING_ASSERTION(
          !error.Failed(),
          "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
      error.SuppressException();
      rv = InsertTableCellsWithTransaction(aNumberOfColumnsToInsert,
                                           InsertPosition::eBeforeSelectedCell);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "HTMLEditor::InsertTableCellsWithTransaction() "
                           "failed, but might be ignored");
      continue;
    }

@@ -778,41 +775,18 @@ nsresult HTMLEditor::InsertTableColumnsWithTransaction(
      }
    }

    nsCOMPtr<nsINode> lastCellNode;
    rv = GetLastCellInRow(rowElement, getter_AddRefs(lastCellNode));
    EditorDOMPoint atEndOfRow = EditorDOMPoint::AtEndOf(*rowElement);
    if (MOZ_UNLIKELY(NS_WARN_IF(!atEndOfRow.IsSet()))) {
      return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
    }
    nsresult rv =
        InsertTableCellsWithTransaction(atEndOfRow, aNumberOfColumnsToInsert);
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::GetLastCellInRow() failed");
      NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() 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, error);
    if (MOZ_UNLIKELY(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
      NS_WARNING(
          "EditorBase::CollapseSelectionToStartOf() caused destroying the "
          "editor");
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(
        !error.Failed(),
        "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
    error.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;
  return NS_OK;
}

NS_IMETHODIMP HTMLEditor::InsertTableRow(int32_t aNumberOfRowsToInsert,
+46 −0
Original line number Diff line number Diff line
@@ -122,6 +122,52 @@ SimpleTest.waitForFocus(() => {
       'No "input" event should be fired when nsITableEditor.insertTableCell(1, true) causes exception due to no selection range');
  }

  selection.removeAllRanges();
  editor.innerHTML = "<table>" +
                       "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                       '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                       "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                     "</table>";
  editor.focus();
  beforeInputEvents = [];
  inputEvents = [];
  selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                             document.getElementById("select").firstChild, 0);
  getTableEditor().insertTableCell(0, false);
  is(editor.innerHTML, "<table><tbody>" +
                         "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                         '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                         "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                       "</tbody></table>",
     "nsITableEditor.insertTableCell(0, false) should not do nothing without throwing exception");
  is(beforeInputEvents.length, 0,
     'No "beforeinput" event should be fired when calling nsITableEditor.insertTableCell(0, false)');
  is(inputEvents.length, 0,
     'No "input" event should be fired when calling nsITableEditor.insertTableCell(0, false)');

  selection.removeAllRanges();
  editor.innerHTML = "<table><tbody>" +
                       "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                       '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                       "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                     "</tbody></table>";
  editor.focus();
  beforeInputEvents = [];
  inputEvents = [];
  selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
                             document.getElementById("select").firstChild, 0);
  getTableEditor().insertTableCell(0, true);
  is(editor.innerHTML, "<table><tbody>" +
                         "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
                         '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
                         "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
                       "</tbody></table>",
     "nsITableEditor.insertTableCell(0, true) should not do nothing without throwing exception");
  is(beforeInputEvents.length, 0,
     'No "beforeinput" event should be fired when calling nsITableEditor.insertTableCell(0, true)');
  is(inputEvents.length, 0,
     'No "input" event should be fired when calling nsITableEditor.insertTableCell(0, true)');

  selection.removeAllRanges();
  editor.innerHTML = "<table>" +
                       "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +