1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #include "HTMLEditor.h"
7 #include "CSSEditUtils.h"
8 #include "HTMLEditUtils.h"
10 #include "mozilla/Attributes.h"
11 #include "mozilla/PresShell.h"
12 #include "mozilla/PresShellInlines.h"
13 #include "mozilla/dom/BindContext.h"
14 #include "mozilla/dom/Element.h"
15 #include "mozilla/dom/EventTarget.h"
16 #include "mozilla/mozalloc.h"
17 #include "nsAString.h"
19 #include "nsComputedDOMStyle.h"
22 #include "nsGenericHTMLElement.h"
23 #include "nsGkAtoms.h"
25 #include "nsIContent.h"
27 #include "mozilla/dom/Document.h"
28 #include "nsIDocumentObserver.h"
29 #include "nsStubMutationObserver.h"
31 #include "nsISupportsImpl.h"
32 #include "nsISupportsUtils.h"
33 #include "nsLiteralString.h"
34 #include "nsPresContext.h"
35 #include "nsReadableUtils.h"
37 #include "nsStringFwd.h"
38 #include "nsStyledElement.h"
39 #include "nsUnicharUtils.h"
41 #include "nsContentUtils.h" // for nsAutoScriptBlocker
42 #include "nsROCSSPrimitiveValue.h"
44 class nsIDOMEventListener
;
50 // Retrieve the rounded number of CSS pixels from a computed CSS property.
52 // Note that this should only be called for properties whose resolved value
53 // is CSS pixels (like width, height, left, top, right, bottom, margin, padding,
54 // border-*-width, ...).
56 // See: https://drafts.csswg.org/cssom/#resolved-values
57 static int32_t GetCSSFloatValue(nsComputedDOMStyle
* aComputedStyle
,
58 const nsACString
& aProperty
) {
59 MOZ_ASSERT(aComputedStyle
);
61 // get the computed CSSValue of the property
63 nsresult rv
= aComputedStyle
->GetPropertyValue(aProperty
, value
);
65 NS_WARNING("nsComputedDOMStyle::GetPropertyValue() failed");
69 // We only care about resolved values, not a big deal if the element is
70 // undisplayed, for example, and the value is "auto" or what not.
71 int32_t val
= value
.ToInteger(&rv
);
72 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
), "nsAString::ToInteger() failed");
73 return NS_SUCCEEDED(rv
) ? val
: 0;
76 /******************************************************************************
77 * mozilla::ElementDeletionObserver
78 *****************************************************************************/
80 class ElementDeletionObserver final
: public nsStubMultiMutationObserver
{
82 ElementDeletionObserver(nsIContent
* aNativeAnonNode
,
83 Element
* aObservedElement
)
84 : mNativeAnonNode(aNativeAnonNode
), mObservedElement(aObservedElement
) {
85 AddMutationObserverToNode(aNativeAnonNode
);
86 AddMutationObserverToNode(aObservedElement
);
90 NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED
91 NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
94 ~ElementDeletionObserver() = default;
95 nsIContent
* mNativeAnonNode
;
96 Element
* mObservedElement
;
99 NS_IMPL_ISUPPORTS(ElementDeletionObserver
, nsIMutationObserver
)
101 void ElementDeletionObserver::ParentChainChanged(nsIContent
* aContent
) {
102 // If the native anonymous content has been unbound already in
103 // DeleteRefToAnonymousNode, mNativeAnonNode's parentNode is null.
104 if (aContent
!= mObservedElement
|| !mNativeAnonNode
||
105 mNativeAnonNode
->GetParent() != aContent
) {
109 ManualNACPtr::RemoveContentFromNACArray(mNativeAnonNode
);
111 mObservedElement
->RemoveMutationObserver(this);
112 mObservedElement
= nullptr;
113 mNativeAnonNode
->RemoveMutationObserver(this);
114 mNativeAnonNode
= nullptr;
118 void ElementDeletionObserver::NodeWillBeDestroyed(nsINode
* aNode
) {
119 NS_ASSERTION(aNode
== mNativeAnonNode
|| aNode
== mObservedElement
,
121 if (aNode
== mNativeAnonNode
) {
122 mObservedElement
->RemoveMutationObserver(this);
123 mObservedElement
= nullptr;
125 mNativeAnonNode
->RemoveMutationObserver(this);
126 mNativeAnonNode
->UnbindFromTree();
127 mNativeAnonNode
= nullptr;
133 /******************************************************************************
134 * mozilla::HTMLEditor
135 *****************************************************************************/
137 ManualNACPtr
HTMLEditor::CreateAnonymousElement(nsAtom
* aTag
,
138 nsIContent
& aParentContent
,
139 const nsAString
& aAnonClass
,
140 bool aIsCreatedHidden
) {
141 // Don't put anonymous editor element into non-HTML element.
142 // It is mainly for avoiding other anonymous element being inserted
143 // into <svg:use>, but in general we probably don't want to insert
144 // some random HTML anonymous element into a non-HTML element.
145 if (!aParentContent
.IsHTMLElement()) {
149 if (NS_WARN_IF(!GetDocument())) {
153 RefPtr
<PresShell
> presShell
= GetPresShell();
154 if (NS_WARN_IF(!presShell
)) {
158 // Create a new node through the element factory
159 RefPtr
<Element
> newElement
= CreateHTMLContent(aTag
);
161 NS_WARNING("EditorBase::CreateHTMLContent() failed");
165 // add the "hidden" class if needed
166 if (aIsCreatedHidden
) {
167 nsresult rv
= newElement
->SetAttr(kNameSpaceID_None
, nsGkAtoms::_class
,
170 NS_WARNING("Element::SetAttr(nsGkAtoms::_class, hidden) failed");
175 // add an _moz_anonclass attribute if needed
176 if (!aAnonClass
.IsEmpty()) {
177 nsresult rv
= newElement
->SetAttr(
178 kNameSpaceID_None
, nsGkAtoms::_moz_anonclass
, aAnonClass
, true);
180 NS_WARNING("Element::SetAttr(nsGkAtoms::_moz_anonclass) failed");
186 nsAutoScriptBlocker scriptBlocker
;
188 // establish parenthood of the element
189 newElement
->SetIsNativeAnonymousRoot();
190 BindContext
context(*aParentContent
.AsElement(),
191 BindContext::ForNativeAnonymous
);
192 nsresult rv
= newElement
->BindToTree(context
, aParentContent
);
194 NS_WARNING("Element::BindToTree(BindContext::ForNativeAnonymous) failed");
195 newElement
->UnbindFromTree();
200 ManualNACPtr
newNativeAnonymousContent(newElement
.forget());
202 // Must style the new element, otherwise the PostRecreateFramesFor call
203 // below will do nothing.
204 ServoStyleSet
* styleSet
= presShell
->StyleSet();
205 // Sometimes editor likes to append anonymous content to elements
206 // in display:none subtrees, so avoid styling in those cases.
207 if (ServoStyleSet::MayTraverseFrom(newNativeAnonymousContent
)) {
208 styleSet
->StyleNewSubtree(newNativeAnonymousContent
);
211 auto* observer
= new ElementDeletionObserver(newNativeAnonymousContent
,
212 aParentContent
.AsElement());
213 NS_ADDREF(observer
); // NodeWillBeDestroyed releases.
216 // Editor anonymous content gets passed to PostRecreateFramesFor... which
217 // can't _really_ deal with anonymous content (because it can't get the frame
218 // tree ordering right). But for us the ordering doesn't matter so this is
220 newNativeAnonymousContent
->SetProperty(nsGkAtoms::restylableAnonymousNode
,
221 reinterpret_cast<void*>(true));
224 // display the element
225 presShell
->PostRecreateFramesFor(newNativeAnonymousContent
);
227 return newNativeAnonymousContent
;
230 // Removes event listener and calls DeleteRefToAnonymousNode.
231 void HTMLEditor::RemoveListenerAndDeleteRef(const nsAString
& aEvent
,
232 nsIDOMEventListener
* aListener
,
234 ManualNACPtr aElement
,
235 PresShell
* aPresShell
) {
237 aElement
->RemoveEventListener(aEvent
, aListener
, aUseCapture
);
239 DeleteRefToAnonymousNode(std::move(aElement
), aPresShell
);
242 // Deletes all references to an anonymous element
243 void HTMLEditor::DeleteRefToAnonymousNode(ManualNACPtr aContent
,
244 PresShell
* aPresShell
) {
245 // call ContentRemoved() for the anonymous content
246 // node so its references get removed from the frame manager's
247 // undisplay map, and its layout frames get destroyed!
249 if (NS_WARN_IF(!aContent
)) {
253 if (NS_WARN_IF(!aContent
->GetParent())) {
254 // aContent was already removed?
258 nsAutoScriptBlocker scriptBlocker
;
259 // Need to check whether aPresShell has been destroyed (but not yet deleted).
261 if (aContent
->IsInComposedDoc() && aPresShell
&&
262 !aPresShell
->IsDestroying()) {
263 MOZ_ASSERT(aContent
->IsRootOfNativeAnonymousSubtree());
264 MOZ_ASSERT(!aContent
->GetPreviousSibling(), "NAC has no siblings");
266 // FIXME(emilio): This is the only caller to PresShell::ContentRemoved that
267 // passes NAC into it. This is not great!
268 aPresShell
->ContentRemoved(aContent
, nullptr);
271 // The ManualNACPtr destructor will invoke UnbindFromTree.
274 void HTMLEditor::HideAnonymousEditingUIs() {
275 if (mAbsolutelyPositionedObject
) {
276 HideGrabberInternal();
277 NS_ASSERTION(!mAbsolutelyPositionedObject
,
278 "HTMLEditor::HideGrabberInternal() failed, but ignored");
280 if (mInlineEditedCell
) {
281 HideInlineTableEditingUIInternal();
284 "HTMLEditor::HideInlineTableEditingUIInternal() failed, but ignored");
286 if (mResizedObject
) {
287 DebugOnly
<nsresult
> rvIgnored
= HideResizersInternal();
288 NS_WARNING_ASSERTION(
289 NS_SUCCEEDED(rvIgnored
),
290 "HTMLEditor::HideResizersInternal() failed, but ignored");
291 NS_ASSERTION(!mResizedObject
,
292 "HTMLEditor::HideResizersInternal() failed, but ignored");
296 void HTMLEditor::HideAnonymousEditingUIsIfUnnecessary() {
297 // XXX Perhaps, this is wrong approach to hide multiple UIs because
298 // hiding one UI may causes overwriting existing UI with newly
299 // created one. In such case, we will leak ovewritten UI.
300 if (!IsAbsolutePositionEditorEnabled() && mAbsolutelyPositionedObject
) {
301 // XXX If we're moving something, we need to cancel or commit the
303 HideGrabberInternal();
304 NS_ASSERTION(!mAbsolutelyPositionedObject
,
305 "HTMLEditor::HideGrabberInternal() failed, but ignored");
307 if (!IsInlineTableEditorEnabled() && mInlineEditedCell
) {
308 // XXX If we're resizing a table element, we need to cancel or commit the
310 HideInlineTableEditingUIInternal();
313 "HTMLEditor::HideInlineTableEditingUIInternal() failed, but ignored");
315 if (!IsObjectResizerEnabled() && mResizedObject
) {
316 // XXX If we're resizing something, we need to cancel or commit the
318 DebugOnly
<nsresult
> rvIgnored
= HideResizersInternal();
319 NS_WARNING_ASSERTION(
320 NS_SUCCEEDED(rvIgnored
),
321 "HTMLEditor::HideResizersInternal() failed, but ignored");
322 NS_ASSERTION(!mResizedObject
,
323 "HTMLEditor::HideResizersInternal() failed, but ignored");
327 NS_IMETHODIMP
HTMLEditor::CheckSelectionStateForAnonymousButtons() {
328 AutoEditActionDataSetter
editActionData(*this, EditAction::eNotEditing
);
329 if (NS_WARN_IF(!editActionData
.CanHandle())) {
330 return NS_ERROR_NOT_INITIALIZED
;
333 nsresult rv
= RefreshEditingUI();
334 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
),
335 "HTMLEditor::RefereshEditingUI() failed");
336 return EditorBase::ToGenericNSResult(rv
);
339 nsresult
HTMLEditor::RefreshEditingUI() {
340 MOZ_ASSERT(IsEditActionDataAvailable());
342 // First, we need to remove unnecessary editing UI now since some of them
343 // may be disabled while them are visible.
344 HideAnonymousEditingUIsIfUnnecessary();
346 // early way out if all contextual UI extensions are disabled
347 if (!IsObjectResizerEnabled() && !IsAbsolutePositionEditorEnabled() &&
348 !IsInlineTableEditorEnabled()) {
352 // Don't change selection state if we're moving.
357 // let's get the containing element of the selection
358 RefPtr
<Element
> selectionContainerElement
= GetSelectionContainerElement();
359 if (NS_WARN_IF(!selectionContainerElement
)) {
363 // If we're not in a document, don't try to add resizers
364 if (!selectionContainerElement
->IsInUncomposedDoc()) {
369 RefPtr
<Element
> focusElement
= std::move(selectionContainerElement
);
370 nsAtom
* focusTagAtom
= focusElement
->NodeInfo()->NameAtom();
372 RefPtr
<Element
> absPosElement
;
373 if (IsAbsolutePositionEditorEnabled()) {
374 // Absolute Positioning support is enabled, is the selection contained
375 // in an absolutely positioned element ?
376 absPosElement
= GetAbsolutelyPositionedSelectionContainer();
377 if (NS_WARN_IF(Destroyed())) {
378 return NS_ERROR_EDITOR_DESTROYED
;
382 RefPtr
<Element
> cellElement
;
383 if (IsObjectResizerEnabled() || IsInlineTableEditorEnabled()) {
384 // Resizing or Inline Table Editing is enabled, we need to check if the
385 // selection is contained in a table cell
386 cellElement
= GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td
);
389 if (IsObjectResizerEnabled() && cellElement
) {
390 // we are here because Resizing is enabled AND selection is contained in
393 // get the enclosing table
394 if (nsGkAtoms::img
!= focusTagAtom
) {
395 // the element container of the selection is not an image, so we'll show
396 // the resizers around the table
397 // XXX There may be a bug. cellElement may be not in <table> in invalid
398 // tree. So, perhaps, GetClosestAncestorTableElement() returns
399 // nullptr, we should not set focusTagAtom to nsGkAtoms::table.
401 HTMLEditUtils::GetClosestAncestorTableElement(*cellElement
);
402 focusTagAtom
= nsGkAtoms::table
;
406 // we allow resizers only around images, tables, and absolutely positioned
407 // elements. If we don't have image/table, let's look at the latter case.
408 if (nsGkAtoms::img
!= focusTagAtom
&& nsGkAtoms::table
!= focusTagAtom
) {
409 focusElement
= absPosElement
;
412 // at this point, focusElement contains the element for Resizing,
413 // cellElement contains the element for InlineTableEditing
414 // absPosElement contains the element for Positioning
416 // Note: All the Hide/Show methods below may change attributes on real
417 // content which means a DOMAttrModified handler may cause arbitrary
418 // side effects while this code runs (bug 420439).
420 if (IsAbsolutePositionEditorEnabled() && mAbsolutelyPositionedObject
&&
421 absPosElement
!= mAbsolutelyPositionedObject
) {
422 HideGrabberInternal();
423 NS_ASSERTION(!mAbsolutelyPositionedObject
,
424 "HTMLEditor::HideGrabberInternal() failed, but ignored");
427 if (IsObjectResizerEnabled() && mResizedObject
&&
428 mResizedObject
!= focusElement
) {
429 // Perhaps, even if HideResizersInternal() failed, we should try to hide
430 // inline table editing UI. However, it returns error only when we cannot
431 // do anything. So, it's okay for now.
432 nsresult rv
= HideResizersInternal();
434 NS_WARNING("HTMLEditor::HideResizersInternal() failed");
437 NS_ASSERTION(!mResizedObject
,
438 "HTMLEditor::HideResizersInternal() failed, but ignored");
441 if (IsInlineTableEditorEnabled() && mInlineEditedCell
&&
442 mInlineEditedCell
!= cellElement
) {
443 HideInlineTableEditingUIInternal();
446 "HTMLEditor::HideInlineTableEditingUIInternal failed, but ignored");
449 // now, let's display all contextual UI for good
450 nsIContent
* hostContent
= ComputeEditingHost();
452 if (IsObjectResizerEnabled() && focusElement
&&
453 HTMLEditUtils::IsSimplyEditableNode(*focusElement
) &&
454 focusElement
!= hostContent
) {
455 if (nsGkAtoms::img
== focusTagAtom
) {
456 mResizedObjectIsAnImage
= true;
458 if (mResizedObject
) {
459 nsresult rv
= RefreshResizersInternal();
461 NS_WARNING("HTMLEditor::RefreshResizersInternal() failed");
465 nsresult rv
= ShowResizersInternal(*focusElement
);
467 NS_WARNING("HTMLEditor::ShowResizersInternal() failed");
473 if (IsAbsolutePositionEditorEnabled() && absPosElement
&&
474 HTMLEditUtils::IsSimplyEditableNode(*absPosElement
) &&
475 absPosElement
!= hostContent
) {
476 if (mAbsolutelyPositionedObject
) {
477 nsresult rv
= RefreshGrabberInternal();
479 NS_WARNING("HTMLEditor::RefreshGrabberInternal() failed");
483 nsresult rv
= ShowGrabberInternal(*absPosElement
);
485 NS_WARNING("HTMLEditor::ShowGrabberInternal() failed");
491 // XXX Shouldn't we check whether the `<table>` element is editable or not?
492 if (IsInlineTableEditorEnabled() && cellElement
&&
493 HTMLEditUtils::IsSimplyEditableNode(*cellElement
) &&
494 cellElement
!= hostContent
) {
495 if (mInlineEditedCell
) {
496 nsresult rv
= RefreshInlineTableEditingUIInternal();
498 NS_WARNING("HTMLEditor::RefreshInlineTableEditingUIInternal() failed");
502 nsresult rv
= ShowInlineTableEditingUIInternal(*cellElement
);
504 NS_WARNING("HTMLEditor::ShowInlineTableEditingUIInternal() failed");
513 // Resizing and Absolute Positioning need to know everything about the
514 // containing box of the element: position, size, margins, borders
515 nsresult
HTMLEditor::GetPositionAndDimensions(Element
& aElement
, int32_t& aX
,
516 int32_t& aY
, int32_t& aW
,
517 int32_t& aH
, int32_t& aBorderLeft
,
519 int32_t& aMarginLeft
,
520 int32_t& aMarginTop
) {
521 // Is the element positioned ? let's check the cheap way first...
523 aElement
.HasAttr(kNameSpaceID_None
, nsGkAtoms::_moz_abspos
);
525 // hmmm... the expensive way now...
526 nsAutoString positionValue
;
527 DebugOnly
<nsresult
> rvIgnored
= CSSEditUtils::GetComputedProperty(
528 aElement
, *nsGkAtoms::position
, positionValue
);
529 if (NS_WARN_IF(Destroyed())) {
530 return NS_ERROR_EDITOR_DESTROYED
;
532 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored
),
533 "CSSEditUtils::GetComputedProperty(nsGkAtoms::"
534 "position) failed, but ignored");
535 isPositioned
= positionValue
.EqualsLiteral("absolute");
539 // Yes, it is absolutely positioned
540 mResizedObjectIsAbsolutelyPositioned
= true;
542 // Get the all the computed css styles attached to the element node
543 RefPtr
<nsComputedDOMStyle
> computedDOMStyle
=
544 CSSEditUtils::GetComputedStyle(&aElement
);
545 if (NS_WARN_IF(!computedDOMStyle
)) {
546 return NS_ERROR_FAILURE
;
549 aBorderLeft
= GetCSSFloatValue(computedDOMStyle
, "border-left-width"_ns
);
550 aBorderTop
= GetCSSFloatValue(computedDOMStyle
, "border-top-width"_ns
);
551 aMarginLeft
= GetCSSFloatValue(computedDOMStyle
, "margin-left"_ns
);
552 aMarginTop
= GetCSSFloatValue(computedDOMStyle
, "margin-top"_ns
);
554 aX
= GetCSSFloatValue(computedDOMStyle
, "left"_ns
) + aMarginLeft
+
556 aY
= GetCSSFloatValue(computedDOMStyle
, "top"_ns
) + aMarginTop
+ aBorderTop
;
557 aW
= GetCSSFloatValue(computedDOMStyle
, "width"_ns
);
558 aH
= GetCSSFloatValue(computedDOMStyle
, "height"_ns
);
560 mResizedObjectIsAbsolutelyPositioned
= false;
561 RefPtr
<nsGenericHTMLElement
> htmlElement
=
562 nsGenericHTMLElement::FromNode(aElement
);
564 return NS_ERROR_NULL_POINTER
;
566 DebugOnly
<nsresult
> rvIgnored
= GetElementOrigin(aElement
, aX
, aY
);
567 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored
),
568 "HTMLEditor::GetElementOrigin() failed, but ignored");
570 aW
= htmlElement
->OffsetWidth();
571 aH
= htmlElement
->OffsetHeight();
581 nsresult
HTMLEditor::SetAnonymousElementPositionWithoutTransaction(
582 nsStyledElement
& aStyledElement
, int32_t aX
, int32_t aY
) {
584 rv
= CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
585 aStyledElement
, *nsGkAtoms::left
, aX
);
586 if (rv
== NS_ERROR_EDITOR_DESTROYED
) {
588 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::left) "
589 "destroyed the editor");
590 return NS_ERROR_EDITOR_DESTROYED
;
592 NS_WARNING_ASSERTION(
594 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::left) "
595 "failed, but ignored");
596 rv
= CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
597 aStyledElement
, *nsGkAtoms::top
, aY
);
598 if (rv
== NS_ERROR_EDITOR_DESTROYED
) {
600 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::top) "
601 "destroyed the editor");
602 return NS_ERROR_EDITOR_DESTROYED
;
604 NS_WARNING_ASSERTION(
606 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::top) "
607 "failed, but ignored");
611 } // namespace mozilla