HTMLEditorInsertLineBreakHandler.cpp (18746B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=2 sw=2 et tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "EditorBase.h" 8 #include "HTMLEditor.h" 9 #include "HTMLEditorInlines.h" 10 #include "HTMLEditorNestedClasses.h" 11 12 #include "CSSEditUtils.h" 13 #include "EditAction.h" 14 #include "EditorDOMPoint.h" 15 #include "EditorLineBreak.h" 16 #include "EditorUtils.h" 17 #include "HTMLEditHelpers.h" 18 #include "HTMLEditUtils.h" 19 #include "PendingStyles.h" // for SpecifiedStyle 20 #include "WhiteSpaceVisibilityKeeper.h" 21 #include "WSRunScanner.h" 22 23 #include "ErrorList.h" 24 #include "mozilla/Assertions.h" 25 #include "mozilla/AutoRestore.h" 26 #include "mozilla/ContentIterator.h" 27 #include "mozilla/EditorForwards.h" 28 #include "mozilla/Maybe.h" 29 #include "mozilla/PresShell.h" 30 #include "mozilla/TextComposition.h" 31 #include "mozilla/dom/RangeBinding.h" 32 #include "mozilla/dom/Selection.h" 33 #include "nsContentUtils.h" 34 #include "nsDebug.h" 35 #include "nsError.h" 36 #include "nsFrameSelection.h" 37 #include "nsGkAtoms.h" 38 #include "nsIContent.h" 39 #include "nsINode.h" 40 #include "nsRange.h" 41 #include "nsTArray.h" 42 #include "nsTextNode.h" 43 44 class nsISupports; 45 46 namespace mozilla { 47 48 using namespace dom; 49 using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; 50 using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions; 51 52 nsresult HTMLEditor::InsertLineBreakAsSubAction() { 53 MOZ_ASSERT(IsEditActionDataAvailable()); 54 MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); 55 56 if (NS_WARN_IF(!mInitSucceeded)) { 57 return NS_ERROR_NOT_INITIALIZED; 58 } 59 60 { 61 Result<EditActionResult, nsresult> result = 62 CanHandleHTMLEditSubAction(CheckSelectionInReplacedElement::No); 63 if (MOZ_UNLIKELY(result.isErr())) { 64 NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); 65 return result.unwrapErr(); 66 } 67 if (result.inspect().Canceled()) { 68 return NS_OK; 69 } 70 } 71 72 // XXX This may be called by execCommand() with "insertLineBreak". 73 // In such case, naming the transaction "TypingTxnName" is odd. 74 AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, 75 ScrollSelectionIntoView::Yes, 76 __FUNCTION__); 77 78 // calling it text insertion to trigger moz br treatment by rules 79 // XXX Why do we use EditSubAction::eInsertText here? Looks like 80 // EditSubAction::eInsertLineBreak or EditSubAction::eInsertNode 81 // is better. 82 IgnoredErrorResult ignoredError; 83 AutoEditSubActionNotifier startToHandleEditSubAction( 84 *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError); 85 if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { 86 return ignoredError.StealNSResult(); 87 } 88 NS_WARNING_ASSERTION( 89 !ignoredError.Failed(), 90 "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); 91 92 UndefineCaretBidiLevel(); 93 94 // If the selection isn't collapsed, delete it. 95 if (!SelectionRef().IsCollapsed()) { 96 nsresult rv = 97 DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip); 98 if (NS_FAILED(rv)) { 99 NS_WARNING( 100 "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed"); 101 return rv; 102 } 103 } 104 105 const RefPtr<Element> editingHost = 106 ComputeEditingHost(LimitInBodyElement::No); 107 if (NS_WARN_IF(!editingHost)) { 108 return NS_ERROR_FAILURE; 109 } 110 111 AutoInsertLineBreakHandler handler(*this, *editingHost); 112 nsresult rv = handler.Run(); 113 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 114 "AutoInsertLineBreakHandler::Run() failed"); 115 return rv; 116 } 117 118 nsresult HTMLEditor::AutoInsertLineBreakHandler::Run() { 119 MOZ_ASSERT(mHTMLEditor.IsEditActionDataAvailable()); 120 121 const auto atStartOfSelection = 122 mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>(); 123 if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) { 124 return NS_ERROR_FAILURE; 125 } 126 MOZ_ASSERT(atStartOfSelection.IsSetAndValidInComposedDoc()); 127 128 const Maybe<LineBreakType> lineBreakType = 129 mHTMLEditor.GetPreferredLineBreakType( 130 *atStartOfSelection.ContainerAs<nsIContent>(), mEditingHost); 131 if (MOZ_UNLIKELY(!lineBreakType)) { 132 return NS_SUCCESS_DOM_NO_OPERATION; // Cannot insert a line break there. 133 } 134 if (lineBreakType.value() == LineBreakType::BRElement) { 135 nsresult rv = HandleInsertBRElement(); 136 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 137 "AutoInsertLineBreakHandler::HandleInsertBRElement()"); 138 return rv; 139 } 140 141 nsresult rv = HandleInsertLinefeed(); 142 NS_WARNING_ASSERTION( 143 NS_SUCCEEDED(rv), 144 "AutoInsertLineBreakHandler::HandleInsertLinefeed() failed"); 145 return rv; 146 } 147 148 nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertBRElement() { 149 const EditorDOMPoint pointToInsert = [&]() { 150 const auto atStartOfSelection = 151 mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>(); 152 MOZ_ASSERT(atStartOfSelection.IsInContentNode()); 153 return HTMLEditUtils::GetPossiblePointToInsert( 154 atStartOfSelection, *nsGkAtoms::br, mEditingHost); 155 }(); 156 if (NS_WARN_IF(!pointToInsert.IsSet())) { 157 return Err(NS_ERROR_FAILURE); 158 } 159 MOZ_ASSERT(pointToInsert.IsInContentNode()); 160 161 // XXX Should we check the preferred line break again? 162 163 Result<CreateLineBreakResult, nsresult> insertLineBreakResultOrError = 164 mHTMLEditor.InsertLineBreak(WithTransaction::Yes, 165 LineBreakType::BRElement, pointToInsert, 166 nsIEditor::eNext); 167 if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) { 168 NS_WARNING( 169 "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " 170 "LineBreakType::BRElement, eNext) failed"); 171 return insertLineBreakResultOrError.unwrapErr(); 172 } 173 CreateLineBreakResult insertLineBreakResult = 174 insertLineBreakResultOrError.unwrap(); 175 MOZ_ASSERT(insertLineBreakResult.Handled()); 176 insertLineBreakResult.IgnoreCaretPointSuggestion(); 177 178 auto pointToPutCaret = insertLineBreakResult.UnwrapCaretPoint(); 179 if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) { 180 NS_WARNING("Inserted <br> was unexpectedly removed"); 181 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); 182 } 183 const WSScanResult backwardScanFromBeforeBRElementResult = 184 WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( 185 {WSRunScanner::Option::OnlyEditableNodes}, 186 insertLineBreakResult.AtLineBreak<EditorDOMPoint>()); 187 if (MOZ_UNLIKELY(backwardScanFromBeforeBRElementResult.Failed())) { 188 NS_WARNING("WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() failed"); 189 return Err(NS_ERROR_FAILURE); 190 } 191 192 const WSScanResult forwardScanFromAfterBRElementResult = 193 WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( 194 {WSRunScanner::Option::OnlyEditableNodes}, pointToPutCaret); 195 if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) { 196 NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); 197 return Err(NS_ERROR_FAILURE); 198 } 199 const bool brElementIsAfterBlock = 200 backwardScanFromBeforeBRElementResult.ReachedBlockBoundary() || 201 // FIXME: This is wrong considering because the inline editing host may 202 // be surrounded by visible inline content. However, WSRunScanner is 203 // not aware of block boundary around it and stopping this change causes 204 // starting to fail some WPT. Therefore, we need to keep doing this for 205 // now. 206 backwardScanFromBeforeBRElementResult.ReachedInlineEditingHostBoundary(); 207 const bool brElementIsBeforeBlock = 208 forwardScanFromAfterBRElementResult.ReachedBlockBoundary() || 209 // FIXME: See above comment. 210 forwardScanFromAfterBRElementResult.ReachedInlineEditingHostBoundary(); 211 const bool isEmptyEditingHost = HTMLEditUtils::IsEmptyNode( 212 mEditingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible}); 213 if (brElementIsBeforeBlock && 214 (isEmptyEditingHost || !brElementIsAfterBlock)) { 215 // Empty last line is invisible if it's immediately before either parent 216 // or another block's boundary so that we need to put invisible <br> 217 // element here for making it visible. 218 Result<CreateLineBreakResult, nsresult> 219 insertPaddingBRElementResultOrError = 220 WhiteSpaceVisibilityKeeper::InsertLineBreak( 221 LineBreakType::BRElement, mHTMLEditor, pointToPutCaret); 222 if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) { 223 NS_WARNING( 224 "WhiteSpaceVisibilityKeeper::InsertLineBreak(LineBreakType::" 225 "BRElement) failed"); 226 return insertPaddingBRElementResultOrError.unwrapErr(); 227 } 228 CreateLineBreakResult insertPaddingBRElementResult = 229 insertPaddingBRElementResultOrError.unwrap(); 230 pointToPutCaret = 231 insertPaddingBRElementResult.AtLineBreak<EditorDOMPoint>(); 232 insertPaddingBRElementResult.IgnoreCaretPointSuggestion(); 233 } else if (forwardScanFromAfterBRElementResult 234 .InVisibleOrCollapsibleCharacters()) { 235 pointToPutCaret = forwardScanFromAfterBRElementResult 236 .PointAtReachedContent<EditorDOMPoint>(); 237 } else if (forwardScanFromAfterBRElementResult.ReachedSpecialContent()) { 238 // Next inserting text should be inserted into styled inline elements if 239 // they have first visible thing in the new line. 240 pointToPutCaret = forwardScanFromAfterBRElementResult 241 .PointAtReachedContent<EditorDOMPoint>(); 242 } 243 244 nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret); 245 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 246 "EditorBase::CollapseSelectionTo() failed"); 247 return rv; 248 } 249 250 nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertLinefeed() { 251 nsresult rv = mHTMLEditor.EnsureNoPaddingBRElementForEmptyEditor(); 252 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { 253 return NS_ERROR_EDITOR_DESTROYED; 254 } 255 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 256 "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " 257 "failed, but ignored"); 258 259 if (NS_SUCCEEDED(rv) && mHTMLEditor.SelectionRef().IsCollapsed()) { 260 nsresult rv = 261 mHTMLEditor.EnsureCaretNotAfterInvisibleBRElement(mEditingHost); 262 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { 263 return NS_ERROR_EDITOR_DESTROYED; 264 } 265 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 266 "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " 267 "failed, but ignored"); 268 if (NS_SUCCEEDED(rv)) { 269 nsresult rv = mHTMLEditor.PrepareInlineStylesForCaret(); 270 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { 271 return NS_ERROR_EDITOR_DESTROYED; 272 } 273 NS_WARNING_ASSERTION( 274 NS_SUCCEEDED(rv), 275 "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); 276 } 277 } 278 279 const EditorDOMPoint atStartOfSelection = 280 mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>(); 281 if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) { 282 return NS_ERROR_FAILURE; 283 } 284 MOZ_ASSERT(atStartOfSelection.IsSetAndValidInComposedDoc()); 285 286 // Do nothing if the node is read-only 287 if (!HTMLEditUtils::IsSimplyEditableNode( 288 *atStartOfSelection.GetContainer())) { 289 return NS_SUCCESS_DOM_NO_OPERATION; 290 } 291 292 Result<EditorDOMPoint, nsresult> insertLineFeedResult = 293 AutoInsertLineBreakHandler::InsertLinefeed( 294 mHTMLEditor, atStartOfSelection, mEditingHost); 295 if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) { 296 NS_WARNING("AutoInsertLineBreakHandler::InsertLinefeed() failed"); 297 return insertLineFeedResult.unwrapErr(); 298 } 299 rv = mHTMLEditor.CollapseSelectionTo(insertLineFeedResult.inspect()); 300 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 301 "EditorBase::CollapseSelectionTo() failed"); 302 return rv; 303 } 304 305 // static 306 Result<EditorDOMPoint, nsresult> 307 HTMLEditor::AutoInsertLineBreakHandler::InsertLinefeed( 308 HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToBreak, 309 const Element& aEditingHost) { 310 if (NS_WARN_IF(!aPointToBreak.IsSet())) { 311 return Err(NS_ERROR_INVALID_ARG); 312 } 313 314 const RefPtr<Document> document = aHTMLEditor.GetDocument(); 315 MOZ_DIAGNOSTIC_ASSERT(document); 316 if (NS_WARN_IF(!document)) { 317 return Err(NS_ERROR_FAILURE); 318 } 319 320 // TODO: The following code is duplicated from `HandleInsertText`. They 321 // should be merged when we fix bug 92921. 322 323 Result<EditorDOMPoint, nsresult> setStyleResult = 324 aHTMLEditor.CreateStyleForInsertText(aPointToBreak, aEditingHost); 325 if (MOZ_UNLIKELY(setStyleResult.isErr())) { 326 NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); 327 return setStyleResult.propagateErr(); 328 } 329 330 EditorDOMPoint pointToInsert = setStyleResult.inspect().IsSet() 331 ? setStyleResult.inspect() 332 : aPointToBreak; 333 if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) || 334 NS_WARN_IF(!pointToInsert.IsInContentNode())) { 335 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); 336 } 337 MOZ_ASSERT(pointToInsert.IsSetAndValid()); 338 339 // The node may not be able to have a text node so that we need to check it 340 // here. 341 pointToInsert = HTMLEditUtils::GetPossiblePointToInsert( 342 pointToInsert, *nsGkAtoms::textTagName, aEditingHost); 343 if (NS_WARN_IF(!pointToInsert.IsSet())) { 344 return Err(NS_ERROR_FAILURE); 345 } 346 MOZ_ASSERT(pointToInsert.IsInContentNode()); 347 348 // FIXME: If the computed point does not preformat linefeed, we should switch 349 // back to inserting a <br>. However, I think it should be handled before 350 // calling this. 351 352 AutoRestore<bool> disableListener( 353 aHTMLEditor.EditSubActionDataRef().mAdjustChangedRangeFromListener); 354 aHTMLEditor.EditSubActionDataRef().mAdjustChangedRangeFromListener = false; 355 356 // TODO: We don't need AutoTransactionsConserveSelection here in the normal 357 // cases, but removing this may cause the behavior with the legacy 358 // mutation event listeners. We should try to delete this in a bug. 359 AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); 360 361 EditorDOMPoint pointToPutCaret; 362 { 363 AutoTrackDOMPoint trackingInsertingPosition(aHTMLEditor.RangeUpdaterRef(), 364 &pointToInsert); 365 Result<CreateLineBreakResult, nsresult> insertLinefeedResultOrError = 366 aHTMLEditor.InsertLineBreak(WithTransaction::Yes, 367 LineBreakType::Linefeed, pointToInsert, 368 eNext); 369 if (MOZ_UNLIKELY(insertLinefeedResultOrError.isErr())) { 370 NS_WARNING( 371 "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " 372 "LineBreakType::Linefeed, eNext) failed"); 373 return insertLinefeedResultOrError.propagateErr(); 374 } 375 pointToPutCaret = insertLinefeedResultOrError.unwrap().UnwrapCaretPoint(); 376 } 377 378 // Insert a padding <br> if the inserted linefeed is followed by a block 379 // boundary. Note that it should always be <br> for avoiding padding line 380 // breaks appear in `.textContent` value. 381 if (pointToPutCaret.IsInContentNode() && pointToPutCaret.IsEndOfContainer()) { 382 const WSRunScanner scannerAtCaret({}, pointToPutCaret, &aEditingHost); 383 const WSScanResult prevVisibleThing = 384 scannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( 385 pointToPutCaret); 386 const WSScanResult nextVisibleThing = 387 scannerAtCaret.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( 388 pointToPutCaret); 389 if (prevVisibleThing.ReachedPreformattedLineBreak() && 390 (nextVisibleThing.ReachedCurrentBlockBoundary() || 391 nextVisibleThing.ReachedInlineEditingHostBoundary()) && 392 HTMLEditUtils::CanNodeContain(*nextVisibleThing.ElementPtr(), 393 *nsGkAtoms::br)) { 394 AutoTrackDOMPoint trackingInsertedPosition(aHTMLEditor.RangeUpdaterRef(), 395 &pointToInsert); 396 AutoTrackDOMPoint trackingNewCaretPosition(aHTMLEditor.RangeUpdaterRef(), 397 &pointToPutCaret); 398 Result<CreateLineBreakResult, nsresult> insertBRElementResultOrError = 399 aHTMLEditor.InsertLineBreak( 400 WithTransaction::Yes, LineBreakType::BRElement, pointToPutCaret); 401 if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { 402 NS_WARNING( 403 "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " 404 "LineBreakType::BRElement) failed"); 405 return insertBRElementResultOrError.propagateErr(); 406 } 407 CreateLineBreakResult insertBRElementResult = 408 insertBRElementResultOrError.unwrap(); 409 MOZ_ASSERT(insertBRElementResult.Handled()); 410 insertBRElementResult.IgnoreCaretPointSuggestion(); 411 } 412 } 413 414 // manually update the doc changed range so that 415 // OnEndHandlingTopLevelEditSubActionInternal will clean up the correct 416 // portion of the document. 417 MOZ_ASSERT(pointToPutCaret.IsSet()); 418 if (NS_WARN_IF(!pointToPutCaret.IsSet())) { 419 // XXX Here is odd. We did mChangedRange->SetStartAndEnd(pointToInsert, 420 // pointToPutCaret), but it always fails because of the latter is unset. 421 // Therefore, always returning NS_ERROR_FAILURE from here is the 422 // traditional behavior... 423 // TODO: Stop updating the interline position of Selection with fixing here 424 // and returning expected point. 425 DebugOnly<nsresult> rvIgnored = 426 aHTMLEditor.SelectionRef().SetInterlinePosition( 427 InterlinePosition::EndOfLine); 428 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), 429 "Selection::SetInterlinePosition(InterlinePosition::" 430 "EndOfLine) failed, but ignored"); 431 if (NS_FAILED(aHTMLEditor.TopLevelEditSubActionDataRef() 432 .mChangedRange->CollapseTo(pointToInsert))) { 433 NS_WARNING("nsRange::CollapseTo() failed"); 434 return Err(NS_ERROR_FAILURE); 435 } 436 NS_WARNING( 437 "We always return NS_ERROR_FAILURE here because of a failure of " 438 "updating mChangedRange"); 439 return Err(NS_ERROR_FAILURE); 440 } 441 442 if (NS_FAILED(aHTMLEditor.TopLevelEditSubActionDataRef() 443 .mChangedRange->SetStartAndEnd( 444 pointToInsert.ToRawRangeBoundary(), 445 pointToPutCaret.ToRawRangeBoundary()))) { 446 NS_WARNING("nsRange::SetStartAndEnd() failed"); 447 return Err(NS_ERROR_FAILURE); 448 } 449 450 pointToPutCaret.SetInterlinePosition(InterlinePosition::EndOfLine); 451 return pointToPutCaret; 452 } 453 454 } // namespace mozilla