Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#include "HTMLEditor.h"
#include "HTMLEditHelpers.h"
#include "HTMLEditorInlines.h"
#include "AutoRangeArray.h"
#include "CSSEditUtils.h"
#include "EditAction.h"
#include "EditorBase.h"
#include "EditorDOMPoint.h"
#include "EditorUtils.h"
#include "ErrorList.h"
#include "HTMLEditorEventListener.h"
#include "HTMLEditUtils.h"
#include "InsertNodeTransaction.h"
#include "JoinNodesTransaction.h"
#include "JoinSplitNodeDirection.h"
#include "MoveNodeTransaction.h"
#include "PendingStyles.h"
#include "ReplaceTextTransaction.h"
#include "SplitNodeTransaction.h"
#include "WSRunObject.h"
#include "mozilla/ComposerCommandsUpdater.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/Encoding.h" // for Encoding
#include "mozilla/IntegerRange.h" // for IntegerRange
#include "mozilla/InternalMutationEvent.h"
#include "mozilla/mozInlineSpellChecker.h"
#include "mozilla/Preferences.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_editor.h"
#include "mozilla/StyleSheet.h"
#include "mozilla/StyleSheetInlines.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TextEvents.h"
#include "mozilla/TextServicesDocument.h"
#include "mozilla/ToString.h"
#include "mozilla/css/Loader.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Attr.h"
#include "mozilla/dom/DocumentFragment.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/EventTarget.h"
#include "mozilla/dom/HTMLAnchorElement.h"
#include "mozilla/dom/HTMLBodyElement.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/NameSpaceConstants.h"
#include "mozilla/dom/Selection.h"
#include "nsContentList.h"
#include "nsContentUtils.h"
#include "nsCRT.h"
#include "nsDebug.h"
#include "nsDOMAttributeMap.h"
#include "nsElementTable.h"
#include "nsFocusManager.h"
#include "nsGenericHTMLElement.h"
#include "nsGkAtoms.h"
#include "nsHTMLDocument.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsIEditActionListener.h"
#include "nsIFrame.h"
#include "nsIPrincipal.h"
#include "nsISelectionController.h"
#include "nsIURI.h"
#include "nsIWidget.h"
#include "nsNetUtil.h"
#include "nsPresContext.h"
#include "nsPrintfCString.h"
#include "nsPIDOMWindow.h"
#include "nsStyledElement.h"
#include "nsTextFragment.h"
#include "nsUnicharUtils.h"
namespace mozilla {
using namespace dom;
using namespace widget;
using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
using LeafNodeType = HTMLEditUtils::LeafNodeType;
using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
// Some utilities to handle overloading of "A" tag for link and named anchor.
static bool IsLinkTag(const nsAtom& aTagName) {
return &aTagName == nsGkAtoms::href;
}
static bool IsNamedAnchorTag(const nsAtom& aTagName) {
return &aTagName == nsGkAtoms::anchor;
}
// Helper struct for DoJoinNodes() and DoSplitNode().
struct MOZ_STACK_CLASS SavedRange final {
RefPtr<Selection> mSelection;
nsCOMPtr<nsINode> mStartContainer;
nsCOMPtr<nsINode> mEndContainer;
uint32_t mStartOffset = 0;
uint32_t mEndOffset = 0;
};
/******************************************************************************
* HTMLEditor::AutoSelectionRestorer
*****************************************************************************/
HTMLEditor::AutoSelectionRestorer::AutoSelectionRestorer(
HTMLEditor& aHTMLEditor)
: mHTMLEditor(nullptr) {
if (aHTMLEditor.ArePreservingSelection()) {
// We already have initialized mParentData::mSavedSelection, so this must
// be nested call.
return;
}
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
mHTMLEditor = &aHTMLEditor;
mHTMLEditor->PreserveSelectionAcrossActions();
}
HTMLEditor::AutoSelectionRestorer::~AutoSelectionRestorer() {
if (!mHTMLEditor || !mHTMLEditor->ArePreservingSelection()) {
return;
}
DebugOnly<nsresult> rvIgnored = mHTMLEditor->RestorePreservedSelection();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"EditorBase::RestorePreservedSelection() failed, but ignored");
}
void HTMLEditor::AutoSelectionRestorer::Abort() {
if (mHTMLEditor) {
mHTMLEditor->StopPreservingSelection();
}
}
/******************************************************************************
* HTMLEditor
*****************************************************************************/
template Result<CreateContentResult, nsresult>
HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(
nsIContent& aContentToInsert, const EditorDOMPoint& aPointToInsert,
SplitAtEdges aSplitAtEdges);
template Result<CreateElementResult, nsresult>
HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(
Element& aContentToInsert, const EditorDOMPoint& aPointToInsert,
SplitAtEdges aSplitAtEdges);
template Result<CreateTextResult, nsresult>
HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(
Text& aContentToInsert, const EditorDOMPoint& aPointToInsert,
SplitAtEdges aSplitAtEdges);
HTMLEditor::InitializeInsertingElement HTMLEditor::DoNothingForNewElement =
[](HTMLEditor&, Element&, const EditorDOMPoint&) { return NS_OK; };
HTMLEditor::InitializeInsertingElement HTMLEditor::InsertNewBRElement =
[](HTMLEditor& aHTMLEditor, Element& aNewElement, const EditorDOMPoint&)
MOZ_CAN_RUN_SCRIPT_BOUNDARY {
MOZ_ASSERT(!aNewElement.IsInComposedDoc());
Result<CreateElementResult, nsresult> createBRElementResult =
aHTMLEditor.InsertBRElement(WithTransaction::No,
EditorDOMPoint(&aNewElement, 0u));
if (MOZ_UNLIKELY(createBRElementResult.isErr())) {
NS_WARNING_ASSERTION(
createBRElementResult.isOk(),
"HTMLEditor::InsertBRElement(WithTransaction::No) failed");
return createBRElementResult.unwrapErr();
}
createBRElementResult.unwrap().IgnoreCaretPointSuggestion();
return NS_OK;
};
// static
Result<CreateElementResult, nsresult>
HTMLEditor::AppendNewElementToInsertingElement(
HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName, Element& aNewElement,
const InitializeInsertingElement& aInitializer) {
MOZ_ASSERT(!aNewElement.IsInComposedDoc());
Result<CreateElementResult, nsresult> createNewElementResult =
aHTMLEditor.CreateAndInsertElement(
WithTransaction::No, const_cast<nsStaticAtom&>(aTagName),
EditorDOMPoint(&aNewElement, 0u), aInitializer);
NS_WARNING_ASSERTION(
createNewElementResult.isOk(),
"HTMLEditor::CreateAndInsertElement(WithTransaction::No) failed");
return createNewElementResult;
}
// static
Result<CreateElementResult, nsresult>
HTMLEditor::AppendNewElementWithBRToInsertingElement(
HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName,
Element& aNewElement) {
MOZ_ASSERT(!aNewElement.IsInComposedDoc());
Result<CreateElementResult, nsresult> createNewElementWithBRResult =
HTMLEditor::AppendNewElementToInsertingElement(
aHTMLEditor, aTagName, aNewElement, HTMLEditor::InsertNewBRElement);
NS_WARNING_ASSERTION(
createNewElementWithBRResult.isOk(),
"HTMLEditor::AppendNewElementToInsertingElement() failed");
return createNewElementWithBRResult;
}
HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributes =
[](HTMLEditor&, const Element&, const Element&, const Attr&, nsString&) {
return true;
};
HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptId =
[](HTMLEditor&, const Element&, const Element&, const Attr& aAttr,
nsString&) {
return aAttr.NodeInfo()->NamespaceID() != kNameSpaceID_None ||
aAttr.NodeInfo()->NameAtom() != nsGkAtoms::id;
};
HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptDir =
[](HTMLEditor&, const Element&, const Element&, const Attr& aAttr,
nsString&) {
return aAttr.NodeInfo()->NamespaceID() != kNameSpaceID_None ||
aAttr.NodeInfo()->NameAtom() != nsGkAtoms::dir;
};
HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptIdAndDir =
[](HTMLEditor&, const Element&, const Element&, const Attr& aAttr,
nsString&) {
return !(aAttr.NodeInfo()->NamespaceID() == kNameSpaceID_None &&
(aAttr.NodeInfo()->NameAtom() == nsGkAtoms::id ||
aAttr.NodeInfo()->NameAtom() == nsGkAtoms::dir));
};
static bool ShouldUseTraditionalJoinSplitDirection(const Document& aDocument) {
if (nsIPrincipal* principal = aDocument.GetPrincipalForPrefBasedHacks()) {
if (principal->IsURIInPrefList("editor.join_split_direction."
"force_use_traditional_direction")) {
return true;
}
if (principal->IsURIInPrefList("editor.join_split_direction."
"force_use_compatible_direction")) {
return false;
}
}
return !StaticPrefs::
editor_join_split_direction_compatible_with_the_other_browsers();
}
HTMLEditor::HTMLEditor(const Document& aDocument)
: EditorBase(EditorBase::EditorType::HTML),
mCRInParagraphCreatesParagraph(false),
mUseGeckoTraditionalJoinSplitBehavior(
ShouldUseTraditionalJoinSplitDirection(aDocument)),
mIsObjectResizingEnabled(
StaticPrefs::editor_resizing_enabled_by_default()),
mIsResizing(false),
mPreserveRatio(false),
mResizedObjectIsAnImage(false),
mIsAbsolutelyPositioningEnabled(
StaticPrefs::editor_positioning_enabled_by_default()),
mResizedObjectIsAbsolutelyPositioned(false),
mGrabberClicked(false),
mIsMoving(false),
mSnapToGridEnabled(false),
mIsInlineTableEditingEnabled(
StaticPrefs::editor_inline_table_editing_enabled_by_default()),
mIsCSSPrefChecked(StaticPrefs::editor_use_css()),
mOriginalX(0),
mOriginalY(0),
mResizedObjectX(0),
mResizedObjectY(0),
mResizedObjectWidth(0),
mResizedObjectHeight(0),
mResizedObjectMarginLeft(0),
mResizedObjectMarginTop(0),
mResizedObjectBorderLeft(0),
mResizedObjectBorderTop(0),
mXIncrementFactor(0),
mYIncrementFactor(0),
mWidthIncrementFactor(0),
mHeightIncrementFactor(0),
mInfoXIncrement(20),
mInfoYIncrement(20),
mPositionedObjectX(0),
mPositionedObjectY(0),
mPositionedObjectWidth(0),
mPositionedObjectHeight(0),
mPositionedObjectMarginLeft(0),
mPositionedObjectMarginTop(0),
mPositionedObjectBorderLeft(0),
mPositionedObjectBorderTop(0),
mGridSize(0),
mDefaultParagraphSeparator(ParagraphSeparator::div) {}
HTMLEditor::~HTMLEditor() {
// Collect the data of `beforeinput` event only when it's enabled because
// web apps should switch their behavior with feature detection with
// checking `onbeforeinput` or `getTargetRanges`.
if (StaticPrefs::dom_input_events_beforeinput_enabled()) {
Telemetry::Accumulate(
Telemetry::HTMLEDITORS_WITH_BEFOREINPUT_LISTENERS,
MayHaveBeforeInputEventListenersForTelemetry() ? 1 : 0);
Telemetry::Accumulate(
Telemetry::HTMLEDITORS_OVERRIDDEN_BY_BEFOREINPUT_LISTENERS,
mHasBeforeInputBeenCanceled ? 1 : 0);
Telemetry::Accumulate(
Telemetry::
HTMLEDITORS_WITH_MUTATION_LISTENERS_WITHOUT_BEFOREINPUT_LISTENERS,
!MayHaveBeforeInputEventListenersForTelemetry() &&
MayHaveMutationEventListeners()
? 1
: 0);
Telemetry::Accumulate(
Telemetry::
HTMLEDITORS_WITH_MUTATION_OBSERVERS_WITHOUT_BEFOREINPUT_LISTENERS,
!MayHaveBeforeInputEventListenersForTelemetry() &&
MutationObserverHasObservedNodeForTelemetry()
? 1
: 0);
}
mPendingStylesToApplyToNewContent = nullptr;
if (mDisabledLinkHandling) {
if (Document* doc = GetDocument()) {
doc->SetLinkHandlingEnabled(mOldLinkHandlingEnabled);
}
}
RemoveEventListeners();
HideAnonymousEditingUIs();
}
NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEditor, EditorBase)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingStylesToApplyToNewContent)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mComposerCommandsUpdater)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mChangedRangeForTopLevelEditSubAction)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPaddingBRElementForEmptyEditor)
tmp->HideAnonymousEditingUIs();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEditor, EditorBase)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingStylesToApplyToNewContent)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mComposerCommandsUpdater)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChangedRangeForTopLevelEditSubAction)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPaddingBRElementForEmptyEditor)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopLeftHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopRightHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLeftHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRightHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomLeftHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomRightHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActivatedHandle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingShadow)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingInfo)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizedObject)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbsolutelyPositionedObject)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGrabber)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositioningShadow)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineEditedCell)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnBeforeButton)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveColumnButton)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnAfterButton)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowBeforeButton)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveRowButton)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowAfterButton)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_ADDREF_INHERITED(HTMLEditor, EditorBase)
NS_IMPL_RELEASE_INHERITED(HTMLEditor, EditorBase)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLEditor)
NS_INTERFACE_MAP_ENTRY(nsIHTMLEditor)
NS_INTERFACE_MAP_ENTRY(nsIHTMLObjectResizer)
NS_INTERFACE_MAP_ENTRY(nsIHTMLAbsPosEditor)
NS_INTERFACE_MAP_ENTRY(nsIHTMLInlineTableEditor)
NS_INTERFACE_MAP_ENTRY(nsITableEditor)
NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport)
NS_INTERFACE_MAP_END_INHERITING(EditorBase)
nsresult HTMLEditor::Init(Document& aDocument,
ComposerCommandsUpdater& aComposerCommandsUpdater,
uint32_t aFlags) {
MOZ_ASSERT(!mInitSucceeded,
"HTMLEditor::Init() called again without calling PreDestroy()?");
MOZ_DIAGNOSTIC_ASSERT(!mComposerCommandsUpdater ||
mComposerCommandsUpdater == &aComposerCommandsUpdater);
mComposerCommandsUpdater = &aComposerCommandsUpdater;
RefPtr<PresShell> presShell = aDocument.GetPresShell();
if (NS_WARN_IF(!presShell)) {
return NS_ERROR_FAILURE;
}
nsresult rv = InitInternal(aDocument, nullptr, *presShell, aFlags);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::InitInternal() failed");
return rv;
}
// Init mutation observer
aDocument.AddMutationObserverUnlessExists(this);
if (!mRootElement) {
UpdateRootElement();
}
// disable Composer-only features
if (IsMailEditor()) {
DebugOnly<nsresult> rvIgnored = SetAbsolutePositioningEnabled(false);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetAbsolutePositioningEnabled(false) failed, but ignored");
rvIgnored = SetSnapToGridEnabled(false);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::SetSnapToGridEnabled(false) failed, but ignored");
}
// disable links
Document* document = GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_FAILURE;
}
if (!IsInPlaintextMode() && !IsInteractionAllowed()) {
mDisabledLinkHandling = true;
mOldLinkHandlingEnabled = document->LinkHandlingEnabled();
document->SetLinkHandlingEnabled(false);
}
// init the type-in state
mPendingStylesToApplyToNewContent = new PendingStyles();
if (!IsInteractionAllowed()) {
nsCOMPtr<nsIURI> uaURI;
rv = NS_NewURI(getter_AddRefs(uaURI),
NS_ENSURE_SUCCESS(rv, rv);
rv = document->LoadAdditionalStyleSheet(Document::eAgentSheet, uaURI);
NS_ENSURE_SUCCESS(rv, rv);
}
AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_FAILURE;
}
rv = InitEditorContentAndSelection();
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::InitEditorContentAndSelection() failed");
// XXX Sholdn't we expose `NS_ERROR_EDITOR_DESTROYED` even though this
// is a public method?
return EditorBase::ToGenericNSResult(rv);
}
// Throw away the old transaction manager if this is not the first time that
// we're initializing the editor.
ClearUndoRedo();
EnableUndoRedo(); // FYI: Creating mTransactionManager in this call
if (mTransactionManager) {
mTransactionManager->Attach(*this);
}
MOZ_ASSERT(!mInitSucceeded, "HTMLEditor::Init() shouldn't be nested");
mInitSucceeded = true;
return NS_OK;
}
nsresult HTMLEditor::PostCreate() {
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv = PostCreateInternal();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::PostCreatInternal() failed");
return rv;
}
void HTMLEditor::PreDestroy() {
if (mDidPreDestroy) {
return;
}
mInitSucceeded = false;
// FYI: Cannot create AutoEditActionDataSetter here. However, it does not
// necessary for the methods called by the following code.
RefPtr<Document> document = GetDocument();
if (document) {
document->RemoveMutationObserver(this);
if (!IsInteractionAllowed()) {
nsCOMPtr<nsIURI> uaURI;
nsresult rv = NS_NewURI(getter_AddRefs(uaURI),
if (NS_SUCCEEDED(rv)) {
document->RemoveAdditionalStyleSheet(Document::eAgentSheet, uaURI);
}
}
}
// Clean up after our anonymous content -- we don't want these nodes to
// stay around (which they would, since the frames have an owning reference).
PresShell* presShell = GetPresShell();
if (presShell && presShell->IsDestroying()) {
// Just destroying PresShell now.
// We have to keep UI elements of anonymous content until PresShell
// is destroyed.
RefPtr<HTMLEditor> self = this;
nsContentUtils::AddScriptRunner(
NS_NewRunnableFunction("HTMLEditor::PreDestroy",
[self]() { self->HideAnonymousEditingUIs(); }));
} else {
// PresShell is alive or already gone.
HideAnonymousEditingUIs();
}
mPaddingBRElementForEmptyEditor = nullptr;
PreDestroyInternal();
}
NS_IMETHODIMP HTMLEditor::GetDocumentCharacterSet(nsACString& aCharacterSet) {
nsresult rv = GetDocumentCharsetInternal(aCharacterSet);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::GetDocumentCharsetInternal() failed");
return rv;
}
NS_IMETHODIMP HTMLEditor::SetDocumentCharacterSet(
const nsACString& aCharacterSet) {
AutoEditActionDataSetter editActionData(*this, EditAction::eSetCharacterSet);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent() failed");
return EditorBase::ToGenericNSResult(rv);
}
RefPtr<Document> document = GetDocument();
if (NS_WARN_IF(!document)) {
return EditorBase::ToGenericNSResult(NS_ERROR_NOT_INITIALIZED);
}
// This method is scriptable, so add-ons could pass in something other
// than a canonical name.
const Encoding* encoding = Encoding::ForLabelNoReplacement(aCharacterSet);
if (!encoding) {
NS_WARNING("Encoding::ForLabelNoReplacement() failed");
return EditorBase::ToGenericNSResult(NS_ERROR_INVALID_ARG);
}
document->SetDocumentCharacterSet(WrapNotNull(encoding));
// Update META charset element.
if (UpdateMetaCharsetWithTransaction(*document, aCharacterSet)) {
return NS_OK;
}
// Set attributes to the created element
if (aCharacterSet.IsEmpty()) {
return NS_OK;
}
RefPtr<nsContentList> headElementList =
document->GetElementsByTagName(u"head"_ns);
if (NS_WARN_IF(!headElementList)) {
return NS_OK;
}
nsCOMPtr<nsIContent> primaryHeadElement = headElementList->Item(0);
if (NS_WARN_IF(!primaryHeadElement)) {
return NS_OK;
}
// Create a new meta charset tag
Result<CreateElementResult, nsresult> createNewMetaElementResult =
CreateAndInsertElement(
WithTransaction::Yes, *nsGkAtoms::meta,
EditorDOMPoint(primaryHeadElement, 0),
[&aCharacterSet](HTMLEditor&, Element& aMetaElement,
const EditorDOMPoint&) {
MOZ_ASSERT(!aMetaElement.IsInComposedDoc());
DebugOnly<nsresult> rvIgnored =
aMetaElement.SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv,
u"Content-Type"_ns, false);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"Element::SetAttr(nsGkAtoms::httpEquiv, \"Content-Type\", "
"false) failed, but ignored");
rvIgnored =
aMetaElement.SetAttr(kNameSpaceID_None, nsGkAtoms::content,
u"text/html;charset="_ns +
NS_ConvertASCIItoUTF16(aCharacterSet),
false);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
nsPrintfCString(
"Element::SetAttr(nsGkAtoms::content, "
"\"text/html;charset=%s\", false) failed, but ignored",
nsPromiseFlatCString(aCharacterSet).get())
.get());
return NS_OK;
});
NS_WARNING_ASSERTION(createNewMetaElementResult.isOk(),
"HTMLEditor::CreateAndInsertElement(WithTransaction::"
"Yes, nsGkAtoms::meta) failed, but ignored");
// Probably, we don't need to update selection in this case since we should
// not put selection into <head> element.
createNewMetaElementResult.inspect().IgnoreCaretPointSuggestion();
return NS_OK;
}
bool HTMLEditor::UpdateMetaCharsetWithTransaction(
Document& aDocument, const nsACString& aCharacterSet) {
// get a list of META tags
RefPtr<nsContentList> metaElementList =
aDocument.GetElementsByTagName(u"meta"_ns);
if (NS_WARN_IF(!metaElementList)) {
return false;
}
for (uint32_t i = 0; i < metaElementList->Length(true); ++i) {
RefPtr<Element> metaElement = metaElementList->Item(i)->AsElement();
MOZ_ASSERT(metaElement);
nsAutoString currentValue;
metaElement->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, currentValue);
if (!FindInReadable(u"content-type"_ns, currentValue,
nsCaseInsensitiveStringComparator)) {
continue;
}
metaElement->GetAttr(kNameSpaceID_None, nsGkAtoms::content, currentValue);
constexpr auto charsetEquals = u"charset="_ns;
nsAString::const_iterator originalStart, start, end;
originalStart = currentValue.BeginReading(start);
currentValue.EndReading(end);
if (!FindInReadable(charsetEquals, start, end,
nsCaseInsensitiveStringComparator)) {
continue;
}
// set attribute to <original prefix> charset=text/html
nsresult rv = SetAttributeWithTransaction(
*metaElement, *nsGkAtoms::content,
Substring(originalStart, start) + charsetEquals +
NS_ConvertASCIItoUTF16(aCharacterSet));
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"EditorBase::SetAttributeWithTransaction(nsGkAtoms::content) failed");
return NS_SUCCEEDED(rv);
}
return false;
}
NS_IMETHODIMP HTMLEditor::NotifySelectionChanged(Document* aDocument,
Selection* aSelection,
int16_t aReason,
int32_t aAmount) {
if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) {
return NS_ERROR_INVALID_ARG;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
if (mPendingStylesToApplyToNewContent) {
RefPtr<PendingStyles> pendingStyles = mPendingStylesToApplyToNewContent;
pendingStyles->OnSelectionChange(*this, aReason);
// We used a class which derived from nsISelectionListener to call
// HTMLEditor::RefreshEditingUI(). The lifetime of the class was
// exactly same as mPendingStylesToApplyToNewContent. So, call it only when
// mPendingStylesToApplyToNewContent is not nullptr.
if ((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
nsISelectionListener::KEYPRESS_REASON |
nsISelectionListener::SELECTALL_REASON)) &&
aSelection) {
// the selection changed and we need to check if we have to
// hide and/or redisplay resizing handles
// FYI: This is an XPCOM method. So, the caller, Selection, guarantees
// the lifetime of this instance. So, don't need to grab this with
// local variable.
DebugOnly<nsresult> rv = RefreshEditingUI();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::RefreshEditingUI() failed, but ignored");
}
}
if (mComposerCommandsUpdater) {
RefPtr<ComposerCommandsUpdater> updater = mComposerCommandsUpdater;
updater->OnSelectionChange();
}
nsresult rv = EditorBase::NotifySelectionChanged(aDocument, aSelection,
aReason, aAmount);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::NotifySelectionChanged() failed");
return rv;
}
void HTMLEditor::UpdateRootElement() {
// Use the HTML documents body element as the editor root if we didn't
// get a root element during initialization.
mRootElement = GetBodyElement();
if (!mRootElement) {
RefPtr<Document> doc = GetDocument();
if (doc) {
// If there is no HTML body element,
// we should use the document root element instead.
mRootElement = doc->GetDocumentElement();
}
// else leave it null, for lack of anything better.
}
}
nsresult HTMLEditor::OnFocus(const nsINode& aOriginalEventTargetNode) {
// Before doing anything, we should check whether the original target is still
// valid focus event target because it may have already lost focus.
if (!CanKeepHandlingFocusEvent(aOriginalEventTargetNode)) {
return NS_OK;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_FAILURE;
}
return EditorBase::OnFocus(aOriginalEventTargetNode);
}
nsresult HTMLEditor::OnBlur(const EventTarget* aEventTarget) {
// check if something else is focused. If another element is focused, then
// we should not change the selection.
nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
if (MOZ_UNLIKELY(!focusManager)) {
return NS_OK;
}
// If another element already has focus, we should not maintain the selection
// because we may not have the rights doing it.
if (focusManager->GetFocusedElement()) {
return NS_OK;
}
// If it's in the designMode, and blur occurs, the target must be the
// document node. If a blur event is fired and the target is an element, it
// must be delayed blur event at initializing the `HTMLEditor`.
// TODO: Add automated tests for checking the case that the target node
// is in a shadow DOM tree whose host is in design mode.
if (IsInDesignMode() && Element::FromEventTargetOrNull(aEventTarget)) {
return NS_OK;
}
nsresult rv = FinalizeSelection();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::FinalizeSelection() failed");
return rv;
}
Element* HTMLEditor::FindSelectionRoot(const nsINode& aNode) const {
MOZ_ASSERT(aNode.IsDocument() || aNode.IsContent(),
"aNode must be content or document node");
if (NS_WARN_IF(!aNode.IsInComposedDoc())) {
return nullptr;
}
if (aNode.IsInDesignMode()) {
return GetDocument()->GetRootElement();
}
nsIContent* content = const_cast<nsIContent*>(aNode.AsContent());
if (!content->HasFlag(NODE_IS_EDITABLE)) {
// If the content is in read-write state but is not editable itself,
// return it as the selection root.
if (content->IsElement() &&
content->AsElement()->State().HasState(ElementState::READWRITE)) {
return content->AsElement();
}
return nullptr;
}
// For non-readonly editors we want to find the root of the editable subtree
// containing aContent.
return content->GetEditingHost();
}
bool HTMLEditor::IsInDesignMode() const {
// TODO: If active editing host is in a shadow tree, it means that we should
// behave exactly same as contenteditable mode because shadow tree
// content is not editable even if composed document is in design mode,
// but contenteditable elements in shoadow trees are focusable and
// their content is editable. Changing this affects to drop event
// handler and blur event handler, so please add new tests for them
// when you change here.
Document* document = GetDocument();
return document && document->IsInDesignMode();
}
bool HTMLEditor::EntireDocumentIsEditable() const {
Document* document = GetDocument();
return document && document->GetDocumentElement() &&
(document->GetDocumentElement()->IsEditable() ||
(document->GetBody() && document->GetBody()->IsEditable()));
}
void HTMLEditor::CreateEventListeners() {
// Don't create the handler twice
if (!mEventListener) {
mEventListener = new HTMLEditorEventListener();
}
}
nsresult HTMLEditor::InstallEventListeners() {
if (NS_WARN_IF(!IsInitialized()) || NS_WARN_IF(!mEventListener)) {
return NS_ERROR_NOT_INITIALIZED;
}
// NOTE: HTMLEditor doesn't need to initialize mEventTarget here because
// the target must be document node and it must be referenced as weak pointer.
HTMLEditorEventListener* listener =
reinterpret_cast<HTMLEditorEventListener*>(mEventListener.get());
nsresult rv = listener->Connect(this);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditorEventListener::Connect() failed");
return rv;
}
void HTMLEditor::RemoveEventListeners() {
if (!IsInitialized()) {
return;
}
EditorBase::RemoveEventListeners();
}
void HTMLEditor::Detach(
const ComposerCommandsUpdater& aComposerCommandsUpdater) {
MOZ_DIAGNOSTIC_ASSERT_IF(
mComposerCommandsUpdater,
&aComposerCommandsUpdater == mComposerCommandsUpdater);
if (mComposerCommandsUpdater == &aComposerCommandsUpdater) {
mComposerCommandsUpdater = nullptr;
if (mTransactionManager) {
mTransactionManager->Detach(*this);
}
}
}
NS_IMETHODIMP HTMLEditor::BeginningOfDocument() {
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv = MaybeCollapseSelectionAtFirstEditableNode(false);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed");
return rv;
}
NS_IMETHODIMP HTMLEditor::EndOfDocument() {
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() failed");
// This is low level API for embedders and chrome script so that we can return
// raw error code here.
return rv;
}
nsresult HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() const {
MOZ_ASSERT(IsEditActionDataAvailable());
// We should do nothing with the result of GetRoot() if only a part of the
// document is editable.
if (!EntireDocumentIsEditable()) {
return NS_OK;
}
RefPtr<Element> bodyOrDocumentElement = GetRoot();
if (NS_WARN_IF(!bodyOrDocumentElement)) {
return NS_ERROR_NULL_POINTER;
}
auto pointToPutCaret = [&]() -> EditorRawDOMPoint {
nsCOMPtr<nsIContent> lastLeafContent = HTMLEditUtils::GetLastLeafContent(
*bodyOrDocumentElement, {LeafNodeType::OnlyLeafNode});
if (!lastLeafContent) {
return EditorRawDOMPoint::AtEndOf(*bodyOrDocumentElement);
}
// TODO: We should put caret into text node if it's visible.
return lastLeafContent->IsText() ||
HTMLEditUtils::IsContainerNode(*lastLeafContent)
? EditorRawDOMPoint::AtEndOf(*lastLeafContent)
: EditorRawDOMPoint(lastLeafContent);
}();
nsresult rv = CollapseSelectionTo(pointToPutCaret);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionTo() failed");
return rv;
}
void HTMLEditor::InitializeSelectionAncestorLimit(
nsIContent& aAncestorLimit) const {
MOZ_ASSERT(IsEditActionDataAvailable());
// Hack for initializing selection.
// HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode() will try to
// collapse selection at first editable text node or inline element which
// cannot have text nodes as its children. However, selection has already
// set into the new editing host by user, we should not change it. For
// solving this issue, we should do nothing if selection range is in active
// editing host except it's not collapsed at start of the editing host since
// aSelection.SetAncestorLimiter(aAncestorLimit) will collapse selection
// at start of the new limiter if focus node of aSelection is outside of the
// editing host. However, we need to check here if selection is already
// collapsed at start of the editing host because it's possible JS to do it.
// In such case, we should not modify selection with calling
// MaybeCollapseSelectionAtFirstEditableNode().
// Basically, we should try to collapse selection at first editable node
// in HTMLEditor.
bool tryToCollapseSelectionAtFirstEditableNode = true;
if (SelectionRef().RangeCount() == 1 && SelectionRef().IsCollapsed()) {
Element* editingHost = ComputeEditingHost();
const nsRange* range = SelectionRef().GetRangeAt(0);
if (range->GetStartContainer() == editingHost && !range->StartOffset()) {
// JS or user operation has already collapsed selection at start of
// the editing host. So, we don't need to try to change selection
// in this case.
tryToCollapseSelectionAtFirstEditableNode = false;
}
}
EditorBase::InitializeSelectionAncestorLimit(aAncestorLimit);
// XXX Do we need to check if we still need to change selection? E.g.,
// we could have already lost focus while we're changing the ancestor
// limiter because it may causes "selectionchange" event.
if (tryToCollapseSelectionAtFirstEditableNode) {
DebugOnly<nsresult> rvIgnored =
MaybeCollapseSelectionAtFirstEditableNode(true);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(true) failed, "
"but ignored");
}
// If the target is a text control element, we won't handle user input
// for the `TextEditor` in it. However, we need to be open for `execCommand`.
// Therefore, we shouldn't set ancestor limit in this case.
// Note that we should do this once setting ancestor limiter for backward
// compatiblity of select events, etc. (Selection should be collapsed into
// the text control element.)
if (aAncestorLimit.HasIndependentSelection()) {
SelectionRef().SetAncestorLimiter(nullptr);
}
}
nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(
bool aIgnoreIfSelectionInEditingHost) const {
MOZ_ASSERT(IsEditActionDataAvailable());
RefPtr<Element> editingHost = ComputeEditingHost(LimitInBodyElement::No);
if (NS_WARN_IF(!editingHost)) {
return NS_OK;
}
// If selection range is already in the editing host and the range is not
// start of the editing host, we shouldn't reset selection. E.g., window
// is activated when the editor had focus before inactivated.
if (aIgnoreIfSelectionInEditingHost && SelectionRef().RangeCount() == 1) {
const nsRange* range = SelectionRef().GetRangeAt(0);
if (!range->Collapsed() ||
range->GetStartContainer() != editingHost.get() ||
range->StartOffset()) {
return NS_OK;
}
}
for (nsIContent* leafContent = HTMLEditUtils::GetFirstLeafContent(
*editingHost,
{LeafNodeType::LeafNodeOrNonEditableNode,
LeafNodeType::LeafNodeOrChildBlock},
editingHost);
leafContent;) {
// If we meet a non-editable node first, we should move caret to start
// of the container block or editing host.
if (!EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) {
MOZ_ASSERT(leafContent->GetParent());
MOZ_ASSERT(EditorUtils::IsEditableContent(*leafContent->GetParent(),
EditorType::HTML));
if (const Element* editableBlockElementOrInlineEditingHost =
HTMLEditUtils::GetAncestorElement(
*leafContent,
HTMLEditUtils::
ClosestEditableBlockElementOrInlineEditingHost)) {
nsresult rv = CollapseSelectionTo(
EditorDOMPoint(editableBlockElementOrInlineEditingHost, 0));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionTo() failed");
return rv;
}
NS_WARNING("Found leaf content did not have editable parent, why?");
return NS_ERROR_FAILURE;
}
// When we meet an empty inline element, we should look for a next sibling.
// For example, if current editor is:
// <div contenteditable><span></span><b><br></b></div>
// then, we should put caret at the <br> element. So, let's check if found
// node is an empty inline container element.
if (leafContent->IsElement() &&
HTMLEditUtils::IsInlineElement(*leafContent) &&
!HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent) &&
HTMLEditUtils::CanNodeContain(*leafContent, *nsGkAtoms::textTagName)) {
// Chromium collaps selection to start of the editing host when this is
// the last leaf content. So, we don't need special handling here.
leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
*leafContent, *editingHost,
{LeafNodeType::LeafNodeOrNonEditableNode,
LeafNodeType::LeafNodeOrChildBlock},
editingHost);
continue;
}
if (Text* text = leafContent->GetAsText()) {
// If there is editable and visible text node, move caret at first of
// the visible character.
WSScanResult scanResultInTextNode =
WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
editingHost, EditorRawDOMPoint(text, 0));
if ((scanResultInTextNode.InVisibleOrCollapsibleCharacters() ||
scanResultInTextNode.ReachedPreformattedLineBreak()) &&
scanResultInTextNode.TextPtr() == text) {
nsresult rv = CollapseSelectionTo(
scanResultInTextNode.Point<EditorRawDOMPoint>());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionTo() failed");
return rv;
}
// If it's an invisible text node, keep scanning next leaf.
leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
*leafContent, *editingHost,
{LeafNodeType::LeafNodeOrNonEditableNode,
LeafNodeType::LeafNodeOrChildBlock},
editingHost);
continue;
}
// If there is editable <br> or something void element like <img>, <input>,
// <hr> etc, move caret before it.
if (!HTMLEditUtils::CanNodeContain(*leafContent, *nsGkAtoms::textTagName) ||
HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) {
MOZ_ASSERT(leafContent->GetParent());
if (EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) {
nsresult rv = CollapseSelectionTo(EditorDOMPoint(leafContent));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionTo() failed");
return rv;
}
MOZ_ASSERT_UNREACHABLE(
"How do we reach editable leaf in non-editable element?");
// But if it's not editable, let's put caret at start of editing host
// for now.
nsresult rv = CollapseSelectionTo(EditorDOMPoint(editingHost, 0));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionTo() failed");
return rv;
}
// If we meet non-empty block element, we need to scan its child too.
if (HTMLEditUtils::IsBlockElement(*leafContent) &&
!HTMLEditUtils::IsEmptyNode(
*leafContent, {EmptyCheckOption::TreatSingleBRElementAsVisible}) &&
!HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) {
leafContent = HTMLEditUtils::GetFirstLeafContent(
*leafContent,
{LeafNodeType::LeafNodeOrNonEditableNode,
LeafNodeType::LeafNodeOrChildBlock},
editingHost);
continue;
}
// Otherwise, we must meet an empty block element or a data node like
// comment node. Let's ignore it.
leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
*leafContent, *editingHost,
{LeafNodeType::LeafNodeOrNonEditableNode,
LeafNodeType::LeafNodeOrChildBlock},
editingHost);
}
// If there is no visible/editable node except another block element in
// current editing host, we should move caret to very first of the editing
// host.
// XXX This may not make sense, but Chromium behaves so. Therefore, the
// reason why we do this is just compatibility with Chromium.
nsresult rv = CollapseSelectionTo(EditorDOMPoint(editingHost, 0));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionTo() failed");
return rv;
}
bool HTMLEditor::ArePreservingSelection() const {
return IsEditActionDataAvailable() && SavedSelectionRef().RangeCount();
}
void HTMLEditor::PreserveSelectionAcrossActions() {
MOZ_ASSERT(IsEditActionDataAvailable());
SavedSelectionRef().SaveSelection(SelectionRef());
RangeUpdaterRef().RegisterSelectionState(SavedSelectionRef());
}
nsresult HTMLEditor::RestorePreservedSelection() {
MOZ_ASSERT(IsEditActionDataAvailable());
if (!SavedSelectionRef().RangeCount()) {
// XXX Returing error when it does not store is odd because no selection
// ranges is not illegal case in general.
return NS_ERROR_FAILURE;
}
DebugOnly<nsresult> rvIgnored =
SavedSelectionRef().RestoreSelection(SelectionRef());
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"SelectionState::RestoreSelection() failed, but ignored");
StopPreservingSelection();
return NS_OK;
}
void HTMLEditor::StopPreservingSelection() {
MOZ_ASSERT(IsEditActionDataAvailable());
RangeUpdaterRef().DropSelectionState(SavedSelectionRef());
SavedSelectionRef().RemoveAllRanges();
}
void HTMLEditor::PreHandleMouseDown(const MouseEvent& aMouseDownEvent) {
if (mPendingStylesToApplyToNewContent) {
// mPendingStylesToApplyToNewContent will be notified of selection change
// even if aMouseDownEvent is not an acceptable event for this editor.
// Therefore, we need to notify it of this event too.
mPendingStylesToApplyToNewContent->PreHandleMouseEvent(aMouseDownEvent);
}
}
void HTMLEditor::PreHandleMouseUp(const MouseEvent& aMouseUpEvent) {
if (mPendingStylesToApplyToNewContent) {
// mPendingStylesToApplyToNewContent will be notified of selection change
// even if aMouseUpEvent is not an acceptable event for this editor.
// Therefore, we need to notify it of this event too.
mPendingStylesToApplyToNewContent->PreHandleMouseEvent(aMouseUpEvent);
}
}
void HTMLEditor::PreHandleSelectionChangeCommand(Command aCommand) {
if (mPendingStylesToApplyToNewContent) {
mPendingStylesToApplyToNewContent->PreHandleSelectionChangeCommand(
aCommand);
}
}
void HTMLEditor::PostHandleSelectionChangeCommand(Command aCommand) {
if (!mPendingStylesToApplyToNewContent) {
return;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (!editActionData.CanHandle()) {
return;
}
mPendingStylesToApplyToNewContent->PostHandleSelectionChangeCommand(*this,
aCommand);
}
nsresult HTMLEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) {
// NOTE: When you change this method, you should also change:
// * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
if (NS_WARN_IF(!aKeyboardEvent)) {
return NS_ERROR_UNEXPECTED;
}
if (IsReadonly()) {
HandleKeyPressEventInReadOnlyMode(*aKeyboardEvent);
return NS_OK;
}
MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress,
"HandleKeyPressEvent gets non-keypress event");
switch (aKeyboardEvent->mKeyCode) {
case NS_VK_META:
case NS_VK_WIN:
case NS_VK_SHIFT:
case NS_VK_CONTROL:
case NS_VK_ALT:
// FYI: This shouldn't occur since modifier key shouldn't cause eKeyPress
// event.
aKeyboardEvent->PreventDefault();
return NS_OK;
case NS_VK_BACK:
case NS_VK_DELETE: {
nsresult rv = EditorBase::HandleKeyPressEvent(aKeyboardEvent);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::HandleKeyPressEvent() failed");
return rv;
}
case NS_VK_TAB: {
// Basically, "Tab" key be used only for focus navigation.
// FYI: In web apps, this is always true.
if (IsTabbable()) {
return NS_OK;
}
// If we're in the plaintext mode, and not tabbable editor, let's
// insert a horizontal tabulation.
if (IsInPlaintextMode()) {
if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() ||
aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() ||
aKeyboardEvent->IsOS()) {
return NS_OK;
}
// else we insert the tab straight through
aKeyboardEvent->PreventDefault();
nsresult rv = OnInputText(u"\t"_ns);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::OnInputText(\\t) failed");
return rv;
}
// Otherwise, e.g., we're an embedding editor in chrome, we can handle
// "Tab" key as an input.
if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() ||
aKeyboardEvent->IsMeta() || aKeyboardEvent->IsOS()) {
return NS_OK;
}
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection) || NS_WARN_IF(!selection->RangeCount())) {
return NS_ERROR_FAILURE;
}
nsINode* startContainer = selection->GetRangeAt(0)->GetStartContainer();
MOZ_ASSERT(startContainer);
if (!startContainer->IsContent()) {
break;
}
const Element* editableBlockElement =
HTMLEditUtils::GetInclusiveAncestorElement(
*startContainer->AsContent(),
HTMLEditUtils::ClosestEditableBlockElement);
if (!editableBlockElement) {
break;
}
// If selection is in a table element, we need special handling.
if (HTMLEditUtils::IsAnyTableElement(editableBlockElement)) {
Result<EditActionResult, nsresult> result =
HandleTabKeyPressInTable(aKeyboardEvent);
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::HandleTabKeyPressInTable() failed");
return EditorBase::ToGenericNSResult(result.unwrapErr());
}
if (!result.inspect().Handled()) {
return NS_OK;
}
nsresult rv = ScrollSelectionFocusIntoView();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"EditorBase::ScrollSelectionFocusIntoView() failed");
return EditorBase::ToGenericNSResult(rv);
}
// If selection is in an list item element, treat it as indent or outdent.
if (HTMLEditUtils::IsListItem(editableBlockElement)) {
aKeyboardEvent->PreventDefault();
if (!aKeyboardEvent->IsShift()) {
nsresult rv = IndentAsAction();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::IndentAsAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult rv = OutdentAsAction();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::OutdentAsAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
// If only "Tab" key is pressed in normal context, just treat it as
// horizontal tab character input.
if (aKeyboardEvent->IsShift()) {
return NS_OK;
}
aKeyboardEvent->PreventDefault();
nsresult rv = OnInputText(u"\t"_ns);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::OnInputText(\\t) failed");
return EditorBase::ToGenericNSResult(rv);
}
case NS_VK_RETURN:
if (!aKeyboardEvent->IsInputtingLineBreak()) {
return NS_OK;
}
aKeyboardEvent->PreventDefault(); // consumed
if (aKeyboardEvent->IsShift()) {
// Only inserts a <br> element.
nsresult rv = InsertLineBreakAsAction();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::InsertLineBreakAsAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
// uses rules to figure out what to insert
nsresult rv = InsertParagraphSeparatorAsAction();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::InsertParagraphSeparatorAsAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
if (!aKeyboardEvent->IsInputtingText()) {
// we don't PreventDefault() here or keybindings like control-x won't work
return NS_OK;
}
aKeyboardEvent->PreventDefault();
nsAutoString str(aKeyboardEvent->mCharCode);
nsresult rv = OnInputText(str);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed");
return rv;
}
NS_IMETHODIMP HTMLEditor::NodeIsBlock(nsINode* aNode, bool* aIsBlock) {
*aIsBlock = aNode && aNode->IsContent() &&
HTMLEditUtils::IsBlockElement(*aNode->AsContent());
return NS_OK;
}
NS_IMETHODIMP HTMLEditor::UpdateBaseURL() {
RefPtr<Document> document = GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_FAILURE;
}
// Look for an HTML <base> tag
RefPtr<nsContentList> baseElementList =
document->GetElementsByTagName(u"base"_ns);
// If no base tag, then set baseURL to the document's URL. This is very
// important, else relative URLs for links and images are wrong
if (!baseElementList || !baseElementList->Item(0)) {
document->SetBaseURI(document->GetDocumentURI());
}
return NS_OK;
}
NS_IMETHODIMP HTMLEditor::InsertLineBreak() {
// XPCOM method's InsertLineBreak() should insert paragraph separator in
// HTMLEditor.
AutoEditActionDataSetter editActionData(
*this, EditAction::eInsertParagraphSeparator);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
const RefPtr<Element> editingHost = ComputeEditingHost();
if (!editingHost) {
return NS_SUCCESS_DOM_NO_OPERATION;
}
Result<EditActionResult, nsresult> result =
InsertParagraphSeparatorAsSubAction(*editingHost);
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::InsertParagraphSeparatorAsSubAction() failed");
return EditorBase::ToGenericNSResult(result.unwrapErr());
}
return NS_OK;
}
nsresult HTMLEditor::InsertLineBreakAsAction(nsIPrincipal* aPrincipal) {
AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak,
aPrincipal);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
if (IsSelectionRangeContainerNotContent()) {
return NS_SUCCESS_DOM_NO_OPERATION;
}
rv = InsertLineBreakAsSubAction();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::InsertLineBreakAsSubAction() failed");
// Don't return NS_SUCCESS_DOM_NO_OPERATION for compatibility of `execCommand`
// result of Chrome.
return NS_FAILED(rv) ? rv : NS_OK;
}
nsresult HTMLEditor::InsertParagraphSeparatorAsAction(
nsIPrincipal* aPrincipal) {
AutoEditActionDataSetter editActionData(
*this, EditAction::eInsertParagraphSeparator, aPrincipal);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
const RefPtr<Element> editingHost = ComputeEditingHost();
if (!editingHost) {
return NS_SUCCESS_DOM_NO_OPERATION;
}
Result<EditActionResult, nsresult> result =
InsertParagraphSeparatorAsSubAction(*editingHost);
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::InsertParagraphSeparatorAsSubAction() failed");
return EditorBase::ToGenericNSResult(result.unwrapErr());
}
return NS_OK;
}
Result<EditActionResult, nsresult> HTMLEditor::HandleTabKeyPressInTable(
WidgetKeyboardEvent* aKeyboardEvent) {
MOZ_ASSERT(aKeyboardEvent);
AutoEditActionDataSetter dummyEditActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!dummyEditActionData.CanHandle())) {
// Do nothing if we didn't find a table cell.
return EditActionResult::IgnoredResult();
}
// Find enclosing table cell from selection (cell may be selected element)
const RefPtr<Element> cellElement =
GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
if (!cellElement) {
NS_WARNING(
"HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td) "
"returned nullptr");
// Do nothing if we didn't find a table cell.
return EditActionResult::IgnoredResult();
}
// find enclosing table
RefPtr<Element> table =
HTMLEditUtils::GetClosestAncestorTableElement(*cellElement);
if (!table) {
NS_WARNING("HTMLEditor::GetClosestAncestorTableElement() failed");
return EditActionResult::IgnoredResult();
}
// advance to next cell
// first create an iterator over the table
PostContentIterator postOrderIter;
nsresult rv = postOrderIter.Init(table);
if (NS_FAILED(rv)) {
NS_WARNING("PostContentIterator::Init() failed");
return Err(rv);
}
// position postOrderIter at block
rv = postOrderIter.PositionAt(cellElement);
if (NS_FAILED(rv)) {
NS_WARNING("PostContentIterator::PositionAt() failed");
return Err(rv);
}
do {
if (aKeyboardEvent->IsShift()) {
postOrderIter.Prev();
} else {
postOrderIter.Next();
}
nsCOMPtr<nsINode> node = postOrderIter.GetCurrentNode();
if (node && HTMLEditUtils::IsTableCell(node) &&
HTMLEditUtils::GetClosestAncestorTableElement(*node->AsElement()) ==
table) {
aKeyboardEvent->PreventDefault();
CollapseSelectionToDeepestNonTableFirstChild(node);
if (NS_WARN_IF(Destroyed())) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
return EditActionResult::HandledResult();
}
} while (!postOrderIter.IsDone());
if (aKeyboardEvent->IsShift()) {
return EditActionResult::IgnoredResult();
}
// If we haven't handled it yet, then we must have run off the end of the
// table. Insert a new row.
// XXX We should investigate whether this behavior is supported by other
// browsers later.
AutoEditActionDataSetter editActionData(*this,
EditAction::eInsertTableRowElement);
rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return Err(rv);
}
rv = InsertTableRowsWithTransaction(*cellElement, 1,
InsertPosition::eAfterSelectedCell);
if (NS_WARN_IF(Destroyed())) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::InsertTableRowsWithTransaction(*cellElement, 1, "
"InsertPosition::eAfterSelectedCell) failed");
return Err(rv);
}
aKeyboardEvent->PreventDefault();
// Put selection in right place. Use table code to get selection and index
// to new row...
RefPtr<Element> tblElement, cell;
int32_t row;
rv = GetCellContext(getter_AddRefs(tblElement), getter_AddRefs(cell), nullptr,
nullptr, &row, nullptr);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::GetCellContext() failed");
return Err(rv);
}
if (!tblElement) {
NS_WARNING("HTMLEditor::GetCellContext() didn't return table element");
return Err(NS_ERROR_FAILURE);
}
// ...so that we can ask for first cell in that row...
cell = GetTableCellElementAt(*tblElement, row, 0);
// ...and then set selection there. (Note that normally you should use
// CollapseSelectionToDeepestNonTableFirstChild(), but we know cell is an
// empty new cell, so this works fine)
if (cell) {
nsresult rv = CollapseSelectionToStartOf(*cell);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::CollapseSelectionToStartOf() failed");
return Err(NS_ERROR_EDITOR_DESTROYED);
}
}