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"
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"
23 #include "nsGkAtoms.h"
25 #include "nsISupports.h"
26 #include "nsISupportsImpl.h"
27 #include "nsReadableUtils.h"
35 /********************************************************************
36 * mozilla::PendingStyle
37 *******************************************************************/
39 EditorInlineStyle
PendingStyle::ToInlineStyle() const {
40 return mTag
? EditorInlineStyle(*mTag
, mAttribute
)
41 : EditorInlineStyle::RemoveAllStyles();
44 EditorInlineStyleAndValue
PendingStyle::ToInlineStyleAndValue() const {
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()) {
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
);
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()) {
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
)) {
106 nsIContent
* targetContent
= nsIContent::FromEventTarget(target
);
107 if (NS_WARN_IF(!targetContent
)) {
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
) {
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()) {
131 const auto caretPoint
=
132 aHTMLEditor
.GetFirstSelectionStartPoint
<EditorRawDOMPoint
>();
133 if (NS_WARN_IF(!caretPoint
.IsSet())) {
137 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint
)) {
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()) {
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
149 if (AreSomeStylesSet() ||
150 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
151 ClearLinkAndItsSpecifiedStyle();
156 ClearLinkAndItsSpecifiedStyle();
159 void PendingStyles::OnSelectionChange(const HTMLEditor
& aHTMLEditor
,
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.
165 // XXX: The following code attempts to work around the bogus notifications,
166 // XXX: and should probably be removed once bug 140303 is fixed.
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;
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()))) {
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()) {
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
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()) {
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
) {
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.
264 !mLastSelectionPoint
.GetContainer()->IsInclusiveDescendantOf(
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.
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.
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.
304 } else if (mLastSelectionPoint
== selectionStartPoint
) {
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
);
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
) {
329 ClearLinkAndItsSpecifiedStyle();
334 if (unlink
== IsExplicitlyLinkStyleCleared()) {
338 // Even if we shouldn't touch existing style, we need to set/clear only link
339 // style in some cases.
341 ClearLinkAndItsSpecifiedStyle();
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
,
357 const nsAString
& aAttributeValueOrCSSValue
) {
358 // special case for big/small, these nest
359 if (nsGkAtoms::big
== &aHTMLProperty
) {
363 if (nsGkAtoms::small
== &aHTMLProperty
) {
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
);
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
));
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()) {
400 if (styleToClear
.mHTMLProperty
== nsGkAtoms::href
||
401 styleToClear
.mHTMLProperty
== nsGkAtoms::name
) {
402 ClearStyleInternal(nsGkAtoms::a
, nullptr);
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
)) {
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
441 int32_t PendingStyles::TakeRelativeFontSize() {
442 int32_t relSize
= mRelativeFontSize
;
443 mRelativeFontSize
= 0;
447 PendingStyleState
PendingStyles::GetStyleState(
448 nsStaticAtom
& aHTMLProperty
, nsAtom
* aAttribute
/* = nullptr */,
449 nsString
* aOutNewAttributeValueOrCSSValue
/* = nullptr */) const {
450 if (IndexOfPreservingStyle(aHTMLProperty
, aAttribute
,
451 aOutNewAttributeValueOrCSSValue
)
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;
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
) {
495 *aOutValue
= item
->AttributeValueOrCSSValueRef();
503 } // namespace mozilla