Bug 1686495 [wpt PR 27132] - Add tests for proposed WebDriver Shadow DOM support...
[gecko.git] / editor / libeditor / TypeInState.cpp
blob24533e8f97fca1cb12faca686722b5eade770009
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(Selection* aSelection) {
72 if (NS_WARN_IF(!aSelection)) {
73 return NS_ERROR_INVALID_ARG;
76 if (!aSelection->IsCollapsed()) {
77 return NS_OK;
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);
87 return NS_OK;
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()) {
99 return;
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)) {
105 return;
107 nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
108 if (NS_WARN_IF(!targetContent)) {
109 return;
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) {
122 return;
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()) {
129 return;
132 EditorRawDOMPoint caretPoint(
133 EditorBase::GetStartPoint(*aHTMLEditor.SelectionRefPtr()));
134 if (NS_WARN_IF(!caretPoint.IsSet())) {
135 return;
138 if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) {
139 return;
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()) {
145 return;
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
149 // style.
150 if (AreSomeStylesSet() ||
151 (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
152 ClearProp(nsGkAtoms::a, nullptr);
153 return;
156 Reset();
157 ClearProp(nsGkAtoms::a, nullptr);
160 void TypeInState::OnSelectionChange(const HTMLEditor& aHTMLEditor,
161 int16_t aReason) {
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.
165 // XXX:
166 // XXX: The following code attempts to work around the bogus notifications,
167 // XXX: and should probably be removed once bug 140303 is fixed.
168 // XXX:
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;
198 bool unlink = 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())) {
205 return;
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()) {
212 return;
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
216 // style.
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()) {
245 unlink = true;
246 break;
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) {
254 unlink = true;
255 break;
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.
264 unlink =
265 !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf(
266 linkElement);
267 break;
268 default:
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.
273 unlink = true;
274 break;
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.
286 unlink = true;
287 } else {
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.
299 unlink = true;
300 break;
301 default:
302 break;
305 } else if (mLastSelectionPoint == selectionStartPoint) {
306 return;
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);
313 } else {
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) {
329 Reset();
330 if (unlink) {
331 ClearProp(nsGkAtoms::a, nullptr);
333 return;
336 if (unlink == IsExplicitlyLinkStyleCleared()) {
337 return;
340 // Even if we shouldn't touch existing style, we need to set/clear only link
341 // style in some cases.
342 if (unlink) {
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++) {
355 delete mSetArray[i];
357 mSetArray.Clear();
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) {
364 mRelativeFontSize++;
365 return;
367 if (nsGkAtoms::small == aProp) {
368 mRelativeFontSize--;
369 return;
372 int32_t index;
373 if (IsPropSet(aProp, aAttr, nullptr, index)) {
374 // if it's already set, update the value
375 mSetArray[index]->value = aValue;
376 return;
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)) {
394 return;
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()}
414 : nullptr;
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()}
423 : nullptr;
427 * TakeRelativeFontSize() hands back relative font value, which is then
428 * cleared out.
430 int32_t TypeInState::TakeRelativeFontSize() {
431 int32_t relSize = mRelativeFontSize;
432 mRelativeFontSize = 0;
433 return relSize;
436 void TypeInState::GetTypingState(bool& isSet, bool& theSetting, nsAtom* aProp,
437 nsAtom* aAttr, nsString* aValue) {
438 if (IsPropSet(aProp, aAttr, aValue)) {
439 isSet = true;
440 theSetting = true;
441 } else if (IsPropCleared(aProp, aAttr)) {
442 isSet = true;
443 theSetting = false;
444 } else {
445 isSet = false;
449 void TypeInState::RemovePropFromSetList(nsAtom* aProp, nsAtom* aAttr) {
450 int32_t index;
451 if (!aProp) {
452 // clear _all_ props
453 for (size_t i = 0, n = mSetArray.Length(); i < n; i++) {
454 delete mSetArray[i];
456 mSetArray.Clear();
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) {
465 int32_t index;
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) {
473 int32_t i;
474 return IsPropSet(aProp, aAttr, outValue, i);
477 bool TypeInState::IsPropSet(nsAtom* aProp, nsAtom* aAttr, nsAString* outValue,
478 int32_t& outIndex) {
479 if (aAttr == nsGkAtoms::_empty) {
480 aAttr = nullptr;
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) {
487 if (outValue) {
488 *outValue = item->value;
490 outIndex = i;
491 return true;
494 return false;
497 bool TypeInState::IsPropCleared(nsAtom* aProp, nsAtom* aAttr) {
498 int32_t i;
499 return IsPropCleared(aProp, aAttr, i);
502 bool TypeInState::IsPropCleared(nsAtom* aProp, nsAtom* aAttr,
503 int32_t& outIndex) {
504 if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, outIndex)) {
505 return true;
507 if (AreAllStylesCleared()) {
508 // special case for all props cleared
509 outIndex = -1;
510 return true;
512 return false;
515 bool TypeInState::FindPropInList(nsAtom* aProp, nsAtom* aAttr,
516 nsAString* outValue,
517 const nsTArray<PropItem*>& aList,
518 int32_t& outIndex) {
519 if (aAttr == nsGkAtoms::_empty) {
520 aAttr = nullptr;
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) {
527 if (outValue) {
528 *outValue = item->value;
530 outIndex = i;
531 return true;
534 return false;
537 } // namespace mozilla