PendingStyles.cpp (18736B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "PendingStyles.h" 7 8 #include <stddef.h> 9 10 #include "EditAction.h" 11 #include "EditorBase.h" 12 #include "HTMLEditHelpers.h" // for EditorInlineStyle, EditorInlineStyleAndValue 13 #include "HTMLEditor.h" 14 #include "HTMLEditUtils.h" 15 16 #include "mozilla/mozalloc.h" 17 #include "mozilla/dom/AncestorIterator.h" 18 #include "mozilla/dom/MouseEvent.h" 19 #include "mozilla/dom/Selection.h" 20 21 #include "nsDebug.h" 22 #include "nsError.h" 23 #include "nsGkAtoms.h" 24 #include "nsINode.h" 25 #include "nsISupports.h" 26 #include "nsISupportsImpl.h" 27 #include "nsReadableUtils.h" 28 #include "nsString.h" 29 #include "nsTArray.h" 30 31 namespace mozilla { 32 33 using namespace dom; 34 35 /******************************************************************** 36 * mozilla::PendingStyle 37 *******************************************************************/ 38 39 EditorInlineStyle PendingStyle::ToInlineStyle() const { 40 return mTag ? EditorInlineStyle(*mTag, mAttribute) 41 : EditorInlineStyle::RemoveAllStyles(); 42 } 43 44 EditorInlineStyleAndValue PendingStyle::ToInlineStyleAndValue() const { 45 MOZ_ASSERT(mTag); 46 return mAttribute ? EditorInlineStyleAndValue(*mTag, *mAttribute, 47 mAttributeValueOrCSSValue) 48 : EditorInlineStyleAndValue(*mTag); 49 } 50 51 /******************************************************************** 52 * mozilla::PendingStyleCache 53 *******************************************************************/ 54 55 EditorInlineStyle PendingStyleCache::ToInlineStyle() const { 56 return EditorInlineStyle(mTag, mAttribute); 57 } 58 59 /******************************************************************** 60 * mozilla::PendingStyles 61 *******************************************************************/ 62 63 NS_IMPL_CYCLE_COLLECTION_CLASS(PendingStyles) 64 65 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PendingStyles) 66 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastSelectionPoint) 67 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 68 69 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PendingStyles) 70 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastSelectionPoint) 71 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 72 73 nsresult PendingStyles::UpdateSelState(const HTMLEditor& aHTMLEditor) { 74 if (!aHTMLEditor.SelectionRef().IsCollapsed()) { 75 return NS_OK; 76 } 77 78 mLastSelectionPoint = 79 aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>(); 80 if (!mLastSelectionPoint.IsSet()) { 81 return NS_ERROR_FAILURE; 82 } 83 // We need to store only offset because referring child may be removed by 84 // we'll check the point later. 85 AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint); 86 return NS_OK; 87 } 88 89 void PendingStyles::PreHandleMouseEvent(const MouseEvent& aMouseDownOrUpEvent) { 90 MOZ_ASSERT(aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown || 91 aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseUp); 92 bool& eventFiredInLinkElement = 93 aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown 94 ? mMouseDownFiredInLinkElement 95 : mMouseUpFiredInLinkElement; 96 eventFiredInLinkElement = false; 97 if (aMouseDownOrUpEvent.DefaultPrevented()) { 98 return; 99 } 100 // If mouse button is down or up in a link element, we shouldn't unlink 101 // it when we get a notification of selection change. 102 EventTarget* target = aMouseDownOrUpEvent.GetExplicitOriginalTarget(); 103 if (NS_WARN_IF(!target)) { 104 return; 105 } 106 nsIContent* targetContent = nsIContent::FromEventTarget(target); 107 if (NS_WARN_IF(!targetContent)) { 108 return; 109 } 110 eventFiredInLinkElement = 111 HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent); 112 } 113 114 void PendingStyles::PreHandleSelectionChangeCommand(Command aCommand) { 115 mLastSelectionCommand = aCommand; 116 } 117 118 void PendingStyles::PostHandleSelectionChangeCommand( 119 const HTMLEditor& aHTMLEditor, Command aCommand) { 120 if (mLastSelectionCommand != aCommand) { 121 return; 122 } 123 124 // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`, 125 // it means that it didn't cause selection change. 126 if (!aHTMLEditor.SelectionRef().IsCollapsed() || 127 !aHTMLEditor.SelectionRef().RangeCount()) { 128 return; 129 } 130 131 const auto caretPoint = 132 aHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>(); 133 if (NS_WARN_IF(!caretPoint.IsSet())) { 134 return; 135 } 136 137 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) { 138 return; 139 } 140 141 // If all styles are cleared or link style is explicitly set, we 142 // shouldn't reset them without caret move. 143 if (AreAllStylesCleared() || IsLinkStyleSet()) { 144 return; 145 } 146 // And if non-link styles are cleared or some styles are set, we 147 // shouldn't reset them too, but we may need to change the link 148 // style. 149 if (AreSomeStylesSet() || 150 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) { 151 ClearLinkAndItsSpecifiedStyle(); 152 return; 153 } 154 155 Reset(); 156 ClearLinkAndItsSpecifiedStyle(); 157 } 158 159 void PendingStyles::OnSelectionChange(const HTMLEditor& aHTMLEditor, 160 int16_t aReason) { 161 // XXX: Selection currently generates bogus selection changed notifications 162 // XXX: (bug 140303). It can notify us when the selection hasn't actually 163 // XXX: changed, and it notifies us more than once for the same change. 164 // XXX: 165 // XXX: The following code attempts to work around the bogus notifications, 166 // XXX: and should probably be removed once bug 140303 is fixed. 167 // XXX: 168 // XXX: This code temporarily fixes the problem where clicking the mouse in 169 // XXX: the same location clears the type-in-state. 170 171 const bool causedByFrameSelectionMoveCaret = 172 (aReason & (nsISelectionListener::KEYPRESS_REASON | 173 nsISelectionListener::COLLAPSETOSTART_REASON | 174 nsISelectionListener::COLLAPSETOEND_REASON)) && 175 !(aReason & nsISelectionListener::JS_REASON); 176 177 Command lastSelectionCommand = mLastSelectionCommand; 178 if (causedByFrameSelectionMoveCaret) { 179 mLastSelectionCommand = Command::DoNothing; 180 } 181 182 bool mouseEventFiredInLinkElement = false; 183 if (aReason & (nsISelectionListener::MOUSEDOWN_REASON | 184 nsISelectionListener::MOUSEUP_REASON)) { 185 MOZ_ASSERT((aReason & (nsISelectionListener::MOUSEDOWN_REASON | 186 nsISelectionListener::MOUSEUP_REASON)) != 187 (nsISelectionListener::MOUSEDOWN_REASON | 188 nsISelectionListener::MOUSEUP_REASON)); 189 bool& eventFiredInLinkElement = 190 aReason & nsISelectionListener::MOUSEDOWN_REASON 191 ? mMouseDownFiredInLinkElement 192 : mMouseUpFiredInLinkElement; 193 mouseEventFiredInLinkElement = eventFiredInLinkElement; 194 eventFiredInLinkElement = false; 195 } 196 197 bool unlink = false; 198 bool resetAllStyles = true; 199 if (aHTMLEditor.SelectionRef().IsCollapsed() && 200 aHTMLEditor.SelectionRef().RangeCount()) { 201 const auto selectionStartPoint = 202 aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>(); 203 if (MOZ_UNLIKELY(NS_WARN_IF(!selectionStartPoint.IsSet()))) { 204 return; 205 } 206 207 if (mLastSelectionPoint == selectionStartPoint) { 208 // If all styles are cleared or link style is explicitly set, we 209 // shouldn't reset them without caret move. 210 if (AreAllStylesCleared() || IsLinkStyleSet()) { 211 return; 212 } 213 // And if non-link styles are cleared or some styles are set, we 214 // shouldn't reset them too, but we may need to change the link 215 // style. 216 if (AreSomeStylesSet() || 217 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) { 218 resetAllStyles = false; 219 } 220 } 221 222 RefPtr<Element> linkElement; 223 if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint, 224 getter_AddRefs(linkElement))) { 225 // If caret comes from outside of <a href> element, we should clear "link" 226 // style after reset. 227 if (causedByFrameSelectionMoveCaret) { 228 MOZ_ASSERT(!(aReason & (nsISelectionListener::MOUSEDOWN_REASON | 229 nsISelectionListener::MOUSEUP_REASON))); 230 // If caret is moves in a link per character, we should keep inserting 231 // new text to the link because user may want to keep extending the link 232 // text. Otherwise, e.g., using `End` or `Home` key. we should insert 233 // new text outside the link because it should be possible to user 234 // choose it, and this is similar to the other browsers. 235 switch (lastSelectionCommand) { 236 case Command::CharNext: 237 case Command::CharPrevious: 238 case Command::MoveLeft: 239 case Command::MoveLeft2: 240 case Command::MoveRight: 241 case Command::MoveRight2: 242 // If selection becomes collapsed, we should unlink new text. 243 if (!mLastSelectionPoint.IsSet()) { 244 unlink = true; 245 break; 246 } 247 // Special case, if selection isn't moved, it means that caret is 248 // positioned at start or end of an editing host. In this case, 249 // we can unlink it even with arrow key press. 250 // TODO: This does not work as expected for `ArrowLeft` key press 251 // at start of an editing host. 252 if (mLastSelectionPoint == selectionStartPoint) { 253 unlink = true; 254 break; 255 } 256 // Otherwise, if selection is moved in a link element, we should 257 // keep inserting new text into the link. Note that this is our 258 // traditional behavior, but different from the other browsers. 259 // If this breaks some web apps, we should change our behavior, 260 // but let's wait a report because our traditional behavior allows 261 // user to type text into start/end of a link only when user 262 // moves caret inside the link with arrow keys. 263 unlink = 264 !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf( 265 linkElement); 266 break; 267 default: 268 // If selection is moved without arrow keys, e.g., `Home` and 269 // `End`, we should not insert new text into the link element. 270 // This is important for web-compat especially when the link is 271 // the last content in the block. 272 unlink = true; 273 break; 274 } 275 } else if (aReason & (nsISelectionListener::MOUSEDOWN_REASON | 276 nsISelectionListener::MOUSEUP_REASON)) { 277 // If the corresponding mouse event is fired in a link element, 278 // we should keep treating inputting content as content in the link, 279 // but otherwise, i.e., clicked outside the link, we should stop 280 // treating inputting content as content in the link. 281 unlink = !mouseEventFiredInLinkElement; 282 } else if (aReason & nsISelectionListener::JS_REASON) { 283 // If this is caused by a call of Selection API or something similar 284 // API, we should not contain new inserting content to the link. 285 unlink = true; 286 } else { 287 switch (aHTMLEditor.GetEditAction()) { 288 case EditAction::eDeleteBackward: 289 case EditAction::eDeleteForward: 290 case EditAction::eDeleteSelection: 291 case EditAction::eDeleteToBeginningOfSoftLine: 292 case EditAction::eDeleteToEndOfSoftLine: 293 case EditAction::eDeleteWordBackward: 294 case EditAction::eDeleteWordForward: 295 // This selection change is caused by the editor and the edit 296 // action is deleting content at edge of a link, we shouldn't 297 // keep the link style for new inserted content. 298 unlink = true; 299 break; 300 default: 301 break; 302 } 303 } 304 } else if (mLastSelectionPoint == selectionStartPoint) { 305 return; 306 } 307 308 mLastSelectionPoint = selectionStartPoint; 309 // We need to store only offset because referring child may be removed by 310 // we'll check the point later. 311 AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint); 312 } else { 313 if (aHTMLEditor.SelectionRef().RangeCount()) { 314 // If selection starts from a link, we shouldn't preserve the link style 315 // unless the range is entirely in the link. 316 EditorRawDOMRange firstRange(*aHTMLEditor.SelectionRef().GetRangeAt(0)); 317 if (firstRange.StartRef().IsInContentNode() && 318 HTMLEditUtils::IsContentInclusiveDescendantOfLink( 319 *firstRange.StartRef().ContainerAs<nsIContent>())) { 320 unlink = !HTMLEditUtils::IsRangeEntirelyInLink(firstRange); 321 } 322 } 323 mLastSelectionPoint.Clear(); 324 } 325 326 if (resetAllStyles) { 327 Reset(); 328 if (unlink) { 329 ClearLinkAndItsSpecifiedStyle(); 330 } 331 return; 332 } 333 334 if (unlink == IsExplicitlyLinkStyleCleared()) { 335 return; 336 } 337 338 // Even if we shouldn't touch existing style, we need to set/clear only link 339 // style in some cases. 340 if (unlink) { 341 ClearLinkAndItsSpecifiedStyle(); 342 return; 343 } 344 CancelClearingStyle(*nsGkAtoms::a, nullptr); 345 } 346 347 void PendingStyles::PreserveStyles( 348 const nsTArray<EditorInlineStyleAndValue>& aStylesToPreserve) { 349 for (const EditorInlineStyleAndValue& styleToPreserve : aStylesToPreserve) { 350 PreserveStyle(styleToPreserve.HTMLPropertyRef(), styleToPreserve.mAttribute, 351 styleToPreserve.mAttributeValue); 352 } 353 } 354 355 void PendingStyles::PreserveStyle(nsStaticAtom& aHTMLProperty, 356 nsAtom* aAttribute, 357 const nsAString& aAttributeValueOrCSSValue) { 358 // special case for big/small, these nest 359 if (nsGkAtoms::big == &aHTMLProperty) { 360 mRelativeFontSize++; 361 return; 362 } 363 if (nsGkAtoms::small == &aHTMLProperty) { 364 mRelativeFontSize--; 365 return; 366 } 367 368 Maybe<size_t> index = IndexOfPreservingStyle(aHTMLProperty, aAttribute); 369 if (index.isSome()) { 370 // If it's already set, update the value 371 mPreservingStyles[index.value()]->UpdateAttributeValueOrCSSValue( 372 aAttributeValueOrCSSValue); 373 return; 374 } 375 376 // font-size and font-family need to be applied outer-most because height of 377 // outer inline elements of them are computed without these styles. E.g., 378 // background-color may be applied bottom-half of the text. Therefore, we 379 // need to apply the font styles first. 380 UniquePtr<PendingStyle> style = MakeUnique<PendingStyle>( 381 &aHTMLProperty, aAttribute, aAttributeValueOrCSSValue); 382 if (&aHTMLProperty == nsGkAtoms::font && aAttribute != nsGkAtoms::bgcolor) { 383 MOZ_ASSERT(aAttribute == nsGkAtoms::color || 384 aAttribute == nsGkAtoms::face || aAttribute == nsGkAtoms::size); 385 mPreservingStyles.InsertElementAt(0, std::move(style)); 386 } else { 387 mPreservingStyles.AppendElement(std::move(style)); 388 } 389 390 CancelClearingStyle(aHTMLProperty, aAttribute); 391 } 392 393 void PendingStyles::ClearStyles( 394 const nsTArray<EditorInlineStyle>& aStylesToClear) { 395 for (const EditorInlineStyle& styleToClear : aStylesToClear) { 396 if (styleToClear.IsStyleToClearAllInlineStyles()) { 397 ClearAllStyles(); 398 return; 399 } 400 if (styleToClear.mHTMLProperty == nsGkAtoms::href || 401 styleToClear.mHTMLProperty == nsGkAtoms::name) { 402 ClearStyleInternal(nsGkAtoms::a, nullptr); 403 } else { 404 ClearStyleInternal(styleToClear.mHTMLProperty, styleToClear.mAttribute); 405 } 406 } 407 } 408 409 void PendingStyles::ClearStyleInternal( 410 nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, 411 SpecifiedStyle aSpecifiedStyle /* = SpecifiedStyle::Preserve */) { 412 if (IsStyleCleared(aHTMLProperty, aAttribute)) { 413 return; 414 } 415 416 CancelPreservingStyle(aHTMLProperty, aAttribute); 417 418 mClearingStyles.AppendElement(MakeUnique<PendingStyle>( 419 aHTMLProperty, aAttribute, u""_ns, aSpecifiedStyle)); 420 } 421 422 void PendingStyles::TakeAllPreservedStyles( 423 nsTArray<EditorInlineStyleAndValue>& aOutStylesAndValues) { 424 aOutStylesAndValues.SetCapacity(aOutStylesAndValues.Length() + 425 mPreservingStyles.Length()); 426 for (const UniquePtr<PendingStyle>& preservedStyle : mPreservingStyles) { 427 aOutStylesAndValues.AppendElement( 428 preservedStyle->GetAttribute() 429 ? EditorInlineStyleAndValue( 430 *preservedStyle->GetTag(), *preservedStyle->GetAttribute(), 431 preservedStyle->AttributeValueOrCSSValueRef()) 432 : EditorInlineStyleAndValue(*preservedStyle->GetTag())); 433 } 434 mPreservingStyles.Clear(); 435 } 436 437 /** 438 * TakeRelativeFontSize() hands back relative font value, which is then 439 * cleared out. 440 */ 441 int32_t PendingStyles::TakeRelativeFontSize() { 442 int32_t relSize = mRelativeFontSize; 443 mRelativeFontSize = 0; 444 return relSize; 445 } 446 447 PendingStyleState PendingStyles::GetStyleState( 448 nsStaticAtom& aHTMLProperty, nsAtom* aAttribute /* = nullptr */, 449 nsString* aOutNewAttributeValueOrCSSValue /* = nullptr */) const { 450 if (IndexOfPreservingStyle(aHTMLProperty, aAttribute, 451 aOutNewAttributeValueOrCSSValue) 452 .isSome()) { 453 return PendingStyleState::BeingPreserved; 454 } 455 456 if (IsStyleCleared(&aHTMLProperty, aAttribute)) { 457 return PendingStyleState::BeingCleared; 458 } 459 460 return PendingStyleState::NotUpdated; 461 } 462 463 void PendingStyles::CancelPreservingStyle(nsStaticAtom* aHTMLProperty, 464 nsAtom* aAttribute) { 465 if (!aHTMLProperty) { 466 mPreservingStyles.Clear(); 467 mRelativeFontSize = 0; 468 return; 469 } 470 Maybe<size_t> index = IndexOfPreservingStyle(*aHTMLProperty, aAttribute); 471 if (index.isSome()) { 472 mPreservingStyles.RemoveElementAt(index.value()); 473 } 474 } 475 476 void PendingStyles::CancelClearingStyle(nsStaticAtom& aHTMLProperty, 477 nsAtom* aAttribute) { 478 Maybe<size_t> index = 479 IndexOfStyleInArray(&aHTMLProperty, aAttribute, nullptr, mClearingStyles); 480 if (index.isSome()) { 481 mClearingStyles.RemoveElementAt(index.value()); 482 } 483 } 484 485 Maybe<size_t> PendingStyles::IndexOfStyleInArray( 486 nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, nsAString* aOutValue, 487 const nsTArray<UniquePtr<PendingStyle>>& aArray) { 488 if (aAttribute == nsGkAtoms::_empty) { 489 aAttribute = nullptr; 490 } 491 for (size_t i : IntegerRange(aArray.Length())) { 492 const UniquePtr<PendingStyle>& item = aArray[i]; 493 if (item->GetTag() == aHTMLProperty && item->GetAttribute() == aAttribute) { 494 if (aOutValue) { 495 *aOutValue = item->AttributeValueOrCSSValueRef(); 496 } 497 return Some(i); 498 } 499 } 500 return Nothing(); 501 } 502 503 } // namespace mozilla