Bug 1772588 [wpt PR 34302] - [wpt] Add test for block-in-inline offsetParent., a...
[gecko.git] / editor / libeditor / TypeInState.cpp
blobf151d05172bd99dee0e99ac8400d65252f57a206
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"
8 #include <stddef.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"
19 #include "nsDebug.h"
20 #include "nsError.h"
21 #include "nsGkAtoms.h"
22 #include "nsINode.h"
23 #include "nsISupportsBase.h"
24 #include "nsISupportsImpl.h"
25 #include "nsReadableUtils.h"
26 #include "nsStringFwd.h"
28 // Workaround for windows headers
29 #ifdef SetProp
30 # undef SetProp
31 #endif
33 class nsAtom;
35 namespace mozilla {
37 using namespace dom;
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) {
61 Reset();
64 TypeInState::~TypeInState() {
65 // Call Reset() to release any data that may be in
66 // mClearedArray and mSetArray.
68 Reset();
71 nsresult TypeInState::UpdateSelState(const HTMLEditor& aHTMLEditor) {
72 if (!aHTMLEditor.SelectionRef().IsCollapsed()) {
73 return NS_OK;
76 mLastSelectionPoint =
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);
84 return NS_OK;
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()) {
96 return;
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)) {
102 return;
104 nsIContent* targetContent = nsIContent::FromEventTarget(target);
105 if (NS_WARN_IF(!targetContent)) {
106 return;
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) {
119 return;
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()) {
126 return;
129 const auto caretPoint =
130 aHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>();
131 if (NS_WARN_IF(!caretPoint.IsSet())) {
132 return;
135 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) {
136 return;
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()) {
142 return;
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
146 // style.
147 if (AreSomeStylesSet() ||
148 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
149 ClearLinkPropAndDiscardItsSpecifiedStyle();
150 return;
153 Reset();
154 ClearLinkPropAndDiscardItsSpecifiedStyle();
157 void TypeInState::OnSelectionChange(const HTMLEditor& aHTMLEditor,
158 int16_t aReason) {
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.
162 // XXX:
163 // XXX: The following code attempts to work around the bogus notifications,
164 // XXX: and should probably be removed once bug 140303 is fixed.
165 // XXX:
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;
195 bool unlink = 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()))) {
202 return;
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()) {
209 return;
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
213 // style.
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()) {
242 unlink = true;
243 break;
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) {
251 unlink = true;
252 break;
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.
261 unlink =
262 !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf(
263 linkElement);
264 break;
265 default:
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.
270 unlink = true;
271 break;
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.
283 unlink = true;
284 } else {
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.
296 unlink = true;
297 break;
298 default:
299 break;
302 } else if (mLastSelectionPoint == selectionStartPoint) {
303 return;
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);
310 } else {
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) {
325 Reset();
326 if (unlink) {
327 ClearLinkPropAndDiscardItsSpecifiedStyle();
329 return;
332 if (unlink == IsExplicitlyLinkStyleCleared()) {
333 return;
336 // Even if we shouldn't touch existing style, we need to set/clear only link
337 // style in some cases.
338 if (unlink) {
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++) {
351 delete mSetArray[i];
353 mSetArray.Clear();
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) {
360 mRelativeFontSize++;
361 return;
363 if (nsGkAtoms::small == aProp) {
364 mRelativeFontSize--;
365 return;
368 int32_t index;
369 if (IsPropSet(aProp, aAttr, nullptr, index)) {
370 // if it's already set, update the value
371 mSetArray[index]->value = aValue;
372 return;
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)) {
390 return;
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);
405 int32_t index = -1;
406 MOZ_ALWAYS_TRUE(IsPropCleared(nsGkAtoms::a, nullptr, index));
407 if (index >= 0) {
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()}
419 : nullptr;
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()}
428 : nullptr;
432 * TakeRelativeFontSize() hands back relative font value, which is then
433 * cleared out.
435 int32_t TypeInState::TakeRelativeFontSize() {
436 int32_t relSize = mRelativeFontSize;
437 mRelativeFontSize = 0;
438 return relSize;
441 void TypeInState::GetTypingState(bool& isSet, bool& theSetting, nsAtom* aProp,
442 nsAtom* aAttr, nsString* aValue) {
443 if (IsPropSet(aProp, aAttr, aValue)) {
444 isSet = true;
445 theSetting = true;
446 } else if (IsPropCleared(aProp, aAttr)) {
447 isSet = true;
448 theSetting = false;
449 } else {
450 isSet = false;
454 void TypeInState::RemovePropFromSetList(nsAtom* aProp, nsAtom* aAttr) {
455 int32_t index;
456 if (!aProp) {
457 // clear _all_ props
458 for (size_t i = 0, n = mSetArray.Length(); i < n; i++) {
459 delete mSetArray[i];
461 mSetArray.Clear();
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) {
470 int32_t index;
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) {
478 int32_t i;
479 return IsPropSet(aProp, aAttr, outValue, i);
482 bool TypeInState::IsPropSet(nsAtom* aProp, nsAtom* aAttr, nsAString* outValue,
483 int32_t& outIndex) {
484 if (aAttr == nsGkAtoms::_empty) {
485 aAttr = nullptr;
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) {
492 if (outValue) {
493 *outValue = item->value;
495 outIndex = i;
496 return true;
499 return false;
502 bool TypeInState::IsPropCleared(nsAtom* aProp, nsAtom* aAttr) {
503 int32_t i;
504 return IsPropCleared(aProp, aAttr, i);
507 bool TypeInState::IsPropCleared(nsAtom* aProp, nsAtom* aAttr,
508 int32_t& outIndex) {
509 if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, outIndex)) {
510 return true;
512 if (AreAllStylesCleared()) {
513 // special case for all props cleared
514 outIndex = -1;
515 return true;
517 return false;
520 bool TypeInState::FindPropInList(nsAtom* aProp, nsAtom* aAttr,
521 nsAString* outValue,
522 const nsTArray<PropItem*>& aList,
523 int32_t& outIndex) {
524 if (aAttr == nsGkAtoms::_empty) {
525 aAttr = nullptr;
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) {
532 if (outValue) {
533 *outValue = item->value;
535 outIndex = i;
536 return true;
539 return false;
542 } // namespace mozilla