Source code

Revision control

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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "HTMLEditor.h"
#include "HTMLEditorEventListener.h"
#include "HTMLEditUtils.h"
#include "JoinNodeTransaction.h"
#include "ReplaceTextTransaction.h"
#include "SplitNodeTransaction.h"
#include "TypeInState.h"
#include "WSRunObject.h"
#include "mozilla/ComposerCommandsUpdater.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EditAction.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorUtils.h"
#include "mozilla/Encoding.h" // for Encoding
#include "mozilla/EventStates.h"
#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/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/Selection.h"
#include "nsContentList.h"
#include "nsContentUtils.h"
#include "nsCRT.h"
#include "nsElementTable.h"
#include "nsFocusManager.h"
#include "nsGenericHTMLElement.h"
#include "nsGkAtoms.h"
#include "nsHTMLDocument.h"
#include "nsIContent.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 "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
*****************************************************************************/
HTMLEditor::HTMLEditor()
: mCRInParagraphCreatesParagraph(false),
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()),
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(
Preferences::GetBool("editor.use_div_for_default_newlines", true)
? ParagraphSeparator::div
: ParagraphSeparator::br) {
mIsHTMLEditorClass = true;
}
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);
}
mTypeInState = 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(mTypeInState)
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(mTypeInState)
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, uint32_t aFlags) {
MOZ_ASSERT(!mInitSucceeded,
"HTMLEditor::Init() called again without calling PreDestroy()?");
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");
}
// Init the HTML-CSS utils
mCSSEditUtils = MakeUnique<CSSEditUtils>(this);
// 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
mTypeInState = new TypeInState();
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();
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<RefPtr<Element>, nsresult> maybeNewMetaElement =
CreateNodeWithTransaction(*nsGkAtoms::meta,
EditorDOMPoint(primaryHeadElement, 0));
if (maybeNewMetaElement.isErr()) {
NS_WARNING(
"EditorBase::CreateNodeWithTransaction(nsGkAtoms::meta) failed, but "
"ignored");
return NS_OK;
}
MOZ_ASSERT(maybeNewMetaElement.inspect());
// not undoable, undo should undo CreateNodeWithTransaction().
DebugOnly<nsresult> rvIgnored = NS_OK;
rvIgnored = maybeNewMetaElement.inspect()->SetAttr(
kNameSpaceID_None, nsGkAtoms::httpEquiv, u"Content-Type"_ns, true);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Element::SetAttr(nsGkAtoms::httpEquiv, Content-Type) "
"failed, but ignored");
rvIgnored = maybeNewMetaElement.inspect()->SetAttr(
kNameSpaceID_None, nsGkAtoms::content,
u"text/html;charset="_ns + NS_ConvertASCIItoUTF16(aCharacterSet), true);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"Element::SetAttr(nsGkAtoms::content) failed, but ignored");
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) {
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 (mTypeInState) {
RefPtr<TypeInState> typeInState = mTypeInState;
typeInState->OnSelectionChange(*this, aReason);
// We used a class which derived from nsISelectionListener to call
// HTMLEditor::RefreshEditingUI(). The lifetime of the class was
// exactly same as mTypeInState. So, call it only when mTypeInState
// 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);
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.
}
}
Element* HTMLEditor::FindSelectionRoot(nsINode* aNode) const {
if (NS_WARN_IF(!aNode)) {
return nullptr;
}
MOZ_ASSERT(aNode->IsDocument() || aNode->IsContent(),
"aNode must be content or document node");
Document* document = aNode->GetComposedDoc();
if (NS_WARN_IF(!document)) {
return nullptr;
}
if (aNode->IsInUncomposedDoc() &&
(document->HasFlag(NODE_IS_EDITABLE) || !aNode->IsContent())) {
return document->GetRootElement();
}
// XXX If we have readonly flag, shouldn't return the element which has
// contenteditable="true"? However, such case isn't there without chrome
// permission script.
if (IsReadonly()) {
// We still want to allow selection in a readonly editor.
return GetRoot();
}
nsIContent* content = 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(NS_EVENT_STATE_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();
}
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();
}
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;
}
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 = GetActiveEditingHost();
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 = GetActiveEditingHost(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),
"HTMLEditor::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());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::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),
"HTMLEditor::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),
"HTMLEditor::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),
"HTMLEditor::CollapseSelectionTo() failed");
return rv;
}
bool HTMLEditor::ArePreservingSelection() const {
return IsEditActionDataAvailable() && !SavedSelectionRef().IsEmpty();
}
void HTMLEditor::PreserveSelectionAcrossActions() {
MOZ_ASSERT(IsEditActionDataAvailable());
SavedSelectionRef().SaveSelection(SelectionRef());
RangeUpdaterRef().RegisterSelectionState(SavedSelectionRef());
}
nsresult HTMLEditor::RestorePreservedSelection() {
MOZ_ASSERT(IsEditActionDataAvailable());
if (SavedSelectionRef().IsEmpty()) {
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().Clear();
}
void HTMLEditor::PreHandleMouseDown(const MouseEvent& aMouseDownEvent) {
if (mTypeInState) {
// mTypeInState 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.
mTypeInState->PreHandleMouseEvent(aMouseDownEvent);
}
}
void HTMLEditor::PreHandleMouseUp(const MouseEvent& aMouseUpEvent) {
if (mTypeInState) {
// mTypeInState 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.
mTypeInState->PreHandleMouseEvent(aMouseUpEvent);
}
}
void HTMLEditor::PreHandleSelectionChangeCommand(Command aCommand) {
if (mTypeInState) {
mTypeInState->PreHandleSelectionChangeCommand(aCommand);
}
}
void HTMLEditor::PostHandleSelectionChangeCommand(Command aCommand) {
if (!mTypeInState) {
return;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (!editActionData.CanHandle()) {
return;
}
mTypeInState->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)) {
EditActionResult result = HandleTabKeyPressInTable(aKeyboardEvent);
if (result.Failed()) {
NS_WARNING("HTMLEditor::HandleTabKeyPressInTable() failed");
return EditorBase::ToGenericNSResult(result.Rv());
}
if (!result.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);
}
EditActionResult result = InsertParagraphSeparatorAsSubAction();
NS_WARNING_ASSERTION(
result.Succeeded(),
"HTMLEditor::InsertParagraphSeparatorAsSubAction() failed");
return EditorBase::ToGenericNSResult(result.Rv());
}
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);
}
EditActionResult result = InsertParagraphSeparatorAsSubAction();
NS_WARNING_ASSERTION(
result.Succeeded(),
"HTMLEditor::InsertParagraphSeparatorAsSubAction() failed");
return EditorBase::ToGenericNSResult(result.Rv());
}
EditActionResult 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 EditActionIgnored();
}
// Find enclosing table cell from selection (cell may be selected element)
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 EditActionIgnored();
}
// find enclosing table
RefPtr<Element> table =
HTMLEditUtils::GetClosestAncestorTableElement(*cellElement);
if (!table) {
NS_WARNING("HTMLEditor::GetClosestAncestorTableElement() failed");
return EditActionIgnored();
}
// 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 EditActionResult(rv);
}
// position postOrderIter at block
rv = postOrderIter.PositionAt(cellElement);
if (NS_FAILED(rv)) {
NS_WARNING("PostContentIterator::PositionAt() failed");
return EditActionResult(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);
return EditActionHandled(
NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK);
}
} while (!postOrderIter.IsDone());
if (aKeyboardEvent->IsShift()) {
return EditActionIgnored();
}
// 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 EditActionHandled(rv);
}
rv = InsertTableRowsWithTransaction(1, InsertPosition::eAfterSelectedCell);
if (NS_WARN_IF(Destroyed())) {
return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::InsertTableRowsWithTransaction(1, "
"InsertPosition::eAfterSelectedCell) failed");
return EditActionHandled(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 EditActionHandled(rv);
}
if (!tblElement) {
NS_WARNING("HTMLEditor::GetCellContext() didn't return table element");
return EditActionHandled(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) {
DebugOnly<nsresult> rvIgnored = CollapseSelectionToStartOf(*cell);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::CollapseSelectionToStartOf() failed, but ignored");
}
return EditActionHandled(NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED
: NS_OK);
}
void HTMLEditor::CollapseSelectionToDeepestNonTableFirstChild(nsINode* aNode) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(aNode);
nsCOMPtr<nsINode> node = aNode;
for (nsIContent* child = node->GetFirstChild(); child;
child = child->GetFirstChild()) {
// Stop if we find a table, don't want to go into nested tables
if (HTMLEditUtils::IsTable(child) ||
!HTMLEditUtils::IsContainerNode(*child)) {
break;
}
node = child;
}
DebugOnly<nsresult> rvIgnored = CollapseSelectionToStartOf(*node);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::CollapseSelectionToStartOf() failed, but ignored");
}
nsresult HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction(
const nsAString& aSourceToInsert) {
MOZ_ASSERT(IsEditActionDataAvailable());
// don't do any post processing, rules get confused
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eReplaceHeadWithHTMLSource, nsIEditor::eNone,
ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return ignoredError.StealNSResult();
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
CommitComposition();
// Do not use AutoTopLevelEditSubActionNotifier -- rules code won't let us
// insert in <head>. Use the head node as a parent and delete/insert
// directly.
// XXX We're using AutoTopLevelEditSubActionNotifier above...
RefPtr<Document> document = GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_NOT_INITIALIZED;
}
RefPtr<nsContentList> headElementList =
document->GetElementsByTagName(u"head"_ns);
if (NS_WARN_IF(!headElementList)) {
return NS_ERROR_FAILURE;
}
RefPtr<Element> primaryHeadElement = headElementList->Item(0)->AsElement();
if (NS_WARN_IF(!primaryHeadElement)) {
return NS_ERROR_FAILURE;
}
// First, make sure there are no return chars in the source. Bad things
// happen if you insert returns (instead of dom newlines, \n) into an editor
// document.
nsAutoString inputString(aSourceToInsert);
// Windows linebreaks: Map CRLF to LF:
inputString.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
// Mac linebreaks: Map any remaining CR to LF:
inputString.ReplaceSubstring(u"\r"_ns, u"\n"_ns);
AutoPlaceholderBatch treatAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
// Get the first range in the selection, for context:
RefPtr<const nsRange> range = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!range)) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
RefPtr<DocumentFragment> documentFragment =
range->CreateContextualFragment(inputString, error);
// XXXX BUG 50965: This is not returning the text between <title>...</title>
// Special code is needed in JS to handle title anyway, so it doesn't matter!
if (error.Failed()) {
NS_WARNING("nsRange::CreateContextualFragment() failed");
return error.StealNSResult();
}
if (NS_WARN_IF(!documentFragment)) {
NS_WARNING(
"nsRange::CreateContextualFragment() didn't create DocumentFragment");
return NS_ERROR_FAILURE;
}
// First delete all children in head
while (nsCOMPtr<nsIContent> child = primaryHeadElement->GetFirstChild()) {
nsresult rv = DeleteNodeWithTransaction(*child);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
return rv;
}
}
// Now insert the new nodes
int32_t offsetOfNewNode = 0;
// Loop over the contents of the fragment and move into the document
while (nsCOMPtr<nsIContent> child = documentFragment->GetFirstChild()) {
nsresult rv = InsertNodeWithTransaction(
*child, EditorDOMPoint(primaryHeadElement, offsetOfNewNode++));
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return rv;
}
}
return NS_OK;
}
NS_IMETHODIMP HTMLEditor::RebuildDocumentFromSource(
const nsAString& aSourceString) {
CommitComposition();
AutoEditActionDataSetter editActionData(*this, EditAction::eSetHTML);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
RefPtr<Element> rootElement = GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_NULL_POINTER;
}
// Find where the <body> tag starts.
nsReadingIterator<char16_t> beginbody;
nsReadingIterator<char16_t> endbody;
aSourceString.BeginReading(beginbody);
aSourceString.EndReading(endbody);
bool foundbody =
CaseInsensitiveFindInReadable(u"<body"_ns, beginbody, endbody);
nsReadingIterator<char16_t> beginhead;
nsReadingIterator<char16_t> endhead;
aSourceString.BeginReading(beginhead);
aSourceString.EndReading(endhead);
bool foundhead =
CaseInsensitiveFindInReadable(u"<head"_ns, beginhead, endhead);
// a valid head appears before the body
if (foundbody && beginhead.get() > beginbody.get()) {
foundhead = false;
}
nsReadingIterator<char16_t> beginclosehead;
nsReadingIterator<char16_t> endclosehead;
aSourceString.BeginReading(beginclosehead);
aSourceString.EndReading(endclosehead);
// Find the index after "<head>"
bool foundclosehead = CaseInsensitiveFindInReadable(
u"</head>"_ns, beginclosehead, endclosehead);
// a valid close head appears after a found head
if (foundhead && beginhead.get() > beginclosehead.get()) {
foundclosehead = false;
}
// a valid close head appears before a found body
if (foundbody && beginclosehead.get() > beginbody.get()) {
foundclosehead = false;
}
// Time to change the document
AutoPlaceholderBatch treatAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
nsReadingIterator<char16_t> endtotal;
aSourceString.EndReading(endtotal);
if (foundhead) {
if (foundclosehead) {
nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
Substring(beginhead, beginclosehead));
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
"failed");
return rv;
}
} else if (foundbody) {
nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
Substring(beginhead, beginbody));
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
"failed");
return rv;
}
} else {
// XXX Without recourse to some parser/content sink/docshell hackery we
// don't really know where the head ends and the body begins so we assume
// that there is no body
nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
Substring(beginhead, endtotal));
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
"failed");
return rv;
}
}
} else {
nsReadingIterator<char16_t> begintotal;
aSourceString.BeginReading(begintotal);
constexpr auto head = u"<head>"_ns;
if (foundclosehead) {
nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
head + Substring(begintotal, beginclosehead));
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
"failed");
return rv;
}
} else if (foundbody) {
nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
head + Substring(begintotal, beginbody));
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
"failed");
return rv;
}
} else {
// XXX Without recourse to some parser/content sink/docshell hackery we
// don't really know where the head ends and the body begins so we assume
// that there is no head
nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(head);
if (NS_FAILED(rv)) {