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 "HTMLEditUtils.h"
8 #include "mozilla/Attributes.h"
9 #include "mozilla/PresShell.h"
10 #include "mozilla/PresShellInlines.h"
11 #include "mozilla/dom/BindContext.h"
12 #include "mozilla/dom/Element.h"
13 #include "mozilla/dom/EventTarget.h"
14 #include "mozilla/mozalloc.h"
15 #include "nsAString.h"
17 #include "nsComputedDOMStyle.h"
20 #include "nsGenericHTMLElement.h"
21 #include "nsGkAtoms.h"
23 #include "nsIContent.h"
25 #include "mozilla/dom/Document.h"
26 #include "nsIDocumentObserver.h"
27 #include "nsStubMutationObserver.h"
29 #include "nsISupportsImpl.h"
30 #include "nsISupportsUtils.h"
31 #include "nsLiteralString.h"
32 #include "nsPresContext.h"
33 #include "nsReadableUtils.h"
35 #include "nsStringFwd.h"
36 #include "nsStyledElement.h"
37 #include "nsUnicharUtils.h"
39 #include "nsContentUtils.h" // for nsAutoScriptBlocker
40 #include "nsROCSSPrimitiveValue.h"
42 class nsIDOMEventListener
;
48 // Retrieve the rounded number of CSS pixels from a computed CSS property.
50 // Note that this should only be called for properties whose resolved value
51 // is CSS pixels (like width, height, left, top, right, bottom, margin, padding,
52 // border-*-width, ...).
54 // See: https://drafts.csswg.org/cssom/#resolved-values
55 static int32_t GetCSSFloatValue(nsComputedDOMStyle
* aComputedStyle
,
56 const nsACString
& aProperty
) {
57 MOZ_ASSERT(aComputedStyle
);
59 // get the computed CSSValue of the property
61 nsresult rv
= aComputedStyle
->GetPropertyValue(aProperty
, value
);
63 NS_WARNING("nsComputedDOMStyle::GetPropertyValue() failed");
67 // We only care about resolved values, not a big deal if the element is
68 // undisplayed, for example, and the value is "auto" or what not.
69 int32_t val
= value
.ToInteger(&rv
);
70 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
), "nsAString::ToInteger() failed");
71 return NS_SUCCEEDED(rv
) ? val
: 0;
74 class ElementDeletionObserver final
: public nsStubMutationObserver
{
76 ElementDeletionObserver(nsIContent
* aNativeAnonNode
,
77 Element
* aObservedElement
)
78 : mNativeAnonNode(aNativeAnonNode
), mObservedElement(aObservedElement
) {}
81 NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED
82 NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
85 ~ElementDeletionObserver() = default;
86 nsIContent
* mNativeAnonNode
;
87 Element
* mObservedElement
;
90 NS_IMPL_ISUPPORTS(ElementDeletionObserver
, nsIMutationObserver
)
92 void ElementDeletionObserver::ParentChainChanged(nsIContent
* aContent
) {
93 // If the native anonymous content has been unbound already in
94 // DeleteRefToAnonymousNode, mNativeAnonNode's parentNode is null.
95 if (aContent
!= mObservedElement
|| !mNativeAnonNode
||
96 mNativeAnonNode
->GetParent() != aContent
) {
100 ManualNACPtr::RemoveContentFromNACArray(mNativeAnonNode
);
102 mObservedElement
->RemoveMutationObserver(this);
103 mObservedElement
= nullptr;
104 mNativeAnonNode
->RemoveMutationObserver(this);
105 mNativeAnonNode
= nullptr;
109 void ElementDeletionObserver::NodeWillBeDestroyed(const nsINode
* aNode
) {
110 NS_ASSERTION(aNode
== mNativeAnonNode
|| aNode
== mObservedElement
,
112 if (aNode
== mNativeAnonNode
) {
113 mObservedElement
->RemoveMutationObserver(this);
114 mObservedElement
= nullptr;
116 mNativeAnonNode
->RemoveMutationObserver(this);
117 mNativeAnonNode
->UnbindFromTree();
118 mNativeAnonNode
= nullptr;
124 ManualNACPtr
HTMLEditor::CreateAnonymousElement(nsAtom
* aTag
,
125 nsIContent
& aParentContent
,
126 const nsAString
& aAnonClass
,
127 bool aIsCreatedHidden
) {
128 // Don't put anonymous editor element into non-HTML element.
129 // It is mainly for avoiding other anonymous element being inserted
130 // into <svg:use>, but in general we probably don't want to insert
131 // some random HTML anonymous element into a non-HTML element.
132 if (!aParentContent
.IsHTMLElement()) {
136 if (NS_WARN_IF(!GetDocument())) {
140 RefPtr
<PresShell
> presShell
= GetPresShell();
141 if (NS_WARN_IF(!presShell
)) {
145 // Create a new node through the element factory
146 RefPtr
<Element
> newElement
= CreateHTMLContent(aTag
);
148 NS_WARNING("EditorBase::CreateHTMLContent() failed");
152 // add the "hidden" class if needed
153 if (aIsCreatedHidden
) {
154 nsresult rv
= newElement
->SetAttr(kNameSpaceID_None
, nsGkAtoms::_class
,
157 NS_WARNING("Element::SetAttr(nsGkAtoms::_class, hidden) failed");
162 // add an _moz_anonclass attribute if needed
163 if (!aAnonClass
.IsEmpty()) {
164 nsresult rv
= newElement
->SetAttr(
165 kNameSpaceID_None
, nsGkAtoms::_moz_anonclass
, aAnonClass
, true);
167 NS_WARNING("Element::SetAttr(nsGkAtoms::_moz_anonclass) failed");
173 nsAutoScriptBlocker scriptBlocker
;
175 // establish parenthood of the element
176 newElement
->SetIsNativeAnonymousRoot();
177 BindContext
context(*aParentContent
.AsElement(),
178 BindContext::ForNativeAnonymous
);
179 nsresult rv
= newElement
->BindToTree(context
, aParentContent
);
181 NS_WARNING("Element::BindToTree(BindContext::ForNativeAnonymous) failed");
182 newElement
->UnbindFromTree();
187 ManualNACPtr
newNativeAnonymousContent(newElement
.forget());
189 // Must style the new element, otherwise the PostRecreateFramesFor call
190 // below will do nothing.
191 ServoStyleSet
* styleSet
= presShell
->StyleSet();
192 // Sometimes editor likes to append anonymous content to elements
193 // in display:none subtrees, so avoid styling in those cases.
194 if (ServoStyleSet::MayTraverseFrom(newNativeAnonymousContent
)) {
195 styleSet
->StyleNewSubtree(newNativeAnonymousContent
);
198 auto* observer
= new ElementDeletionObserver(newNativeAnonymousContent
,
199 aParentContent
.AsElement());
200 NS_ADDREF(observer
); // NodeWillBeDestroyed releases.
201 aParentContent
.AddMutationObserver(observer
);
202 newNativeAnonymousContent
->AddMutationObserver(observer
);
205 // Editor anonymous content gets passed to PostRecreateFramesFor... which
206 // can't _really_ deal with anonymous content (because it can't get the frame
207 // tree ordering right). But for us the ordering doesn't matter so this is
209 newNativeAnonymousContent
->SetProperty(nsGkAtoms::restylableAnonymousNode
,
210 reinterpret_cast<void*>(true));
213 // display the element
214 presShell
->PostRecreateFramesFor(newNativeAnonymousContent
);
216 return newNativeAnonymousContent
;
219 // Removes event listener and calls DeleteRefToAnonymousNode.
220 void HTMLEditor::RemoveListenerAndDeleteRef(const nsAString
& aEvent
,
221 nsIDOMEventListener
* aListener
,
223 ManualNACPtr aElement
,
224 PresShell
* aPresShell
) {
226 aElement
->RemoveEventListener(aEvent
, aListener
, aUseCapture
);
228 DeleteRefToAnonymousNode(std::move(aElement
), aPresShell
);
231 // Deletes all references to an anonymous element
232 void HTMLEditor::DeleteRefToAnonymousNode(ManualNACPtr aContent
,
233 PresShell
* aPresShell
) {
234 // call ContentRemoved() for the anonymous content
235 // node so its references get removed from the frame manager's
236 // undisplay map, and its layout frames get destroyed!
238 if (NS_WARN_IF(!aContent
)) {
242 if (NS_WARN_IF(!aContent
->GetParent())) {
243 // aContent was already removed?
247 nsAutoScriptBlocker scriptBlocker
;
248 // Need to check whether aPresShell has been destroyed (but not yet deleted).
250 if (aContent
->IsInComposedDoc() && aPresShell
&&
251 !aPresShell
->IsDestroying()) {
252 MOZ_ASSERT(aContent
->IsRootOfNativeAnonymousSubtree());
253 MOZ_ASSERT(!aContent
->GetPreviousSibling(), "NAC has no siblings");
255 // FIXME(emilio): This is the only caller to PresShell::ContentRemoved that
256 // passes NAC into it. This is not great!
257 aPresShell
->ContentRemoved(aContent
, nullptr);
260 // The ManualNACPtr destructor will invoke UnbindFromTree.
263 void HTMLEditor::HideAnonymousEditingUIs() {
264 if (mAbsolutelyPositionedObject
) {
265 HideGrabberInternal();
266 NS_ASSERTION(!mAbsolutelyPositionedObject
,
267 "HTMLEditor::HideGrabberInternal() failed, but ignored");
269 if (mInlineEditedCell
) {
270 HideInlineTableEditingUIInternal();
273 "HTMLEditor::HideInlineTableEditingUIInternal() failed, but ignored");
275 if (mResizedObject
) {
276 DebugOnly
<nsresult
> rvIgnored
= HideResizersInternal();
277 NS_WARNING_ASSERTION(
278 NS_SUCCEEDED(rvIgnored
),
279 "HTMLEditor::HideResizersInternal() failed, but ignored");
280 NS_ASSERTION(!mResizedObject
,
281 "HTMLEditor::HideResizersInternal() failed, but ignored");
285 void HTMLEditor::HideAnonymousEditingUIsIfUnnecessary() {
286 // XXX Perhaps, this is wrong approach to hide multiple UIs because
287 // hiding one UI may causes overwriting existing UI with newly
288 // created one. In such case, we will leak ovewritten UI.
289 if (!IsAbsolutePositionEditorEnabled() && mAbsolutelyPositionedObject
) {
290 // XXX If we're moving something, we need to cancel or commit the
292 HideGrabberInternal();
293 NS_ASSERTION(!mAbsolutelyPositionedObject
,
294 "HTMLEditor::HideGrabberInternal() failed, but ignored");
296 if (!IsInlineTableEditorEnabled() && mInlineEditedCell
) {
297 // XXX If we're resizing a table element, we need to cancel or commit the
299 HideInlineTableEditingUIInternal();
302 "HTMLEditor::HideInlineTableEditingUIInternal() failed, but ignored");
304 if (!IsObjectResizerEnabled() && mResizedObject
) {
305 // XXX If we're resizing something, we need to cancel or commit the
307 DebugOnly
<nsresult
> rvIgnored
= HideResizersInternal();
308 NS_WARNING_ASSERTION(
309 NS_SUCCEEDED(rvIgnored
),
310 "HTMLEditor::HideResizersInternal() failed, but ignored");
311 NS_ASSERTION(!mResizedObject
,
312 "HTMLEditor::HideResizersInternal() failed, but ignored");
316 NS_IMETHODIMP
HTMLEditor::CheckSelectionStateForAnonymousButtons() {
317 AutoEditActionDataSetter
editActionData(*this, EditAction::eNotEditing
);
318 if (NS_WARN_IF(!editActionData
.CanHandle())) {
319 return NS_ERROR_NOT_INITIALIZED
;
322 nsresult rv
= RefreshEditingUI();
323 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
),
324 "HTMLEditor::RefereshEditingUI() failed");
325 return EditorBase::ToGenericNSResult(rv
);
328 nsresult
HTMLEditor::RefreshEditingUI() {
329 MOZ_ASSERT(IsEditActionDataAvailable());
331 // First, we need to remove unnecessary editing UI now since some of them
332 // may be disabled while them are visible.
333 HideAnonymousEditingUIsIfUnnecessary();
335 // early way out if all contextual UI extensions are disabled
336 if (!IsObjectResizerEnabled() && !IsAbsolutePositionEditorEnabled() &&
337 !IsInlineTableEditorEnabled()) {
341 // Don't change selection state if we're moving.
346 // let's get the containing element of the selection
347 RefPtr
<Element
> selectionContainerElement
= GetSelectionContainerElement();
348 if (NS_WARN_IF(!selectionContainerElement
)) {
352 // If we're not in a document, don't try to add resizers
353 if (!selectionContainerElement
->IsInUncomposedDoc()) {
358 RefPtr
<Element
> focusElement
= std::move(selectionContainerElement
);
359 nsAtom
* focusTagAtom
= focusElement
->NodeInfo()->NameAtom();
361 RefPtr
<Element
> absPosElement
;
362 if (IsAbsolutePositionEditorEnabled()) {
363 // Absolute Positioning support is enabled, is the selection contained
364 // in an absolutely positioned element ?
365 absPosElement
= GetAbsolutelyPositionedSelectionContainer();
366 if (NS_WARN_IF(Destroyed())) {
367 return NS_ERROR_EDITOR_DESTROYED
;
371 RefPtr
<Element
> cellElement
;
372 if (IsObjectResizerEnabled() || IsInlineTableEditorEnabled()) {
373 // Resizing or Inline Table Editing is enabled, we need to check if the
374 // selection is contained in a table cell
375 cellElement
= GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td
);
378 if (IsObjectResizerEnabled() && cellElement
) {
379 // we are here because Resizing is enabled AND selection is contained in
382 // get the enclosing table
383 if (nsGkAtoms::img
!= focusTagAtom
) {
384 // the element container of the selection is not an image, so we'll show
385 // the resizers around the table
386 // XXX There may be a bug. cellElement may be not in <table> in invalid
387 // tree. So, perhaps, GetClosestAncestorTableElement() returns
388 // nullptr, we should not set focusTagAtom to nsGkAtoms::table.
390 HTMLEditUtils::GetClosestAncestorTableElement(*cellElement
);
391 focusTagAtom
= nsGkAtoms::table
;
395 // we allow resizers only around images, tables, and absolutely positioned
396 // elements. If we don't have image/table, let's look at the latter case.
397 if (nsGkAtoms::img
!= focusTagAtom
&& nsGkAtoms::table
!= focusTagAtom
) {
398 focusElement
= absPosElement
;
401 // at this point, focusElement contains the element for Resizing,
402 // cellElement contains the element for InlineTableEditing
403 // absPosElement contains the element for Positioning
405 // Note: All the Hide/Show methods below may change attributes on real
406 // content which means a DOMAttrModified handler may cause arbitrary
407 // side effects while this code runs (bug 420439).
409 if (IsAbsolutePositionEditorEnabled() && mAbsolutelyPositionedObject
&&
410 absPosElement
!= mAbsolutelyPositionedObject
) {
411 HideGrabberInternal();
412 NS_ASSERTION(!mAbsolutelyPositionedObject
,
413 "HTMLEditor::HideGrabberInternal() failed, but ignored");
416 if (IsObjectResizerEnabled() && mResizedObject
&&
417 mResizedObject
!= focusElement
) {
418 // Perhaps, even if HideResizersInternal() failed, we should try to hide
419 // inline table editing UI. However, it returns error only when we cannot
420 // do anything. So, it's okay for now.
421 nsresult rv
= HideResizersInternal();
423 NS_WARNING("HTMLEditor::HideResizersInternal() failed");
426 NS_ASSERTION(!mResizedObject
,
427 "HTMLEditor::HideResizersInternal() failed, but ignored");
430 if (IsInlineTableEditorEnabled() && mInlineEditedCell
&&
431 mInlineEditedCell
!= cellElement
) {
432 HideInlineTableEditingUIInternal();
435 "HTMLEditor::HideInlineTableEditingUIInternal failed, but ignored");
438 // now, let's display all contextual UI for good
439 nsIContent
* hostContent
= GetActiveEditingHost();
441 if (IsObjectResizerEnabled() && focusElement
&&
442 HTMLEditUtils::IsSimplyEditableNode(*focusElement
) &&
443 focusElement
!= hostContent
) {
444 if (nsGkAtoms::img
== focusTagAtom
) {
445 mResizedObjectIsAnImage
= true;
447 if (mResizedObject
) {
448 nsresult rv
= RefreshResizersInternal();
450 NS_WARNING("HTMLEditor::RefreshResizersInternal() failed");
454 nsresult rv
= ShowResizersInternal(*focusElement
);
456 NS_WARNING("HTMLEditor::ShowResizersInternal() failed");
462 if (IsAbsolutePositionEditorEnabled() && absPosElement
&&
463 HTMLEditUtils::IsSimplyEditableNode(*absPosElement
) &&
464 absPosElement
!= hostContent
) {
465 if (mAbsolutelyPositionedObject
) {
466 nsresult rv
= RefreshGrabberInternal();
468 NS_WARNING("HTMLEditor::RefreshGrabberInternal() failed");
472 nsresult rv
= ShowGrabberInternal(*absPosElement
);
474 NS_WARNING("HTMLEditor::ShowGrabberInternal() failed");
480 // XXX Shouldn't we check whether the `<table>` element is editable or not?
481 if (IsInlineTableEditorEnabled() && cellElement
&&
482 HTMLEditUtils::IsSimplyEditableNode(*cellElement
) &&
483 cellElement
!= hostContent
) {
484 if (mInlineEditedCell
) {
485 nsresult rv
= RefreshInlineTableEditingUIInternal();
487 NS_WARNING("HTMLEditor::RefreshInlineTableEditingUIInternal() failed");
491 nsresult rv
= ShowInlineTableEditingUIInternal(*cellElement
);
493 NS_WARNING("HTMLEditor::ShowInlineTableEditingUIInternal() failed");
502 // Resizing and Absolute Positioning need to know everything about the
503 // containing box of the element: position, size, margins, borders
504 nsresult
HTMLEditor::GetPositionAndDimensions(Element
& aElement
, int32_t& aX
,
505 int32_t& aY
, int32_t& aW
,
506 int32_t& aH
, int32_t& aBorderLeft
,
508 int32_t& aMarginLeft
,
509 int32_t& aMarginTop
) {
510 // Is the element positioned ? let's check the cheap way first...
512 aElement
.HasAttr(kNameSpaceID_None
, nsGkAtoms::_moz_abspos
);
514 // hmmm... the expensive way now...
515 nsAutoString positionValue
;
516 DebugOnly
<nsresult
> rvIgnored
= CSSEditUtils::GetComputedProperty(
517 aElement
, *nsGkAtoms::position
, positionValue
);
518 if (NS_WARN_IF(Destroyed())) {
519 return NS_ERROR_EDITOR_DESTROYED
;
521 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored
),
522 "CSSEditUtils::GetComputedProperty(nsGkAtoms::"
523 "position) failed, but ignored");
524 isPositioned
= positionValue
.EqualsLiteral("absolute");
528 // Yes, it is absolutely positioned
529 mResizedObjectIsAbsolutelyPositioned
= true;
531 // Get the all the computed css styles attached to the element node
532 RefPtr
<nsComputedDOMStyle
> computedDOMStyle
=
533 CSSEditUtils::GetComputedStyle(&aElement
);
534 if (NS_WARN_IF(!computedDOMStyle
)) {
535 return NS_ERROR_FAILURE
;
538 aBorderLeft
= GetCSSFloatValue(computedDOMStyle
, "border-left-width"_ns
);
539 aBorderTop
= GetCSSFloatValue(computedDOMStyle
, "border-top-width"_ns
);
540 aMarginLeft
= GetCSSFloatValue(computedDOMStyle
, "margin-left"_ns
);
541 aMarginTop
= GetCSSFloatValue(computedDOMStyle
, "margin-top"_ns
);
543 aX
= GetCSSFloatValue(computedDOMStyle
, "left"_ns
) + aMarginLeft
+
545 aY
= GetCSSFloatValue(computedDOMStyle
, "top"_ns
) + aMarginTop
+ aBorderTop
;
546 aW
= GetCSSFloatValue(computedDOMStyle
, "width"_ns
);
547 aH
= GetCSSFloatValue(computedDOMStyle
, "height"_ns
);
549 mResizedObjectIsAbsolutelyPositioned
= false;
550 RefPtr
<nsGenericHTMLElement
> htmlElement
=
551 nsGenericHTMLElement::FromNode(aElement
);
553 return NS_ERROR_NULL_POINTER
;
555 DebugOnly
<nsresult
> rvIgnored
= GetElementOrigin(aElement
, aX
, aY
);
556 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored
),
557 "HTMLEditor::GetElementOrigin() failed, but ignored");
559 aW
= htmlElement
->OffsetWidth();
560 aH
= htmlElement
->OffsetHeight();
570 nsresult
HTMLEditor::SetAnonymousElementPositionWithoutTransaction(
571 nsStyledElement
& aStyledElement
, int32_t aX
, int32_t aY
) {
573 rv
= mCSSEditUtils
->SetCSSPropertyPixelsWithoutTransaction(
574 aStyledElement
, *nsGkAtoms::left
, aX
);
575 if (rv
== NS_ERROR_EDITOR_DESTROYED
) {
577 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::left) "
578 "destroyed the editor");
579 return NS_ERROR_EDITOR_DESTROYED
;
581 NS_WARNING_ASSERTION(
583 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::left) "
584 "failed, but ignored");
585 rv
= mCSSEditUtils
->SetCSSPropertyPixelsWithoutTransaction(
586 aStyledElement
, *nsGkAtoms::top
, aY
);
587 if (rv
== NS_ERROR_EDITOR_DESTROYED
) {
589 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::top) "
590 "destroyed the editor");
591 return NS_ERROR_EDITOR_DESTROYED
;
593 NS_WARNING_ASSERTION(
595 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::top) "
596 "failed, but ignored");
600 } // namespace mozilla