diff --git a/dom/base/nsRange.cpp b/dom/base/nsRange.cpp index a40ba04a07..a51dd5e7b5 100644 --- a/dom/base/nsRange.cpp +++ b/dom/base/nsRange.cpp @@ -998,7 +998,16 @@ nsRange::DoSetRange(nsINode* aStartN, uint32_t aStartOffset, // Notify any selection listeners. This has to occur last because otherwise the world // could be observed by a selection listener while the range was in an invalid state. if (mSelection) { - mSelection->NotifySelectionListeners(mCalledByJS); + // Our internal code should not move focus with using this instance while + // it's calling Selection::NotifySelectionListeners() which may move focus + // or calls selection listeners. So, let's set mCalledByJS to false here + // since non-*JS() methods don't set it to false. + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = false; + // Be aware, this range may be modified or stop being a range for selection + // after this call. Additionally, the selection instance may have gone. + RefPtr selection = mSelection; + selection->NotifySelectionListeners(calledByJSRestorer.SavedValue()); } } @@ -1228,7 +1237,8 @@ nsRange::IsValidBoundary(nsINode* aNode) void nsRange::SetStartJS(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; SetStart(aNode, aOffset, aErr); } @@ -1290,7 +1300,8 @@ nsRange::SetStart(nsINode* aParent, uint32_t aOffset) void nsRange::SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; SetStartBefore(aNode, aErr); } @@ -1328,7 +1339,8 @@ nsRange::SetStartBefore(nsIDOMNode* aSibling) void nsRange::SetStartAfterJS(nsINode& aNode, ErrorResult& aErr) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; SetStartAfter(aNode, aErr); } @@ -1366,7 +1378,8 @@ nsRange::SetStartAfter(nsIDOMNode* aSibling) void nsRange::SetEndJS(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; SetEnd(aNode, aOffset, aErr); } @@ -1487,7 +1500,8 @@ nsRange::SetStartAndEnd(nsINode* aStartParent, uint32_t aStartOffset, void nsRange::SetEndBeforeJS(nsINode& aNode, ErrorResult& aErr) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; SetEndBefore(aNode, aErr); } @@ -1525,7 +1539,8 @@ nsRange::SetEndBefore(nsIDOMNode* aSibling) void nsRange::SetEndAfterJS(nsINode& aNode, ErrorResult& aErr) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; SetEndAfter(aNode, aErr); } @@ -1578,7 +1593,8 @@ nsRange::Collapse(bool aToStart) void nsRange::CollapseJS(bool aToStart) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; Unused << Collapse(aToStart); } @@ -1596,7 +1612,8 @@ nsRange::SelectNode(nsIDOMNode* aN) void nsRange::SelectNodeJS(nsINode& aNode, ErrorResult& aErr) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; SelectNode(aNode, aErr); } @@ -1642,7 +1659,8 @@ nsRange::SelectNodeContents(nsIDOMNode* aN) void nsRange::SelectNodeContentsJS(nsINode& aNode, ErrorResult& aErr) { - AutoCalledByJSSetter markAsCalledByJS(*this); + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; SelectNodeContents(aNode, aErr); } diff --git a/dom/base/nsRange.h b/dom/base/nsRange.h index f96bdd4c4d..cfd6055fef 100644 --- a/dom/base/nsRange.h +++ b/dom/base/nsRange.h @@ -413,7 +413,7 @@ protected: // Assume that this is guaranteed that this is held by the caller when // this is used. (Note that we cannot use AutoRestore for mCalledByJS // due to a bit field.) - class MOZ_RAII AutoCalledByJSSetter final + class MOZ_RAII AutoCalledByJSRestore final { private: nsRange& mRange; @@ -421,18 +421,18 @@ protected: MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER public: - explicit AutoCalledByJSSetter(nsRange& aRange - MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + explicit AutoCalledByJSRestore(nsRange& aRange + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) : mRange(aRange) , mOldValue(aRange.mCalledByJS) { MOZ_GUARD_OBJECT_NOTIFIER_INIT; - mRange.mCalledByJS = true; } - ~AutoCalledByJSSetter() + ~AutoCalledByJSRestore() { mRange.mCalledByJS = mOldValue; } + bool SavedValue() const { return mOldValue; } }; struct MOZ_STACK_CLASS AutoInvalidateSelection diff --git a/layout/generic/Selection.h b/layout/generic/Selection.h index 5470acecdd..b20e5d7bf6 100644 --- a/layout/generic/Selection.h +++ b/layout/generic/Selection.h @@ -21,8 +21,12 @@ struct CachedOffsetForFrame; class nsAutoScrollTimer; class nsIContentIterator; +class nsIDocument; +class nsIEditor; class nsIFrame; +class nsIHTMLEditor; class nsFrameSelection; +class nsPIDOMWindowOuter; struct SelectionDetails; class nsCopySupport; class nsHTMLCopyEncoder; @@ -359,6 +363,43 @@ private: */ nsresult AddItemInternal(nsRange* aRange, int32_t* aOutIndex); + nsIDocument* GetDocument() const; + nsPIDOMWindowOuter* GetWindow() const; + nsIEditor* GetEditor() const; + + /** + * GetCommonEditingHostForAllRanges() returns common editing host of all + * ranges if there is. If at least one of the ranges is in non-editable + * element, returns nullptr. See following examples for the detail: + * + *
+ * an[cestor + *
+ * non-editable + *
+ * desc]endant + * in this case, this returns div#a because div#c is also in div#a. + * + *
+ * an[ce]stor + *
+ * non-editable + *
+ * de[sc]endant + * in this case, this returns div#a because second range is also in div#a + * and common ancestor of the range (i.e., div#c) is editable. + * + *
+ * an[ce]stor + *
+ * [non]-editable + *
+ * de[sc]endant + * in this case, this returns nullptr because the second range is in + * non-editable area. + */ + Element* GetCommonEditingHostForAllRanges(); + // These are the ranges inside this selection. They are kept sorted in order // of DOM start position. // diff --git a/layout/generic/nsSelection.cpp b/layout/generic/nsSelection.cpp index fd67440599..19ef1c69ca 100644 --- a/layout/generic/nsSelection.cpp +++ b/layout/generic/nsSelection.cpp @@ -85,6 +85,7 @@ static NS_DEFINE_CID(kFrameTraversalCID, NS_FRAMETRAVERSAL_CID); #include "nsIEditor.h" #include "nsIHTMLEditor.h" #include "nsFocusManager.h" +#include "nsPIDOMWindow.h" using namespace mozilla; using namespace mozilla::dom; @@ -1877,6 +1878,7 @@ printf(" * TakeFocus - moving into new cell\n"); // Don't notify selection listeners if batching is on: if (GetBatching()) return NS_OK; + // Be aware, the Selection instance may be destroyed after this call. return NotifySelectionListeners(SelectionType::eNormal); } @@ -1917,6 +1919,7 @@ nsFrameSelection::SetDragState(bool aState) mDragSelectingCells = false; // Notify that reason is mouse up. PostReason(nsISelectionListener::MOUSEUP_REASON); + // Be aware, the Selection instance may be destroyed after this call. NotifySelectionListeners(SelectionType::eNormal); } } @@ -2411,6 +2414,7 @@ nsFrameSelection::EndBatchChanges(int16_t aReason) int16_t postReason = PopReason() | aReason; PostReason(postReason); mChangesDuringBatching = false; + // Be aware, the Selection instance may be destroyed after this call. NotifySelectionListeners(SelectionType::eNormal); } } @@ -2422,7 +2426,8 @@ nsFrameSelection::NotifySelectionListeners(SelectionType aSelectionType) int8_t index = GetIndexFromSelectionType(aSelectionType); if (index >=0 && mDomSelections[index]) { - return mDomSelections[index]->NotifySelectionListeners(); + RefPtr selection = mDomSelections[index]; + return selection->NotifySelectionListeners(); } return NS_ERROR_FAILURE; } @@ -4946,7 +4951,10 @@ Selection::RemoveAllRanges(ErrorResult& aRv) RefPtr frameSelection = mFrameSelection; frameSelection->ClearTableCellSelection(); + // Be aware, this instance may be destroyed after this call. + // XXX Why doesn't this call Selection::NotifySelectionListener() directly? result = frameSelection->NotifySelectionListeners(GetType()); + // Also need to notify the frames! // PresShell::CharacterDataChanged should do that on DocumentChanged if (NS_FAILED(result)) { @@ -5031,6 +5039,8 @@ Selection::AddRangeInternal(nsRange& aRange, nsIDocument* aDocument, if (!mFrameSelection) return;//nothing to do + // Be aware, this instance may be destroyed after this call. + // XXX Why doesn't this call Selection::NotifySelectionListener() directly? RefPtr frameSelection = mFrameSelection; result = frameSelection->NotifySelectionListeners(GetType()); if (NS_FAILED(result)) { @@ -5126,6 +5136,9 @@ Selection::RemoveRange(nsRange& aRange, ErrorResult& aRv) if (!mFrameSelection) return;//nothing to do + + // Be aware, this instance may be destroyed after this call. + // XXX Why doesn't this call Selection::NotifySelectionListener() directly? RefPtr frameSelection = mFrameSelection; rv = frameSelection->NotifySelectionListeners(GetType()); if (NS_FAILED(rv)) { @@ -5241,6 +5254,9 @@ Selection::Collapse(nsINode& aParentNode, uint32_t aOffset, ErrorResult& aRv) } setAnchorFocusRange(0); selectFrames(presContext, range, true); + + // Be aware, this instance may be destroyed after this call. + // XXX Why doesn't this call Selection::NotifySelectionListener() directly? result = frameSelection->NotifySelectionListeners(GetType()); if (NS_FAILED(result)) { aRv.Throw(result); @@ -5816,6 +5832,9 @@ Selection::Extend(nsINode& aParentNode, uint32_t aOffset, ErrorResult& aRv) printf ("Sel. Extend to %p %s %d\n", content.get(), nsAtomCString(content->NodeInfo()->NameAtom()).get(), aOffset); #endif + + // Be aware, this instance may be destroyed after this call. + // XXX Why doesn't this call Selection::NotifySelectionListener() directly? RefPtr frameSelection = mFrameSelection; res = frameSelection->NotifySelectionListeners(GetType()); if (NS_FAILED(res)) { @@ -5989,6 +6008,30 @@ Selection::GetPresShell() const return mFrameSelection->GetShell(); } +nsIDocument* +Selection::GetDocument() const +{ + nsIPresShell* presShell = GetPresShell(); + return presShell ? presShell->GetDocument() : nullptr; +} + +nsPIDOMWindowOuter* +Selection::GetWindow() const +{ + nsIDocument* document = GetDocument(); + return document ? document->GetWindow() : nullptr; +} + +nsIEditor* +Selection::GetEditor() const +{ + nsPresContext* presContext = GetPresContext(); + if (!presContext) { + return nullptr; + } + return nsContentUtils::GetHTMLEditor(presContext); +} + nsIFrame * Selection::GetSelectionAnchorGeometry(SelectionRegion aRegion, nsRect* aRect) { @@ -6290,6 +6333,46 @@ Selection::RemoveSelectionListener(nsISelectionListener* aListenerToRemove, } } +Element* +Selection::GetCommonEditingHostForAllRanges() +{ + Element* editingHost = nullptr; + for (RangeData& rangeData : mRanges) { + nsRange* range = rangeData.mRange; + MOZ_ASSERT(range); + nsINode* commonAncestorNode = range->GetCommonAncestor(); + if (!commonAncestorNode || !commonAncestorNode->IsContent()) { + return nullptr; + } + nsIContent* commonAncestor = commonAncestorNode->AsContent(); + Element* foundEditingHost = commonAncestor->GetEditingHost(); + // Even when common ancestor is a non-editable element in a contenteditable + // element, we don't need to move focus to the contenteditable element + // because Chromium doesn't set focus to it. + if (!foundEditingHost) { + return nullptr; + } + if (!editingHost) { + editingHost = foundEditingHost; + continue; + } + if (editingHost == foundEditingHost) { + continue; + } + if (nsContentUtils::ContentIsDescendantOf(foundEditingHost, editingHost)) { + continue; + } + if (nsContentUtils::ContentIsDescendantOf(editingHost, foundEditingHost)) { + editingHost = foundEditingHost; + continue; + } + // editingHost and foundEditingHost are not a descendant of the other. + // So, there is no common editing host. + return nullptr; + } + return editingHost; +} + nsresult Selection::NotifySelectionListeners(bool aCalledByJS) { @@ -6304,6 +6387,50 @@ Selection::NotifySelectionListeners() if (!mFrameSelection) return NS_OK;//nothing to do + // Our internal code should not move focus with using this class while + // this moves focus nor from selection listeners. + AutoRestore calledByJSRestorer(mCalledByJS); + mCalledByJS = false; + + // When normal selection is changed by Selection API, we need to move focus + // if common ancestor of all ranges are in an editing host. Note that we + // don't need to move focus *to* the other focusable node, because other + // browsers don't do this, either. + if (mSelectionType == SelectionType::eNormal && + calledByJSRestorer.SavedValue()) { + nsPIDOMWindowOuter* window = GetWindow(); + nsIDocument* document = GetDocument(); + // If the document is in design mode or doesn't have contenteditable + // element, we don't need to move focus. + if (window && document && !document->HasFlag(NODE_IS_EDITABLE) && + GetEditor()) { + RefPtr newEditingHost = GetCommonEditingHostForAllRanges(); + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + nsCOMPtr focusedWindow; + nsIContent* focusedContent = + fm->GetFocusedDescendant(window, false, getter_AddRefs(focusedWindow)); + nsCOMPtr focusedElement = do_QueryInterface(focusedContent); + // When all selected ranges are in an editing host, it should take focus. + if (newEditingHost && newEditingHost != focusedElement) { + MOZ_ASSERT(!newEditingHost->IsInNativeAnonymousSubtree()); + nsCOMPtr domElementToFocus = + do_QueryInterface(newEditingHost->AsDOMNode()); + // Note that don't steal focus from focused window if the window doesn't + // have focus. + fm->SetFocus(domElementToFocus, nsIFocusManager::FLAG_NOSWITCHFRAME); + } + // Otherwise, if current focused element is an editing host, it should + // be blurred if there is no common editing host of the selected ranges. + else if (!newEditingHost && focusedElement && + focusedElement == focusedElement->GetEditingHost()) { + IgnoredErrorResult err; + focusedElement->Blur(err); + NS_WARNING_ASSERTION(!err.Failed(), + "Failed to blur focused element"); + } + } + } + RefPtr frameSelection = mFrameSelection; if (frameSelection->GetBatching()) { frameSelection->SetDirty();