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 aComputedStyle
->GetPropertyValue(aProperty
, value
);
64 // We only care about resolved values, not a big deal if the element is
65 // undisplayed, for example, and the value is "auto" or what not.
67 int32_t val
= value
.ToInteger(&rv
);
68 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
), "nsAString::ToInteger() failed");
69 return NS_SUCCEEDED(rv
) ? val
: 0;
72 /******************************************************************************
73 * mozilla::ElementDeletionObserver
74 *****************************************************************************/
76 class ElementDeletionObserver final
: public nsStubMultiMutationObserver
{
78 ElementDeletionObserver(nsIContent
* aNativeAnonNode
,
79 Element
* aObservedElement
)
80 : mNativeAnonNode(aNativeAnonNode
), mObservedElement(aObservedElement
) {
81 AddMutationObserverToNode(aNativeAnonNode
);
82 AddMutationObserverToNode(aObservedElement
);
86 NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED
87 NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
90 ~ElementDeletionObserver() = default;
91 nsIContent
* mNativeAnonNode
;
92 Element
* mObservedElement
;
95 NS_IMPL_ISUPPORTS(ElementDeletionObserver
, nsIMutationObserver
)
97 void ElementDeletionObserver::ParentChainChanged(nsIContent
* aContent
) {
98 // If the native anonymous content has been unbound already in
99 // DeleteRefToAnonymousNode, mNativeAnonNode's parentNode is null.
100 if (aContent
!= mObservedElement
|| !mNativeAnonNode
||
101 mNativeAnonNode
->GetParent() != aContent
) {
105 ManualNACPtr::RemoveContentFromNACArray(mNativeAnonNode
);
107 mObservedElement
->RemoveMutationObserver(this);
108 mObservedElement
= nullptr;
109 mNativeAnonNode
->RemoveMutationObserver(this);
110 mNativeAnonNode
= nullptr;
114 void ElementDeletionObserver::NodeWillBeDestroyed(nsINode
* aNode
) {
115 NS_ASSERTION(aNode
== mNativeAnonNode
|| aNode
== mObservedElement
,
117 if (aNode
== mNativeAnonNode
) {
118 mObservedElement
->RemoveMutationObserver(this);
119 mObservedElement
= nullptr;
121 mNativeAnonNode
->RemoveMutationObserver(this);
122 mNativeAnonNode
->UnbindFromTree();
123 mNativeAnonNode
= nullptr;
129 /******************************************************************************
130 * mozilla::HTMLEditor
131 *****************************************************************************/
133 ManualNACPtr
HTMLEditor::CreateAnonymousElement(nsAtom
* aTag
,
134 nsIContent
& aParentContent
,
135 const nsAString
& aAnonClass
,
136 bool aIsCreatedHidden
) {
137 // Don't put anonymous editor element into non-HTML element.
138 // It is mainly for avoiding other anonymous element being inserted
139 // into <svg:use>, but in general we probably don't want to insert
140 // some random HTML anonymous element into a non-HTML element.
141 if (!aParentContent
.IsHTMLElement()) {
145 if (NS_WARN_IF(!GetDocument())) {
149 RefPtr
<PresShell
> presShell
= GetPresShell();
150 if (NS_WARN_IF(!presShell
)) {
154 // Create a new node through the element factory
155 RefPtr
<Element
> newElement
= CreateHTMLContent(aTag
);
157 NS_WARNING("EditorBase::CreateHTMLContent() failed");
161 // add the "hidden" class if needed
162 if (aIsCreatedHidden
) {
163 nsresult rv
= newElement
->SetAttr(kNameSpaceID_None
, nsGkAtoms::_class
,
166 NS_WARNING("Element::SetAttr(nsGkAtoms::_class, hidden) failed");
171 // add an _moz_anonclass attribute if needed
172 if (!aAnonClass
.IsEmpty()) {
173 nsresult rv
= newElement
->SetAttr(
174 kNameSpaceID_None
, nsGkAtoms::_moz_anonclass
, aAnonClass
, true);
176 NS_WARNING("Element::SetAttr(nsGkAtoms::_moz_anonclass) failed");
182 nsAutoScriptBlocker scriptBlocker
;
184 // establish parenthood of the element
185 newElement
->SetIsNativeAnonymousRoot();
186 BindContext
context(*aParentContent
.AsElement(),
187 BindContext::ForNativeAnonymous
);
188 nsresult rv
= newElement
->BindToTree(context
, aParentContent
);
190 NS_WARNING("Element::BindToTree(BindContext::ForNativeAnonymous) failed");
191 newElement
->UnbindFromTree();
196 ManualNACPtr
newNativeAnonymousContent(newElement
.forget());
198 // Must style the new element, otherwise the PostRecreateFramesFor call
199 // below will do nothing.
200 ServoStyleSet
* styleSet
= presShell
->StyleSet();
201 // Sometimes editor likes to append anonymous content to elements
202 // in display:none subtrees, so avoid styling in those cases.
203 if (ServoStyleSet::MayTraverseFrom(newNativeAnonymousContent
)) {
204 styleSet
->StyleNewSubtree(newNativeAnonymousContent
);
207 auto* observer
= new ElementDeletionObserver(newNativeAnonymousContent
,
208 aParentContent
.AsElement());
209 NS_ADDREF(observer
); // NodeWillBeDestroyed releases.
212 // Editor anonymous content gets passed to PostRecreateFramesFor... which
213 // can't _really_ deal with anonymous content (because it can't get the frame
214 // tree ordering right). But for us the ordering doesn't matter so this is
216 newNativeAnonymousContent
->SetProperty(nsGkAtoms::restylableAnonymousNode
,
217 reinterpret_cast<void*>(true));
220 // display the element
221 presShell
->PostRecreateFramesFor(newNativeAnonymousContent
);
223 return newNativeAnonymousContent
;
226 // Removes event listener and calls DeleteRefToAnonymousNode.
227 void HTMLEditor::RemoveListenerAndDeleteRef(const nsAString
& aEvent
,
228 nsIDOMEventListener
* aListener
,
230 ManualNACPtr aElement
,
231 PresShell
* aPresShell
) {
233 aElement
->RemoveEventListener(aEvent
, aListener
, aUseCapture
);
235 DeleteRefToAnonymousNode(std::move(aElement
), aPresShell
);
238 // Deletes all references to an anonymous element
239 void HTMLEditor::DeleteRefToAnonymousNode(ManualNACPtr aContent
,
240 PresShell
* aPresShell
) {
241 // call ContentRemoved() for the anonymous content
242 // node so its references get removed from the frame manager's
243 // undisplay map, and its layout frames get destroyed!
245 if (NS_WARN_IF(!aContent
)) {
249 if (NS_WARN_IF(!aContent
->GetParent())) {
250 // aContent was already removed?
254 nsAutoScriptBlocker scriptBlocker
;
255 // Need to check whether aPresShell has been destroyed (but not yet deleted).
257 if (aContent
->IsInComposedDoc() && aPresShell
&&
258 !aPresShell
->IsDestroying()) {
259 MOZ_ASSERT(aContent
->IsRootOfNativeAnonymousSubtree());
260 MOZ_ASSERT(!aContent
->GetPreviousSibling(), "NAC has no siblings");
262 // FIXME(emilio): This is the only caller to PresShell::ContentRemoved that
263 // passes NAC into it. This is not great!
264 aPresShell
->ContentRemoved(aContent
, nullptr);
267 // The ManualNACPtr destructor will invoke UnbindFromTree.
270 void HTMLEditor::HideAnonymousEditingUIs() {
271 if (mAbsolutelyPositionedObject
) {
272 HideGrabberInternal();
273 NS_ASSERTION(!mAbsolutelyPositionedObject
,
274 "HTMLEditor::HideGrabberInternal() failed, but ignored");
276 if (mInlineEditedCell
) {
277 HideInlineTableEditingUIInternal();
280 "HTMLEditor::HideInlineTableEditingUIInternal() failed, but ignored");
282 if (mResizedObject
) {
283 DebugOnly
<nsresult
> rvIgnored
= HideResizersInternal();
284 NS_WARNING_ASSERTION(
285 NS_SUCCEEDED(rvIgnored
),
286 "HTMLEditor::HideResizersInternal() failed, but ignored");
287 NS_ASSERTION(!mResizedObject
,
288 "HTMLEditor::HideResizersInternal() failed, but ignored");
292 void HTMLEditor::HideAnonymousEditingUIsIfUnnecessary() {
293 // XXX Perhaps, this is wrong approach to hide multiple UIs because
294 // hiding one UI may causes overwriting existing UI with newly
295 // created one. In such case, we will leak ovewritten UI.
296 if (!IsAbsolutePositionEditorEnabled() && mAbsolutelyPositionedObject
) {
297 // XXX If we're moving something, we need to cancel or commit the
299 HideGrabberInternal();
300 NS_ASSERTION(!mAbsolutelyPositionedObject
,
301 "HTMLEditor::HideGrabberInternal() failed, but ignored");
303 if (!IsInlineTableEditorEnabled() && mInlineEditedCell
) {
304 // XXX If we're resizing a table element, we need to cancel or commit the
306 HideInlineTableEditingUIInternal();
309 "HTMLEditor::HideInlineTableEditingUIInternal() failed, but ignored");
311 if (!IsObjectResizerEnabled() && mResizedObject
) {
312 // XXX If we're resizing something, we need to cancel or commit the
314 DebugOnly
<nsresult
> rvIgnored
= HideResizersInternal();
315 NS_WARNING_ASSERTION(
316 NS_SUCCEEDED(rvIgnored
),
317 "HTMLEditor::HideResizersInternal() failed, but ignored");
318 NS_ASSERTION(!mResizedObject
,
319 "HTMLEditor::HideResizersInternal() failed, but ignored");
323 NS_IMETHODIMP
HTMLEditor::CheckSelectionStateForAnonymousButtons() {
324 AutoEditActionDataSetter
editActionData(*this, EditAction::eNotEditing
);
325 if (NS_WARN_IF(!editActionData
.CanHandle())) {
326 return NS_ERROR_NOT_INITIALIZED
;
329 nsresult rv
= RefreshEditingUI();
330 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
),
331 "HTMLEditor::RefereshEditingUI() failed");
332 return EditorBase::ToGenericNSResult(rv
);
335 nsresult
HTMLEditor::RefreshEditingUI() {
336 MOZ_ASSERT(IsEditActionDataAvailable());
338 // First, we need to remove unnecessary editing UI now since some of them
339 // may be disabled while them are visible.
340 HideAnonymousEditingUIsIfUnnecessary();
342 // early way out if all contextual UI extensions are disabled
343 if (!IsObjectResizerEnabled() && !IsAbsolutePositionEditorEnabled() &&
344 !IsInlineTableEditorEnabled()) {
348 // Don't change selection state if we're moving.
353 // let's get the containing element of the selection
354 RefPtr
<Element
> selectionContainerElement
= GetSelectionContainerElement();
355 if (NS_WARN_IF(!selectionContainerElement
)) {
359 // If we're not in a document, don't try to add resizers
360 if (!selectionContainerElement
->IsInComposedDoc()) {
365 RefPtr
<Element
> focusElement
= std::move(selectionContainerElement
);
366 nsAtom
* focusTagAtom
= focusElement
->NodeInfo()->NameAtom();
368 RefPtr
<Element
> absPosElement
;
369 if (IsAbsolutePositionEditorEnabled()) {
370 // Absolute Positioning support is enabled, is the selection contained
371 // in an absolutely positioned element ?
372 absPosElement
= GetAbsolutelyPositionedSelectionContainer();
373 if (NS_WARN_IF(Destroyed())) {
374 return NS_ERROR_EDITOR_DESTROYED
;
378 RefPtr
<Element
> cellElement
;
379 if (IsObjectResizerEnabled() || IsInlineTableEditorEnabled()) {
380 // Resizing or Inline Table Editing is enabled, we need to check if the
381 // selection is contained in a table cell
382 cellElement
= GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td
);
385 if (IsObjectResizerEnabled() && cellElement
) {
386 // we are here because Resizing is enabled AND selection is contained in
389 // get the enclosing table
390 if (nsGkAtoms::img
!= focusTagAtom
) {
391 // the element container of the selection is not an image, so we'll show
392 // the resizers around the table
393 // XXX There may be a bug. cellElement may be not in <table> in invalid
394 // tree. So, perhaps, GetClosestAncestorTableElement() returns
395 // nullptr, we should not set focusTagAtom to nsGkAtoms::table.
397 HTMLEditUtils::GetClosestAncestorTableElement(*cellElement
);
398 focusTagAtom
= nsGkAtoms::table
;
402 // we allow resizers only around images, tables, and absolutely positioned
403 // elements. If we don't have image/table, let's look at the latter case.
404 if (nsGkAtoms::img
!= focusTagAtom
&& nsGkAtoms::table
!= focusTagAtom
) {
405 focusElement
= absPosElement
;
408 // at this point, focusElement contains the element for Resizing,
409 // cellElement contains the element for InlineTableEditing
410 // absPosElement contains the element for Positioning
412 // Note: All the Hide/Show methods below may change attributes on real
413 // content which means a DOMAttrModified handler may cause arbitrary
414 // side effects while this code runs (bug 420439).
416 if (IsAbsolutePositionEditorEnabled() && mAbsolutelyPositionedObject
&&
417 absPosElement
!= mAbsolutelyPositionedObject
) {
418 HideGrabberInternal();
419 NS_ASSERTION(!mAbsolutelyPositionedObject
,
420 "HTMLEditor::HideGrabberInternal() failed, but ignored");
423 if (IsObjectResizerEnabled() && mResizedObject
&&
424 mResizedObject
!= focusElement
) {
425 // Perhaps, even if HideResizersInternal() failed, we should try to hide
426 // inline table editing UI. However, it returns error only when we cannot
427 // do anything. So, it's okay for now.
428 nsresult rv
= HideResizersInternal();
430 NS_WARNING("HTMLEditor::HideResizersInternal() failed");
433 NS_ASSERTION(!mResizedObject
,
434 "HTMLEditor::HideResizersInternal() failed, but ignored");
437 if (IsInlineTableEditorEnabled() && mInlineEditedCell
&&
438 mInlineEditedCell
!= cellElement
) {
439 HideInlineTableEditingUIInternal();
442 "HTMLEditor::HideInlineTableEditingUIInternal failed, but ignored");
445 // now, let's display all contextual UI for good
446 nsIContent
* hostContent
= ComputeEditingHost();
448 if (IsObjectResizerEnabled() && focusElement
&&
449 HTMLEditUtils::IsSimplyEditableNode(*focusElement
) &&
450 focusElement
!= hostContent
) {
451 if (nsGkAtoms::img
== focusTagAtom
) {
452 mResizedObjectIsAnImage
= true;
454 if (mResizedObject
) {
455 nsresult rv
= RefreshResizersInternal();
457 NS_WARNING("HTMLEditor::RefreshResizersInternal() failed");
461 nsresult rv
= ShowResizersInternal(*focusElement
);
463 NS_WARNING("HTMLEditor::ShowResizersInternal() failed");
469 if (IsAbsolutePositionEditorEnabled() && absPosElement
&&
470 HTMLEditUtils::IsSimplyEditableNode(*absPosElement
) &&
471 absPosElement
!= hostContent
) {
472 if (mAbsolutelyPositionedObject
) {
473 nsresult rv
= RefreshGrabberInternal();
475 NS_WARNING("HTMLEditor::RefreshGrabberInternal() failed");
479 nsresult rv
= ShowGrabberInternal(*absPosElement
);
481 NS_WARNING("HTMLEditor::ShowGrabberInternal() failed");
487 // XXX Shouldn't we check whether the `<table>` element is editable or not?
488 if (IsInlineTableEditorEnabled() && cellElement
&&
489 HTMLEditUtils::IsSimplyEditableNode(*cellElement
) &&
490 cellElement
!= hostContent
) {
491 if (mInlineEditedCell
) {
492 nsresult rv
= RefreshInlineTableEditingUIInternal();
494 NS_WARNING("HTMLEditor::RefreshInlineTableEditingUIInternal() failed");
498 nsresult rv
= ShowInlineTableEditingUIInternal(*cellElement
);
500 NS_WARNING("HTMLEditor::ShowInlineTableEditingUIInternal() failed");
509 // Resizing and Absolute Positioning need to know everything about the
510 // containing box of the element: position, size, margins, borders
511 nsresult
HTMLEditor::GetPositionAndDimensions(Element
& aElement
, int32_t& aX
,
512 int32_t& aY
, int32_t& aW
,
513 int32_t& aH
, int32_t& aBorderLeft
,
515 int32_t& aMarginLeft
,
516 int32_t& aMarginTop
) {
517 // Is the element positioned ? let's check the cheap way first...
518 bool isPositioned
= aElement
.HasAttr(nsGkAtoms::_moz_abspos
);
520 // hmmm... the expensive way now...
521 nsAutoString positionValue
;
522 DebugOnly
<nsresult
> rvIgnored
= CSSEditUtils::GetComputedProperty(
523 aElement
, *nsGkAtoms::position
, positionValue
);
524 if (NS_WARN_IF(Destroyed())) {
525 return NS_ERROR_EDITOR_DESTROYED
;
527 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored
),
528 "CSSEditUtils::GetComputedProperty(nsGkAtoms::"
529 "position) failed, but ignored");
530 isPositioned
= positionValue
.EqualsLiteral("absolute");
534 // Yes, it is absolutely positioned
535 mResizedObjectIsAbsolutelyPositioned
= true;
537 // Get the all the computed css styles attached to the element node
538 RefPtr
<nsComputedDOMStyle
> computedDOMStyle
=
539 CSSEditUtils::GetComputedStyle(&aElement
);
540 if (NS_WARN_IF(!computedDOMStyle
)) {
541 return NS_ERROR_FAILURE
;
544 aBorderLeft
= GetCSSFloatValue(computedDOMStyle
, "border-left-width"_ns
);
545 aBorderTop
= GetCSSFloatValue(computedDOMStyle
, "border-top-width"_ns
);
546 aMarginLeft
= GetCSSFloatValue(computedDOMStyle
, "margin-left"_ns
);
547 aMarginTop
= GetCSSFloatValue(computedDOMStyle
, "margin-top"_ns
);
549 aX
= GetCSSFloatValue(computedDOMStyle
, "left"_ns
) + aMarginLeft
+
551 aY
= GetCSSFloatValue(computedDOMStyle
, "top"_ns
) + aMarginTop
+ aBorderTop
;
552 aW
= GetCSSFloatValue(computedDOMStyle
, "width"_ns
);
553 aH
= GetCSSFloatValue(computedDOMStyle
, "height"_ns
);
555 mResizedObjectIsAbsolutelyPositioned
= false;
556 RefPtr
<nsGenericHTMLElement
> htmlElement
=
557 nsGenericHTMLElement::FromNode(aElement
);
559 return NS_ERROR_NULL_POINTER
;
561 DebugOnly
<nsresult
> rvIgnored
= GetElementOrigin(aElement
, aX
, aY
);
562 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored
),
563 "HTMLEditor::GetElementOrigin() failed, but ignored");
565 aW
= htmlElement
->OffsetWidth();
566 aH
= htmlElement
->OffsetHeight();
576 nsresult
HTMLEditor::SetAnonymousElementPositionWithoutTransaction(
577 nsStyledElement
& aStyledElement
, int32_t aX
, int32_t aY
) {
579 rv
= CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
580 aStyledElement
, *nsGkAtoms::left
, aX
);
581 if (rv
== NS_ERROR_EDITOR_DESTROYED
) {
583 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::left) "
584 "destroyed the editor");
585 return NS_ERROR_EDITOR_DESTROYED
;
587 NS_WARNING_ASSERTION(
589 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::left) "
590 "failed, but ignored");
591 rv
= CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
592 aStyledElement
, *nsGkAtoms::top
, aY
);
593 if (rv
== NS_ERROR_EDITOR_DESTROYED
) {
595 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::top) "
596 "destroyed the editor");
597 return NS_ERROR_EDITOR_DESTROYED
;
599 NS_WARNING_ASSERTION(
601 "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::top) "
602 "failed, but ignored");
606 } // namespace mozilla