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 (!aSelection
.IsCollapsed()) {
76 mLastSelectionPoint
= EditorBase::GetStartPoint(aSelection
);
77 if (!mLastSelectionPoint
.IsSet()) {
78 return NS_ERROR_FAILURE
;
80 // We need to store only offset because referring child may be removed by
81 // we'll check the point later.
82 AutoEditorDOMPointChildInvalidator
saveOnlyOffset(mLastSelectionPoint
);
86 void TypeInState::PreHandleMouseEvent(const MouseEvent
& aMouseDownOrUpEvent
) {
87 MOZ_ASSERT(aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseDown
||
88 aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseUp
);
89 bool& eventFiredInLinkElement
=
90 aMouseDownOrUpEvent
.WidgetEventPtr()->mMessage
== eMouseDown
91 ? mMouseDownFiredInLinkElement
92 : mMouseUpFiredInLinkElement
;
93 eventFiredInLinkElement
= false;
94 if (aMouseDownOrUpEvent
.DefaultPrevented()) {
97 // If mouse button is down or up in a link element, we shouldn't unlink
98 // it when we get a notification of selection change.
99 EventTarget
* target
= aMouseDownOrUpEvent
.GetExplicitOriginalTarget();
100 if (NS_WARN_IF(!target
)) {
103 nsCOMPtr
<nsIContent
> targetContent
= do_QueryInterface(target
);
104 if (NS_WARN_IF(!targetContent
)) {
107 eventFiredInLinkElement
=
108 HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent
);
111 void TypeInState::PreHandleSelectionChangeCommand(Command aCommand
) {
112 mLastSelectionCommand
= aCommand
;
115 void TypeInState::PostHandleSelectionChangeCommand(
116 const HTMLEditor
& aHTMLEditor
, Command aCommand
) {
117 if (mLastSelectionCommand
!= aCommand
) {
121 // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`,
122 // it means that it didn't cause selection change.
123 if (!aHTMLEditor
.SelectionRef().IsCollapsed() ||
124 !aHTMLEditor
.SelectionRef().RangeCount()) {
128 EditorRawDOMPoint
caretPoint(
129 EditorBase::GetStartPoint(aHTMLEditor
.SelectionRef()));
130 if (NS_WARN_IF(!caretPoint
.IsSet())) {
134 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint
)) {
138 // If all styles are cleared or link style is explicitly set, we
139 // shouldn't reset them without caret move.
140 if (AreAllStylesCleared() || IsLinkStyleSet()) {
143 // And if non-link styles are cleared or some styles are set, we
144 // shouldn't reset them too, but we may need to change the link
146 if (AreSomeStylesSet() ||
147 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
148 ClearLinkPropAndDiscardItsSpecifiedStyle();
153 ClearLinkPropAndDiscardItsSpecifiedStyle();
156 void TypeInState::OnSelectionChange(const HTMLEditor
& aHTMLEditor
,
158 // XXX: Selection currently generates bogus selection changed notifications
159 // XXX: (bug 140303). It can notify us when the selection hasn't actually
160 // XXX: changed, and it notifies us more than once for the same change.
162 // XXX: The following code attempts to work around the bogus notifications,
163 // XXX: and should probably be removed once bug 140303 is fixed.
165 // XXX: This code temporarily fixes the problem where clicking the mouse in
166 // XXX: the same location clears the type-in-state.
168 const bool causedByFrameSelectionMoveCaret
=
169 (aReason
& (nsISelectionListener::KEYPRESS_REASON
|
170 nsISelectionListener::COLLAPSETOSTART_REASON
|
171 nsISelectionListener::COLLAPSETOEND_REASON
)) &&
172 !(aReason
& nsISelectionListener::JS_REASON
);
174 Command lastSelectionCommand
= mLastSelectionCommand
;
175 if (causedByFrameSelectionMoveCaret
) {
176 mLastSelectionCommand
= Command::DoNothing
;
179 bool mouseEventFiredInLinkElement
= false;
180 if (aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
181 nsISelectionListener::MOUSEUP_REASON
)) {
182 MOZ_ASSERT((aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
183 nsISelectionListener::MOUSEUP_REASON
)) !=
184 (nsISelectionListener::MOUSEDOWN_REASON
|
185 nsISelectionListener::MOUSEUP_REASON
));
186 bool& eventFiredInLinkElement
=
187 aReason
& nsISelectionListener::MOUSEDOWN_REASON
188 ? mMouseDownFiredInLinkElement
189 : mMouseUpFiredInLinkElement
;
190 mouseEventFiredInLinkElement
= eventFiredInLinkElement
;
191 eventFiredInLinkElement
= false;
195 bool resetAllStyles
= true;
196 if (aHTMLEditor
.SelectionRef().IsCollapsed() &&
197 aHTMLEditor
.SelectionRef().RangeCount()) {
198 EditorRawDOMPoint
selectionStartPoint(
199 EditorBase::GetStartPoint(aHTMLEditor
.SelectionRef()));
200 if (NS_WARN_IF(!selectionStartPoint
.IsSet())) {
204 if (mLastSelectionPoint
== selectionStartPoint
) {
205 // If all styles are cleared or link style is explicitly set, we
206 // shouldn't reset them without caret move.
207 if (AreAllStylesCleared() || IsLinkStyleSet()) {
210 // And if non-link styles are cleared or some styles are set, we
211 // shouldn't reset them too, but we may need to change the link
213 if (AreSomeStylesSet() ||
214 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
215 resetAllStyles
= false;
219 RefPtr
<Element
> linkElement
;
220 if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint
,
221 getter_AddRefs(linkElement
))) {
222 // If caret comes from outside of <a href> element, we should clear "link"
223 // style after reset.
224 if (causedByFrameSelectionMoveCaret
) {
225 MOZ_ASSERT(!(aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
226 nsISelectionListener::MOUSEUP_REASON
)));
227 // If caret is moves in a link per character, we should keep inserting
228 // new text to the link because user may want to keep extending the link
229 // text. Otherwise, e.g., using `End` or `Home` key. we should insert
230 // new text outside the link because it should be possible to user
231 // choose it, and this is similar to the other browsers.
232 switch (lastSelectionCommand
) {
233 case Command::CharNext
:
234 case Command::CharPrevious
:
235 case Command::MoveLeft
:
236 case Command::MoveLeft2
:
237 case Command::MoveRight
:
238 case Command::MoveRight2
:
239 // If selection becomes collapsed, we should unlink new text.
240 if (!mLastSelectionPoint
.IsSet()) {
244 // Special case, if selection isn't moved, it means that caret is
245 // positioned at start or end of an editing host. In this case,
246 // we can unlink it even with arrow key press.
247 // TODO: This does not work as expected for `ArrowLeft` key press
248 // at start of an editing host.
249 if (mLastSelectionPoint
== selectionStartPoint
) {
253 // Otherwise, if selection is moved in a link element, we should
254 // keep inserting new text into the link. Note that this is our
255 // traditional behavior, but different from the other browsers.
256 // If this breaks some web apps, we should change our behavior,
257 // but let's wait a report because our traditional behavior allows
258 // user to type text into start/end of a link only when user
259 // moves caret inside the link with arrow keys.
261 !mLastSelectionPoint
.GetContainer()->IsInclusiveDescendantOf(
265 // If selection is moved without arrow keys, e.g., `Home` and
266 // `End`, we should not insert new text into the link element.
267 // This is important for web-compat especially when the link is
268 // the last content in the block.
272 } else if (aReason
& (nsISelectionListener::MOUSEDOWN_REASON
|
273 nsISelectionListener::MOUSEUP_REASON
)) {
274 // If the corresponding mouse event is fired in a link element,
275 // we should keep treating inputting content as content in the link,
276 // but otherwise, i.e., clicked outside the link, we should stop
277 // treating inputting content as content in the link.
278 unlink
= !mouseEventFiredInLinkElement
;
279 } else if (aReason
& nsISelectionListener::JS_REASON
) {
280 // If this is caused by a call of Selection API or something similar
281 // API, we should not contain new inserting content to the link.
284 switch (aHTMLEditor
.GetEditAction()) {
285 case EditAction::eDeleteBackward
:
286 case EditAction::eDeleteForward
:
287 case EditAction::eDeleteSelection
:
288 case EditAction::eDeleteToBeginningOfSoftLine
:
289 case EditAction::eDeleteToEndOfSoftLine
:
290 case EditAction::eDeleteWordBackward
:
291 case EditAction::eDeleteWordForward
:
292 // This selection change is caused by the editor and the edit
293 // action is deleting content at edge of a link, we shouldn't
294 // keep the link style for new inserted content.
301 } else if (mLastSelectionPoint
== selectionStartPoint
) {
305 mLastSelectionPoint
= selectionStartPoint
;
306 // We need to store only offset because referring child may be removed by
307 // we'll check the point later.
308 AutoEditorDOMPointChildInvalidator
saveOnlyOffset(mLastSelectionPoint
);
310 if (aHTMLEditor
.SelectionRef().RangeCount()) {
311 // If selection starts from a link, we shouldn't preserve the link style
312 // unless the range is entirely in the link.
313 EditorRawDOMRange
firstRange(*aHTMLEditor
.SelectionRef().GetRangeAt(0));
314 if (firstRange
.StartRef().IsInContentNode() &&
315 HTMLEditUtils::IsContentInclusiveDescendantOfLink(
316 *firstRange
.StartRef().ContainerAsContent())) {
317 unlink
= !HTMLEditUtils::IsRangeEntirelyInLink(firstRange
);
320 mLastSelectionPoint
.Clear();
323 if (resetAllStyles
) {
326 ClearLinkPropAndDiscardItsSpecifiedStyle();
331 if (unlink
== IsExplicitlyLinkStyleCleared()) {
335 // Even if we shouldn't touch existing style, we need to set/clear only link
336 // style in some cases.
338 ClearLinkPropAndDiscardItsSpecifiedStyle();
339 } else if (!unlink
) {
340 RemovePropFromClearedList(nsGkAtoms::a
, nullptr);
344 void TypeInState::Reset() {
345 for (size_t i
= 0, n
= mClearedArray
.Length(); i
< n
; i
++) {
346 delete mClearedArray
[i
];
348 mClearedArray
.Clear();
349 for (size_t i
= 0, n
= mSetArray
.Length(); i
< n
; i
++) {
355 void TypeInState::SetProp(nsAtom
* aProp
, nsAtom
* aAttr
,
356 const nsAString
& aValue
) {
357 // special case for big/small, these nest
358 if (nsGkAtoms::big
== aProp
) {
362 if (nsGkAtoms::small
== aProp
) {
368 if (IsPropSet(aProp
, aAttr
, nullptr, index
)) {
369 // if it's already set, update the value
370 mSetArray
[index
]->value
= aValue
;
374 // Make a new propitem and add it to the list of set properties.
375 mSetArray
.AppendElement(new PropItem(aProp
, aAttr
, aValue
));
377 // remove it from the list of cleared properties, if we have a match
378 RemovePropFromClearedList(aProp
, aAttr
);
381 void TypeInState::ClearAllProps() {
382 // null prop means "all" props
383 ClearProp(nullptr, nullptr);
386 void TypeInState::ClearProp(nsAtom
* aProp
, nsAtom
* aAttr
) {
387 // if it's already cleared we are done
388 if (IsPropCleared(aProp
, aAttr
)) {
392 // make a new propitem
393 PropItem
* item
= new PropItem(aProp
, aAttr
, u
""_ns
);
395 // remove it from the list of set properties, if we have a match
396 RemovePropFromSetList(aProp
, aAttr
);
398 // add it to the list of cleared properties
399 mClearedArray
.AppendElement(item
);
402 void TypeInState::ClearLinkPropAndDiscardItsSpecifiedStyle() {
403 ClearProp(nsGkAtoms::a
, nullptr);
405 MOZ_ALWAYS_TRUE(IsPropCleared(nsGkAtoms::a
, nullptr, index
));
407 mClearedArray
[index
]->specifiedStyle
= SpecifiedStyle::Discard
;
412 * TakeClearProperty() hands back next property item on the clear list.
413 * Caller assumes ownership of PropItem and must delete it.
415 UniquePtr
<PropItem
> TypeInState::TakeClearProperty() {
416 return mClearedArray
.Length()
417 ? UniquePtr
<PropItem
>{mClearedArray
.PopLastElement()}
422 * TakeSetProperty() hands back next poroperty item on the set list.
423 * Caller assumes ownership of PropItem and must delete it.
425 UniquePtr
<PropItem
> TypeInState::TakeSetProperty() {
426 return mSetArray
.Length() ? UniquePtr
<PropItem
>{mSetArray
.PopLastElement()}
431 * TakeRelativeFontSize() hands back relative font value, which is then
434 int32_t TypeInState::TakeRelativeFontSize() {
435 int32_t relSize
= mRelativeFontSize
;
436 mRelativeFontSize
= 0;
440 void TypeInState::GetTypingState(bool& isSet
, bool& theSetting
, nsAtom
* aProp
,
441 nsAtom
* aAttr
, nsString
* aValue
) {
442 if (IsPropSet(aProp
, aAttr
, aValue
)) {
445 } else if (IsPropCleared(aProp
, aAttr
)) {
453 void TypeInState::RemovePropFromSetList(nsAtom
* aProp
, nsAtom
* aAttr
) {
457 for (size_t i
= 0, n
= mSetArray
.Length(); i
< n
; i
++) {
461 mRelativeFontSize
= 0;
462 } else if (FindPropInList(aProp
, aAttr
, nullptr, mSetArray
, index
)) {
463 delete mSetArray
[index
];
464 mSetArray
.RemoveElementAt(index
);
468 void TypeInState::RemovePropFromClearedList(nsAtom
* aProp
, nsAtom
* aAttr
) {
470 if (FindPropInList(aProp
, aAttr
, nullptr, mClearedArray
, index
)) {
471 delete mClearedArray
[index
];
472 mClearedArray
.RemoveElementAt(index
);
476 bool TypeInState::IsPropSet(nsAtom
* aProp
, nsAtom
* aAttr
, nsAString
* outValue
) {
478 return IsPropSet(aProp
, aAttr
, outValue
, i
);
481 bool TypeInState::IsPropSet(nsAtom
* aProp
, nsAtom
* aAttr
, nsAString
* outValue
,
483 if (aAttr
== nsGkAtoms::_empty
) {
486 // linear search. list should be short.
487 size_t count
= mSetArray
.Length();
488 for (size_t i
= 0; i
< count
; i
++) {
489 PropItem
* item
= mSetArray
[i
];
490 if (item
->tag
== aProp
&& item
->attr
== aAttr
) {
492 *outValue
= item
->value
;
501 bool TypeInState::IsPropCleared(nsAtom
* aProp
, nsAtom
* aAttr
) {
503 return IsPropCleared(aProp
, aAttr
, i
);
506 bool TypeInState::IsPropCleared(nsAtom
* aProp
, nsAtom
* aAttr
,
508 if (FindPropInList(aProp
, aAttr
, nullptr, mClearedArray
, outIndex
)) {
511 if (AreAllStylesCleared()) {
512 // special case for all props cleared
519 bool TypeInState::FindPropInList(nsAtom
* aProp
, nsAtom
* aAttr
,
521 const nsTArray
<PropItem
*>& aList
,
523 if (aAttr
== nsGkAtoms::_empty
) {
526 // linear search. list should be short.
527 size_t count
= aList
.Length();
528 for (size_t i
= 0; i
< count
; i
++) {
529 PropItem
* item
= aList
[i
];
530 if (item
->tag
== aProp
&& item
->attr
== aAttr
) {
532 *outValue
= item
->value
;
541 } // namespace mozilla