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 "TypeInState.h"
10 #include "HTMLEditUtils.h"
11 #include "mozilla/EditAction.h"
12 #include "mozilla/EditorBase.h"
13 #include "mozilla/HTMLEditor.h"
14 #include "mozilla/mozalloc.h"
15 #include "mozilla/dom/AncestorIterator.h"
16 #include "mozilla/dom/MouseEvent.h"
17 #include "mozilla/dom/Selection.h"
18 #include "nsAString.h"
21 #include "nsGkAtoms.h"
23 #include "nsISupportsBase.h"
24 #include "nsISupportsImpl.h"
25 #include "nsReadableUtils.h"
26 #include "nsStringFwd.h"
28 // Workaround for windows headers
39 /********************************************************************
40 * mozilla::TypeInState
41 *******************************************************************/
43 NS_IMPL_CYCLE_COLLECTION_CLASS(TypeInState
)
45 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TypeInState
)
46 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastSelectionPoint
)
47 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
49 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TypeInState
)
50 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastSelectionPoint
)
51 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
53 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(TypeInState
, AddRef
)
54 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(TypeInState
, Release
)
56 TypeInState::TypeInState()
57 : mRelativeFontSize(0),
58 mLastSelectionCommand(Command::DoNothing
),
59 mMouseDownFiredInLinkElement(false),
60 mMouseUpFiredInLinkElement(false) {
64 TypeInState::~TypeInState() {
65 // Call Reset() to release any data that may be in
66 // mClearedArray and mSetArray.
71 nsresult
TypeInState::UpdateSelState(const HTMLEditor
& aHTMLEditor
) {
72 if (!aHTMLEditor
.SelectionRef().IsCollapsed()) {
77 aHTMLEditor
.GetFirstSelectionStartPoint
<EditorDOMPoint
>();
78 if (!mLastSelectionPoint
.IsSet()) {
79 return NS_ERROR_FAILURE
;
81 // We need to store only offset because referring child may be removed by
82 // we'll check the point later.
83 AutoEditorDOMPointChildInvalidator
saveOnlyOffset(mLastSelectionPoint
);
87 void TypeInState::PreHandleMouseEvent(const MouseEvent
& aMouseDownOrUpEvent
) {
88 MOZ_ASSERT(aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseDown
||
89 aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseUp
);
90 bool& eventFiredInLinkElement
=
91 aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseDown
92 ? mMouseDownFiredInLinkElement
93 : mMouseUpFiredInLinkElement
;
94 eventFiredInLinkElement
= false;
95 if (aMouseDownOrUpEvent
.DefaultPrevented()) {
98 // If mouse button is down or up in a link element, we shouldn't unlink
99 // it when we get a notification of selection change.
100 EventTarget
* target
= aMouseDownOrUpEvent
.GetExplicitOriginalTarget();
101 if (NS_WARN_IF(!target
)) {
104 nsIContent
* targetContent
= nsIContent::FromEventTarget(target
);
105 if (NS_WARN_IF(!targetContent
)) {
108 eventFiredInLinkElement
=
109 HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent
);
112 void TypeInState::PreHandleSelectionChangeCommand(Command aCommand
) {
113 mLastSelectionCommand
= aCommand
;
116 void TypeInState::PostHandleSelectionChangeCommand(
117 const HTMLEditor
& aHTMLEditor
, Command aCommand
) {
118 if (mLastSelectionCommand
!= aCommand
) {
122 // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`,
123 // it means that it didn't cause selection change.
124 if (!aHTMLEditor
.SelectionRef().IsCollapsed() ||
125 !aHTMLEditor
.SelectionRef().RangeCount()) {
129 const auto caretPoint
=
130 aHTMLEditor
.GetFirstSelectionStartPoint
<EditorRawDOMPoint
>();
131 if (NS_WARN_IF(!caretPoint
.IsSet())) {
135 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint
)) {
139 // If all styles are cleared or link style is explicitly set, we
140 // shouldn't reset them without caret move.
141 if (AreAllStylesCleared() || IsLinkStyleSet()) {
144 // And if non-link styles are cleared or some styles are set, we
145 // shouldn't reset them too, but we may need to change the link
147 if (AreSomeStylesSet() ||
148 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
149 ClearLinkPropAndDiscardItsSpecifiedStyle();
154 ClearLinkPropAndDiscardItsSpecifiedStyle();
157 void TypeInState::OnSelectionChange(const HTMLEditor
& aHTMLEditor
,
159 // XXX: Selection currently generates bogus selection changed notifications
160 // XXX: (bug 140303). It can notify us when the selection hasn't actually
161 // XXX: changed, and it notifies us more than once for the same change.
163 // XXX: The following code attempts to work around the bogus notifications,
164 // XXX: and should probably be removed once bug 140303 is fixed.
166 // XXX: This code temporarily fixes the problem where clicking the mouse in
167 // XXX: the same location clears the type-in-state.
169 const bool causedByFrameSelectionMoveCaret
=
170 (aReason
& (nsISelectionListener::KEYPRESS_REASON
|
171 nsISelectionListener::COLLAPSETOSTART_REASON
|
172 nsISelectionListener::COLLAPSETOEND_REASON
)) &&
173 !(aReason
& nsISelectionListener::JS_REASON
);
175 Command lastSelectionCommand
= mLastSelectionCommand
;
176 if (causedByFrameSelectionMoveCaret
) {
177 mLastSelectionCommand
= Command::DoNothing
;
180 bool mouseEventFiredInLinkElement
= false;
181 if (aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
182 nsISelectionListener::MOUSEUP_REASON
)) {
183 MOZ_ASSERT((aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
184 nsISelectionListener::MOUSEUP_REASON
)) !=
185 (nsISelectionListener::MOUSEDOWN_REASON
|
186 nsISelectionListener::MOUSEUP_REASON
));
187 bool& eventFiredInLinkElement
=
188 aReason
& nsISelectionListener::MOUSEDOWN_REASON
189 ? mMouseDownFiredInLinkElement
190 : mMouseUpFiredInLinkElement
;
191 mouseEventFiredInLinkElement
= eventFiredInLinkElement
;
192 eventFiredInLinkElement
= false;
196 bool resetAllStyles
= true;
197 if (aHTMLEditor
.SelectionRef().IsCollapsed() &&
198 aHTMLEditor
.SelectionRef().RangeCount()) {
199 const auto selectionStartPoint
=
200 aHTMLEditor
.GetFirstSelectionStartPoint
<EditorDOMPoint
>();
201 if (MOZ_UNLIKELY(NS_WARN_IF(!selectionStartPoint
.IsSet()))) {
205 if (mLastSelectionPoint
== selectionStartPoint
) {
206 // If all styles are cleared or link style is explicitly set, we
207 // shouldn't reset them without caret move.
208 if (AreAllStylesCleared() || IsLinkStyleSet()) {
211 // And if non-link styles are cleared or some styles are set, we
212 // shouldn't reset them too, but we may need to change the link
214 if (AreSomeStylesSet() ||
215 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
216 resetAllStyles
= false;
220 RefPtr
<Element
> linkElement
;
221 if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint
,
222 getter_AddRefs(linkElement
))) {
223 // If caret comes from outside of <a href> element, we should clear "link"
224 // style after reset.
225 if (causedByFrameSelectionMoveCaret
) {
226 MOZ_ASSERT(!(aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
227 nsISelectionListener::MOUSEUP_REASON
)));
228 // If caret is moves in a link per character, we should keep inserting
229 // new text to the link because user may want to keep extending the link
230 // text. Otherwise, e.g., using `End` or `Home` key. we should insert
231 // new text outside the link because it should be possible to user
232 // choose it, and this is similar to the other browsers.
233 switch (lastSelectionCommand
) {
234 case Command::CharNext
:
235 case Command::CharPrevious
:
236 case Command::MoveLeft
:
237 case Command::MoveLeft2
:
238 case Command::MoveRight
:
239 case Command::MoveRight2
:
240 // If selection becomes collapsed, we should unlink new text.
241 if (!mLastSelectionPoint
.IsSet()) {
245 // Special case, if selection isn't moved, it means that caret is
246 // positioned at start or end of an editing host. In this case,
247 // we can unlink it even with arrow key press.
248 // TODO: This does not work as expected for `ArrowLeft` key press
249 // at start of an editing host.
250 if (mLastSelectionPoint
== selectionStartPoint
) {
254 // Otherwise, if selection is moved in a link element, we should
255 // keep inserting new text into the link. Note that this is our
256 // traditional behavior, but different from the other browsers.
257 // If this breaks some web apps, we should change our behavior,
258 // but let's wait a report because our traditional behavior allows
259 // user to type text into start/end of a link only when user
260 // moves caret inside the link with arrow keys.
262 !mLastSelectionPoint
.GetContainer()->IsInclusiveDescendantOf(
266 // If selection is moved without arrow keys, e.g., `Home` and
267 // `End`, we should not insert new text into the link element.
268 // This is important for web-compat especially when the link is
269 // the last content in the block.
273 } else if (aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
274 nsISelectionListener::MOUSEUP_REASON
)) {
275 // If the corresponding mouse event is fired in a link element,
276 // we should keep treating inputting content as content in the link,
277 // but otherwise, i.e., clicked outside the link, we should stop
278 // treating inputting content as content in the link.
279 unlink
= !mouseEventFiredInLinkElement
;
280 } else if (aReason
& nsISelectionListener::JS_REASON
) {
281 // If this is caused by a call of Selection API or something similar
282 // API, we should not contain new inserting content to the link.
285 switch (aHTMLEditor
.GetEditAction()) {
286 case EditAction::eDeleteBackward
:
287 case EditAction::eDeleteForward
:
288 case EditAction::eDeleteSelection
:
289 case EditAction::eDeleteToBeginningOfSoftLine
:
290 case EditAction::eDeleteToEndOfSoftLine
:
291 case EditAction::eDeleteWordBackward
:
292 case EditAction::eDeleteWordForward
:
293 // This selection change is caused by the editor and the edit
294 // action is deleting content at edge of a link, we shouldn't
295 // keep the link style for new inserted content.
302 } else if (mLastSelectionPoint
== selectionStartPoint
) {
306 mLastSelectionPoint
= selectionStartPoint
;
307 // We need to store only offset because referring child may be removed by
308 // we'll check the point later.
309 AutoEditorDOMPointChildInvalidator
saveOnlyOffset(mLastSelectionPoint
);
311 if (aHTMLEditor
.SelectionRef().RangeCount()) {
312 // If selection starts from a link, we shouldn't preserve the link style
313 // unless the range is entirely in the link.
314 EditorRawDOMRange
firstRange(*aHTMLEditor
.SelectionRef().GetRangeAt(0));
315 if (firstRange
.StartRef().IsInContentNode() &&
316 HTMLEditUtils::IsContentInclusiveDescendantOfLink(
317 *firstRange
.StartRef().ContainerAsContent())) {
318 unlink
= !HTMLEditUtils::IsRangeEntirelyInLink(firstRange
);
321 mLastSelectionPoint
.Clear();
324 if (resetAllStyles
) {
327 ClearLinkPropAndDiscardItsSpecifiedStyle();
332 if (unlink
== IsExplicitlyLinkStyleCleared()) {
336 // Even if we shouldn't touch existing style, we need to set/clear only link
337 // style in some cases.
339 ClearLinkPropAndDiscardItsSpecifiedStyle();
340 } else if (!unlink
) {
341 RemovePropFromClearedList(nsGkAtoms::a
, nullptr);
345 void TypeInState::Reset() {
346 for (size_t i
= 0, n
= mClearedArray
.Length(); i
< n
; i
++) {
347 delete mClearedArray
[i
];
349 mClearedArray
.Clear();
350 for (size_t i
= 0, n
= mSetArray
.Length(); i
< n
; i
++) {
356 void TypeInState::SetProp(nsAtom
* aProp
, nsAtom
* aAttr
,
357 const nsAString
& aValue
) {
358 // special case for big/small, these nest
359 if (nsGkAtoms::big
== aProp
) {
363 if (nsGkAtoms::small
== aProp
) {
369 if (IsPropSet(aProp
, aAttr
, nullptr, index
)) {
370 // if it's already set, update the value
371 mSetArray
[index
]->value
= aValue
;
375 // Make a new propitem and add it to the list of set properties.
376 mSetArray
.AppendElement(new PropItem(aProp
, aAttr
, aValue
));
378 // remove it from the list of cleared properties, if we have a match
379 RemovePropFromClearedList(aProp
, aAttr
);
382 void TypeInState::ClearAllProps() {
383 // null prop means "all" props
384 ClearProp(nullptr, nullptr);
387 void TypeInState::ClearProp(nsAtom
* aProp
, nsAtom
* aAttr
) {
388 // if it's already cleared we are done
389 if (IsPropCleared(aProp
, aAttr
)) {
393 // make a new propitem
394 PropItem
* item
= new PropItem(aProp
, aAttr
, u
""_ns
);
396 // remove it from the list of set properties, if we have a match
397 RemovePropFromSetList(aProp
, aAttr
);
399 // add it to the list of cleared properties
400 mClearedArray
.AppendElement(item
);
403 void TypeInState::ClearLinkPropAndDiscardItsSpecifiedStyle() {
404 ClearProp(nsGkAtoms::a
, nullptr);
406 MOZ_ALWAYS_TRUE(IsPropCleared(nsGkAtoms::a
, nullptr, index
));
408 mClearedArray
[index
]->specifiedStyle
= SpecifiedStyle::Discard
;
413 * TakeClearProperty() hands back next property item on the clear list.
414 * Caller assumes ownership of PropItem and must delete it.
416 UniquePtr
<PropItem
> TypeInState::TakeClearProperty() {
417 return mClearedArray
.Length()
418 ? UniquePtr
<PropItem
>{mClearedArray
.PopLastElement()}
423 * TakeSetProperty() hands back next poroperty item on the set list.
424 * Caller assumes ownership of PropItem and must delete it.
426 UniquePtr
<PropItem
> TypeInState::TakeSetProperty() {
427 return mSetArray
.Length() ? UniquePtr
<PropItem
>{mSetArray
.PopLastElement()}
432 * TakeRelativeFontSize() hands back relative font value, which is then
435 int32_t TypeInState::TakeRelativeFontSize() {
436 int32_t relSize
= mRelativeFontSize
;
437 mRelativeFontSize
= 0;
441 void TypeInState::GetTypingState(bool& isSet
, bool& theSetting
, nsAtom
* aProp
,
442 nsAtom
* aAttr
, nsString
* aValue
) {
443 if (IsPropSet(aProp
, aAttr
, aValue
)) {
446 } else if (IsPropCleared(aProp
, aAttr
)) {
454 void TypeInState::RemovePropFromSetList(nsAtom
* aProp
, nsAtom
* aAttr
) {
458 for (size_t i
= 0, n
= mSetArray
.Length(); i
< n
; i
++) {
462 mRelativeFontSize
= 0;
463 } else if (FindPropInList(aProp
, aAttr
, nullptr, mSetArray
, index
)) {
464 delete mSetArray
[index
];
465 mSetArray
.RemoveElementAt(index
);
469 void TypeInState::RemovePropFromClearedList(nsAtom
* aProp
, nsAtom
* aAttr
) {
471 if (FindPropInList(aProp
, aAttr
, nullptr, mClearedArray
, index
)) {
472 delete mClearedArray
[index
];
473 mClearedArray
.RemoveElementAt(index
);
477 bool TypeInState::IsPropSet(nsAtom
* aProp
, nsAtom
* aAttr
, nsAString
* outValue
) {
479 return IsPropSet(aProp
, aAttr
, outValue
, i
);
482 bool TypeInState::IsPropSet(nsAtom
* aProp
, nsAtom
* aAttr
, nsAString
* outValue
,
484 if (aAttr
== nsGkAtoms::_empty
) {
487 // linear search. list should be short.
488 size_t count
= mSetArray
.Length();
489 for (size_t i
= 0; i
< count
; i
++) {
490 PropItem
* item
= mSetArray
[i
];
491 if (item
->tag
== aProp
&& item
->attr
== aAttr
) {
493 *outValue
= item
->value
;
502 bool TypeInState::IsPropCleared(nsAtom
* aProp
, nsAtom
* aAttr
) {
504 return IsPropCleared(aProp
, aAttr
, i
);
507 bool TypeInState::IsPropCleared(nsAtom
* aProp
, nsAtom
* aAttr
,
509 if (FindPropInList(aProp
, aAttr
, nullptr, mClearedArray
, outIndex
)) {
512 if (AreAllStylesCleared()) {
513 // special case for all props cleared
520 bool TypeInState::FindPropInList(nsAtom
* aProp
, nsAtom
* aAttr
,
522 const nsTArray
<PropItem
*>& aList
,
524 if (aAttr
== nsGkAtoms::_empty
) {
527 // linear search. list should be short.
528 size_t count
= aList
.Length();
529 for (size_t i
= 0; i
< count
; i
++) {
530 PropItem
* item
= aList
[i
];
531 if (item
->tag
== aProp
&& item
->attr
== aAttr
) {
533 *outValue
= item
->value
;
542 } // namespace mozilla