Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / editor / libeditor / PendingStyles.cpp
blob12666c52897d1f210e8a866f10087d0259d42c04
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/. */
6 #include "PendingStyles.h"
8 #include <stddef.h>
10 #include "EditAction.h"
11 #include "EditorBase.h"
12 #include "HTMLEditHelpers.h" // for EditorInlineStyle, EditorInlineStyleAndValue
13 #include "HTMLEditor.h"
14 #include "HTMLEditUtils.h"
16 #include "mozilla/mozalloc.h"
17 #include "mozilla/dom/AncestorIterator.h"
18 #include "mozilla/dom/MouseEvent.h"
19 #include "mozilla/dom/Selection.h"
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"
31 namespace mozilla {
33 using namespace dom;
35 /********************************************************************
36 * mozilla::PendingStyle
37 *******************************************************************/
39 EditorInlineStyle PendingStyle::ToInlineStyle() const {
40 return mTag ? EditorInlineStyle(*mTag, mAttribute)
41 : EditorInlineStyle::RemoveAllStyles();
44 EditorInlineStyleAndValue PendingStyle::ToInlineStyleAndValue() const {
45 MOZ_ASSERT(mTag);
46 return mAttribute ? EditorInlineStyleAndValue(*mTag, *mAttribute,
47 mAttributeValueOrCSSValue)
48 : EditorInlineStyleAndValue(*mTag);
51 /********************************************************************
52 * mozilla::PendingStyleCache
53 *******************************************************************/
55 EditorInlineStyle PendingStyleCache::ToInlineStyle() const {
56 return EditorInlineStyle(mTag, mAttribute);
59 /********************************************************************
60 * mozilla::PendingStyles
61 *******************************************************************/
63 NS_IMPL_CYCLE_COLLECTION_CLASS(PendingStyles)
65 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PendingStyles)
66 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastSelectionPoint)
67 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
69 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PendingStyles)
70 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastSelectionPoint)
71 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
73 nsresult PendingStyles::UpdateSelState(const HTMLEditor& aHTMLEditor) {
74 if (!aHTMLEditor.SelectionRef().IsCollapsed()) {
75 return NS_OK;
78 mLastSelectionPoint =
79 aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
80 if (!mLastSelectionPoint.IsSet()) {
81 return NS_ERROR_FAILURE;
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;
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;
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;
106 nsIContent* targetContent = nsIContent::FromEventTarget(target);
107 if (NS_WARN_IF(!targetContent)) {
108 return;
110 eventFiredInLinkElement =
111 HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent);
114 void PendingStyles::PreHandleSelectionChangeCommand(Command aCommand) {
115 mLastSelectionCommand = aCommand;
118 void PendingStyles::PostHandleSelectionChangeCommand(
119 const HTMLEditor& aHTMLEditor, Command aCommand) {
120 if (mLastSelectionCommand != aCommand) {
121 return;
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;
131 const auto caretPoint =
132 aHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>();
133 if (NS_WARN_IF(!caretPoint.IsSet())) {
134 return;
137 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) {
138 return;
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;
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;
155 Reset();
156 ClearLinkAndItsSpecifiedStyle();
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.
171 const bool causedByFrameSelectionMoveCaret =
172 (aReason & (nsISelectionListener::KEYPRESS_REASON |
173 nsISelectionListener::COLLAPSETOSTART_REASON |
174 nsISelectionListener::COLLAPSETOEND_REASON)) &&
175 !(aReason & nsISelectionListener::JS_REASON);
177 Command lastSelectionCommand = mLastSelectionCommand;
178 if (causedByFrameSelectionMoveCaret) {
179 mLastSelectionCommand = Command::DoNothing;
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;
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;
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;
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;
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;
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;
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;
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;
304 } else if (mLastSelectionPoint == selectionStartPoint) {
305 return;
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);
323 mLastSelectionPoint.Clear();
326 if (resetAllStyles) {
327 Reset();
328 if (unlink) {
329 ClearLinkAndItsSpecifiedStyle();
331 return;
334 if (unlink == IsExplicitlyLinkStyleCleared()) {
335 return;
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;
344 CancelClearingStyle(*nsGkAtoms::a, nullptr);
347 void PendingStyles::PreserveStyles(
348 const nsTArray<EditorInlineStyleAndValue>& aStylesToPreserve) {
349 for (const EditorInlineStyleAndValue& styleToPreserve : aStylesToPreserve) {
350 PreserveStyle(styleToPreserve.HTMLPropertyRef(), styleToPreserve.mAttribute,
351 styleToPreserve.mAttributeValue);
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;
363 if (nsGkAtoms::small == &aHTMLProperty) {
364 mRelativeFontSize--;
365 return;
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;
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));
390 CancelClearingStyle(aHTMLProperty, aAttribute);
393 void PendingStyles::ClearStyles(
394 const nsTArray<EditorInlineStyle>& aStylesToClear) {
395 for (const EditorInlineStyle& styleToClear : aStylesToClear) {
396 if (styleToClear.IsStyleToClearAllInlineStyles()) {
397 ClearAllStyles();
398 return;
400 if (styleToClear.mHTMLProperty == nsGkAtoms::href ||
401 styleToClear.mHTMLProperty == nsGkAtoms::name) {
402 ClearStyleInternal(nsGkAtoms::a, nullptr);
403 } else {
404 ClearStyleInternal(styleToClear.mHTMLProperty, styleToClear.mAttribute);
409 void PendingStyles::ClearStyleInternal(
410 nsStaticAtom* aHTMLProperty, nsAtom* aAttribute,
411 SpecifiedStyle aSpecifiedStyle /* = SpecifiedStyle::Preserve */) {
412 if (IsStyleCleared(aHTMLProperty, aAttribute)) {
413 return;
416 CancelPreservingStyle(aHTMLProperty, aAttribute);
418 mClearingStyles.AppendElement(MakeUnique<PendingStyle>(
419 aHTMLProperty, aAttribute, u""_ns, aSpecifiedStyle));
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()));
434 mPreservingStyles.Clear();
438 * TakeRelativeFontSize() hands back relative font value, which is then
439 * cleared out.
441 int32_t PendingStyles::TakeRelativeFontSize() {
442 int32_t relSize = mRelativeFontSize;
443 mRelativeFontSize = 0;
444 return relSize;
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;
456 if (IsStyleCleared(&aHTMLProperty, aAttribute)) {
457 return PendingStyleState::BeingCleared;
460 return PendingStyleState::NotUpdated;
463 void PendingStyles::CancelPreservingStyle(nsStaticAtom* aHTMLProperty,
464 nsAtom* aAttribute) {
465 if (!aHTMLProperty) {
466 mPreservingStyles.Clear();
467 mRelativeFontSize = 0;
468 return;
470 Maybe<size_t> index = IndexOfPreservingStyle(*aHTMLProperty, aAttribute);
471 if (index.isSome()) {
472 mPreservingStyles.RemoveElementAt(index.value());
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());
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;
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();
497 return Some(i);
500 return Nothing();
503 } // namespace mozilla