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(Selection
* aSelection
) {
72 if (NS_WARN_IF(!aSelection
)) {
73 return NS_ERROR_INVALID_ARG
;
76 if (!aSelection
->IsCollapsed()) {
80 mLastSelectionPoint
= EditorBase::GetStartPoint(*aSelection
);
81 if (!mLastSelectionPoint
.IsSet()) {
82 return NS_ERROR_FAILURE
;
84 // We need to store only offset because referring child may be removed by
85 // we'll check the point later.
86 AutoEditorDOMPointChildInvalidator
saveOnlyOffset(mLastSelectionPoint
);
90 void TypeInState::PreHandleMouseEvent(const MouseEvent
& aMouseDownOrUpEvent
) {
91 MOZ_ASSERT(aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseDown
||
92 aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseUp
);
93 bool& eventFiredInLinkElement
=
94 aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseDown
95 ? mMouseDownFiredInLinkElement
96 : mMouseUpFiredInLinkElement
;
97 eventFiredInLinkElement
= false;
98 if (aMouseDownOrUpEvent
.DefaultPrevented()) {
101 // If mouse button is down or up in a link element, we shouldn't unlink
102 // it when we get a notification of selection change.
103 EventTarget
* target
= aMouseDownOrUpEvent
.GetExplicitOriginalTarget();
104 if (NS_WARN_IF(!target
)) {
107 nsCOMPtr
<nsIContent
> targetContent
= do_QueryInterface(target
);
108 if (NS_WARN_IF(!targetContent
)) {
111 eventFiredInLinkElement
=
112 HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent
);
115 void TypeInState::PreHandleSelectionChangeCommand(Command aCommand
) {
116 mLastSelectionCommand
= aCommand
;
119 void TypeInState::PostHandleSelectionChangeCommand(
120 const HTMLEditor
& aHTMLEditor
, Command aCommand
) {
121 if (mLastSelectionCommand
!= aCommand
) {
125 // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`,
126 // it means that it didn't cause selection change.
127 if (!aHTMLEditor
.SelectionRefPtr()->IsCollapsed() ||
128 !aHTMLEditor
.SelectionRefPtr()->RangeCount()) {
132 EditorRawDOMPoint
caretPoint(
133 EditorBase::GetStartPoint(*aHTMLEditor
.SelectionRefPtr()));
134 if (NS_WARN_IF(!caretPoint
.IsSet())) {
138 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint
)) {
142 // If all styles are cleared or link style is explicitly set, we
143 // shouldn't reset them without caret move.
144 if (AreAllStylesCleared() || IsLinkStyleSet()) {
147 // And if non-link styles are cleared or some styles are set, we
148 // shouldn't reset them too, but we may need to change the link
150 if (AreSomeStylesSet() ||
151 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
152 ClearProp(nsGkAtoms::a
, nullptr);
157 ClearProp(nsGkAtoms::a
, nullptr);
160 void TypeInState::OnSelectionChange(const HTMLEditor
& aHTMLEditor
,
162 // XXX: Selection currently generates bogus selection changed notifications
163 // XXX: (bug 140303). It can notify us when the selection hasn't actually
164 // XXX: changed, and it notifies us more than once for the same change.
166 // XXX: The following code attempts to work around the bogus notifications,
167 // XXX: and should probably be removed once bug 140303 is fixed.
169 // XXX: This code temporarily fixes the problem where clicking the mouse in
170 // XXX: the same location clears the type-in-state.
172 const bool causedByFrameSelectionMoveCaret
=
173 (aReason
& (nsISelectionListener::KEYPRESS_REASON
|
174 nsISelectionListener::COLLAPSETOSTART_REASON
|
175 nsISelectionListener::COLLAPSETOEND_REASON
)) &&
176 !(aReason
& nsISelectionListener::JS_REASON
);
178 Command lastSelectionCommand
= mLastSelectionCommand
;
179 if (causedByFrameSelectionMoveCaret
) {
180 mLastSelectionCommand
= Command::DoNothing
;
183 bool mouseEventFiredInLinkElement
= false;
184 if (aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
185 nsISelectionListener::MOUSEUP_REASON
)) {
186 MOZ_ASSERT((aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
187 nsISelectionListener::MOUSEUP_REASON
)) !=
188 (nsISelectionListener::MOUSEDOWN_REASON
|
189 nsISelectionListener::MOUSEUP_REASON
));
190 bool& eventFiredInLinkElement
=
191 aReason
& nsISelectionListener::MOUSEDOWN_REASON
192 ? mMouseDownFiredInLinkElement
193 : mMouseUpFiredInLinkElement
;
194 mouseEventFiredInLinkElement
= eventFiredInLinkElement
;
195 eventFiredInLinkElement
= false;
199 bool resetAllStyles
= true;
200 if (aHTMLEditor
.SelectionRefPtr()->IsCollapsed() &&
201 aHTMLEditor
.SelectionRefPtr()->RangeCount()) {
202 EditorRawDOMPoint
selectionStartPoint(
203 EditorBase::GetStartPoint(*aHTMLEditor
.SelectionRefPtr()));
204 if (NS_WARN_IF(!selectionStartPoint
.IsSet())) {
208 if (mLastSelectionPoint
== selectionStartPoint
) {
209 // If all styles are cleared or link style is explicitly set, we
210 // shouldn't reset them without caret move.
211 if (AreAllStylesCleared() || IsLinkStyleSet()) {
214 // And if non-link styles are cleared or some styles are set, we
215 // shouldn't reset them too, but we may need to change the link
217 if (AreSomeStylesSet() ||
218 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
219 resetAllStyles
= false;
223 RefPtr
<Element
> linkElement
;
224 if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint
,
225 getter_AddRefs(linkElement
))) {
226 // If caret comes from outside of <a href> element, we should clear "link"
227 // style after reset.
228 if (causedByFrameSelectionMoveCaret
) {
229 MOZ_ASSERT(!(aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
230 nsISelectionListener::MOUSEUP_REASON
)));
231 // If caret is moves in a link per character, we should keep inserting
232 // new text to the link because user may want to keep extending the link
233 // text. Otherwise, e.g., using `End` or `Home` key. we should insert
234 // new text outside the link because it should be possible to user
235 // choose it, and this is similar to the other browsers.
236 switch (lastSelectionCommand
) {
237 case Command::CharNext
:
238 case Command::CharPrevious
:
239 case Command::MoveLeft
:
240 case Command::MoveLeft2
:
241 case Command::MoveRight
:
242 case Command::MoveRight2
:
243 // If selection becomes collapsed, we should unlink new text.
244 if (!mLastSelectionPoint
.IsSet()) {
248 // Special case, if selection isn't moved, it means that caret is
249 // positioned at start or end of an editing host. In this case,
250 // we can unlink it even with arrow key press.
251 // TODO: This does not work as expected for `ArrowLeft` key press
252 // at start of an editing host.
253 if (mLastSelectionPoint
== selectionStartPoint
) {
257 // Otherwise, if selection is moved in a link element, we should
258 // keep inserting new text into the link. Note that this is our
259 // traditional behavior, but different from the other browsers.
260 // If this breaks some web apps, we should change our behavior,
261 // but let's wait a report because our traditional behavior allows
262 // user to type text into start/end of a link only when user
263 // moves caret inside the link with arrow keys.
265 !mLastSelectionPoint
.GetContainer()->IsInclusiveDescendantOf(
269 // If selection is moved without arrow keys, e.g., `Home` and
270 // `End`, we should not insert new text into the link element.
271 // This is important for web-compat especially when the link is
272 // the last content in the block.
276 } else if (aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
277 nsISelectionListener::MOUSEUP_REASON
)) {
278 // If the corresponding mouse event is fired in a link element,
279 // we should keep treating inputting content as content in the link,
280 // but otherwise, i.e., clicked outside the link, we should stop
281 // treating inputting content as content in the link.
282 unlink
= !mouseEventFiredInLinkElement
;
283 } else if (aReason
& nsISelectionListener::JS_REASON
) {
284 // If this is caused by a call of Selection API or something similar
285 // API, we should not contain new inserting content to the link.
288 switch (aHTMLEditor
.GetEditAction()) {
289 case EditAction::eDeleteBackward
:
290 case EditAction::eDeleteForward
:
291 case EditAction::eDeleteSelection
:
292 case EditAction::eDeleteToBeginningOfSoftLine
:
293 case EditAction::eDeleteToEndOfSoftLine
:
294 case EditAction::eDeleteWordBackward
:
295 case EditAction::eDeleteWordForward
:
296 // This selection change is caused by the editor and the edit
297 // action is deleting content at edge of a link, we shouldn't
298 // keep the link style for new inserted content.
305 } else if (mLastSelectionPoint
== selectionStartPoint
) {
309 mLastSelectionPoint
= selectionStartPoint
;
310 // We need to store only offset because referring child may be removed by
311 // we'll check the point later.
312 AutoEditorDOMPointChildInvalidator
saveOnlyOffset(mLastSelectionPoint
);
314 if (aHTMLEditor
.SelectionRefPtr()->RangeCount()) {
315 // If selection starts from a link, we shouldn't preserve the link style
316 // unless the range is entirely in the link.
317 EditorRawDOMRange
firstRange(
318 *aHTMLEditor
.SelectionRefPtr()->GetRangeAt(0));
319 if (firstRange
.StartRef().IsInContentNode() &&
320 HTMLEditUtils::IsContentInclusiveDescendantOfLink(
321 *firstRange
.StartRef().ContainerAsContent())) {
322 unlink
= !HTMLEditUtils::IsRangeEntirelyInLink(firstRange
);
325 mLastSelectionPoint
.Clear();
328 if (resetAllStyles
) {
331 ClearProp(nsGkAtoms::a
, nullptr);
336 if (unlink
== IsExplicitlyLinkStyleCleared()) {
340 // Even if we shouldn't touch existing style, we need to set/clear only link
341 // style in some cases.
343 ClearProp(nsGkAtoms::a
, nullptr);
344 } else if (!unlink
) {
345 RemovePropFromClearedList(nsGkAtoms::a
, nullptr);
349 void TypeInState::Reset() {
350 for (size_t i
= 0, n
= mClearedArray
.Length(); i
< n
; i
++) {
351 delete mClearedArray
[i
];
353 mClearedArray
.Clear();
354 for (size_t i
= 0, n
= mSetArray
.Length(); i
< n
; i
++) {
360 void TypeInState::SetProp(nsAtom
* aProp
, nsAtom
* aAttr
,
361 const nsAString
& aValue
) {
362 // special case for big/small, these nest
363 if (nsGkAtoms::big
== aProp
) {
367 if (nsGkAtoms::small
== aProp
) {
373 if (IsPropSet(aProp
, aAttr
, nullptr, index
)) {
374 // if it's already set, update the value
375 mSetArray
[index
]->value
= aValue
;
379 // Make a new propitem and add it to the list of set properties.
380 mSetArray
.AppendElement(new PropItem(aProp
, aAttr
, aValue
));
382 // remove it from the list of cleared properties, if we have a match
383 RemovePropFromClearedList(aProp
, aAttr
);
386 void TypeInState::ClearAllProps() {
387 // null prop means "all" props
388 ClearProp(nullptr, nullptr);
391 void TypeInState::ClearProp(nsAtom
* aProp
, nsAtom
* aAttr
) {
392 // if it's already cleared we are done
393 if (IsPropCleared(aProp
, aAttr
)) {
397 // make a new propitem
398 PropItem
* item
= new PropItem(aProp
, aAttr
, u
""_ns
);
400 // remove it from the list of set properties, if we have a match
401 RemovePropFromSetList(aProp
, aAttr
);
403 // add it to the list of cleared properties
404 mClearedArray
.AppendElement(item
);
408 * TakeClearProperty() hands back next property item on the clear list.
409 * Caller assumes ownership of PropItem and must delete it.
411 UniquePtr
<PropItem
> TypeInState::TakeClearProperty() {
412 return mClearedArray
.Length()
413 ? UniquePtr
<PropItem
>{mClearedArray
.PopLastElement()}
418 * TakeSetProperty() hands back next poroperty item on the set list.
419 * Caller assumes ownership of PropItem and must delete it.
421 UniquePtr
<PropItem
> TypeInState::TakeSetProperty() {
422 return mSetArray
.Length() ? UniquePtr
<PropItem
>{mSetArray
.PopLastElement()}
427 * TakeRelativeFontSize() hands back relative font value, which is then
430 int32_t TypeInState::TakeRelativeFontSize() {
431 int32_t relSize
= mRelativeFontSize
;
432 mRelativeFontSize
= 0;
436 void TypeInState::GetTypingState(bool& isSet
, bool& theSetting
, nsAtom
* aProp
,
437 nsAtom
* aAttr
, nsString
* aValue
) {
438 if (IsPropSet(aProp
, aAttr
, aValue
)) {
441 } else if (IsPropCleared(aProp
, aAttr
)) {
449 void TypeInState::RemovePropFromSetList(nsAtom
* aProp
, nsAtom
* aAttr
) {
453 for (size_t i
= 0, n
= mSetArray
.Length(); i
< n
; i
++) {
457 mRelativeFontSize
= 0;
458 } else if (FindPropInList(aProp
, aAttr
, nullptr, mSetArray
, index
)) {
459 delete mSetArray
[index
];
460 mSetArray
.RemoveElementAt(index
);
464 void TypeInState::RemovePropFromClearedList(nsAtom
* aProp
, nsAtom
* aAttr
) {
466 if (FindPropInList(aProp
, aAttr
, nullptr, mClearedArray
, index
)) {
467 delete mClearedArray
[index
];
468 mClearedArray
.RemoveElementAt(index
);
472 bool TypeInState::IsPropSet(nsAtom
* aProp
, nsAtom
* aAttr
, nsAString
* outValue
) {
474 return IsPropSet(aProp
, aAttr
, outValue
, i
);
477 bool TypeInState::IsPropSet(nsAtom
* aProp
, nsAtom
* aAttr
, nsAString
* outValue
,
479 if (aAttr
== nsGkAtoms::_empty
) {
482 // linear search. list should be short.
483 size_t count
= mSetArray
.Length();
484 for (size_t i
= 0; i
< count
; i
++) {
485 PropItem
* item
= mSetArray
[i
];
486 if (item
->tag
== aProp
&& item
->attr
== aAttr
) {
488 *outValue
= item
->value
;
497 bool TypeInState::IsPropCleared(nsAtom
* aProp
, nsAtom
* aAttr
) {
499 return IsPropCleared(aProp
, aAttr
, i
);
502 bool TypeInState::IsPropCleared(nsAtom
* aProp
, nsAtom
* aAttr
,
504 if (FindPropInList(aProp
, aAttr
, nullptr, mClearedArray
, outIndex
)) {
507 if (AreAllStylesCleared()) {
508 // special case for all props cleared
515 bool TypeInState::FindPropInList(nsAtom
* aProp
, nsAtom
* aAttr
,
517 const nsTArray
<PropItem
*>& aList
,
519 if (aAttr
== nsGkAtoms::_empty
) {
522 // linear search. list should be short.
523 size_t count
= aList
.Length();
524 for (size_t i
= 0; i
< count
; i
++) {
525 PropItem
* item
= aList
[i
];
526 if (item
->tag
== aProp
&& item
->attr
== aAttr
) {
528 *outValue
= item
->value
;
537 } // namespace mozilla