diff options
Diffstat (limited to 'editor/libeditor/TextEditRules.cpp')
-rw-r--r-- | editor/libeditor/TextEditRules.cpp | 1494 |
1 files changed, 1494 insertions, 0 deletions
diff --git a/editor/libeditor/TextEditRules.cpp b/editor/libeditor/TextEditRules.cpp new file mode 100644 index 000000000..8f8f34e8b --- /dev/null +++ b/editor/libeditor/TextEditRules.cpp @@ -0,0 +1,1494 @@ +/* -*- 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 "mozilla/TextEditRules.h" + +#include "TextEditUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/NodeIterator.h" +#include "mozilla/dom/Selection.h" +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsCRTGlue.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsIDOMCharacterData.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIDOMNode.h" +#include "nsIDOMNodeFilter.h" +#include "nsIDOMNodeIterator.h" +#include "nsIDOMNodeList.h" +#include "nsIDOMText.h" +#include "nsNameSpaceManager.h" +#include "nsINode.h" +#include "nsIPlaintextEditor.h" +#include "nsISupportsBase.h" +#include "nsLiteralString.h" +#include "nsUnicharUtils.h" + +namespace mozilla { + +using namespace dom; + +#define CANCEL_OPERATION_IF_READONLY_OR_DISABLED \ + if (IsReadonly() || IsDisabled()) \ + { \ + *aCancel = true; \ + return NS_OK; \ + }; + +/******************************************************** + * mozilla::TextEditRules + ********************************************************/ + +TextEditRules::TextEditRules() + : mTextEditor(nullptr) + , mPasswordIMEIndex(0) + , mCachedSelectionOffset(0) + , mActionNesting(0) + , mLockRulesSniffing(false) + , mDidExplicitlySetInterline(false) + , mDeleteBidiImmediately(false) + , mTheAction(EditAction::none) + , mLastStart(0) + , mLastLength(0) +{ + InitFields(); +} + +void +TextEditRules::InitFields() +{ + mTextEditor = nullptr; + mPasswordText.Truncate(); + mPasswordIMEText.Truncate(); + mPasswordIMEIndex = 0; + mBogusNode = nullptr; + mCachedSelectionNode = nullptr; + mCachedSelectionOffset = 0; + mActionNesting = 0; + mLockRulesSniffing = false; + mDidExplicitlySetInterline = false; + mDeleteBidiImmediately = false; + mTheAction = EditAction::none; + mTimer = nullptr; + mLastStart = 0; + mLastLength = 0; +} + +TextEditRules::~TextEditRules() +{ + // do NOT delete mTextEditor here. We do not hold a ref count to + // mTextEditor. mTextEditor owns our lifespan. + + if (mTimer) { + mTimer->Cancel(); + } +} + +NS_IMPL_CYCLE_COLLECTION(TextEditRules, mBogusNode, mCachedSelectionNode) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextEditRules) + NS_INTERFACE_MAP_ENTRY(nsIEditRules) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditRules) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextEditRules) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextEditRules) + +NS_IMETHODIMP +TextEditRules::Init(TextEditor* aTextEditor) +{ + if (!aTextEditor) { + return NS_ERROR_NULL_POINTER; + } + + InitFields(); + + // We hold a non-refcounted reference back to our editor. + mTextEditor = aTextEditor; + RefPtr<Selection> selection = mTextEditor->GetSelection(); + NS_WARNING_ASSERTION(selection, "editor cannot get selection"); + + // Put in a magic br if needed. This method handles null selection, + // which should never happen anyway + nsresult rv = CreateBogusNodeIfNeeded(selection); + NS_ENSURE_SUCCESS(rv, rv); + + // If the selection hasn't been set up yet, set it up collapsed to the end of + // our editable content. + int32_t rangeCount; + rv = selection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, rv); + if (!rangeCount) { + rv = mTextEditor->EndOfDocument(); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (IsPlaintextEditor()) { + // ensure trailing br node + rv = CreateTrailingBRIfNeeded(); + NS_ENSURE_SUCCESS(rv, rv); + } + + mDeleteBidiImmediately = + Preferences::GetBool("bidi.edit.delete_immediately", false); + + return NS_OK; +} + +NS_IMETHODIMP +TextEditRules::SetInitialValue(const nsAString& aValue) +{ + if (IsPasswordEditor()) { + mPasswordText = aValue; + } + return NS_OK; +} + +NS_IMETHODIMP +TextEditRules::DetachEditor() +{ + if (mTimer) { + mTimer->Cancel(); + } + mTextEditor = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +TextEditRules::BeforeEdit(EditAction action, + nsIEditor::EDirection aDirection) +{ + if (mLockRulesSniffing) { + return NS_OK; + } + + AutoLockRulesSniffing lockIt(this); + mDidExplicitlySetInterline = false; + if (!mActionNesting) { + // let rules remember the top level action + mTheAction = action; + } + mActionNesting++; + + // get the selection and cache the position before editing + NS_ENSURE_STATE(mTextEditor); + RefPtr<Selection> selection = mTextEditor->GetSelection(); + NS_ENSURE_STATE(selection); + + selection->GetAnchorNode(getter_AddRefs(mCachedSelectionNode)); + selection->GetAnchorOffset(&mCachedSelectionOffset); + + return NS_OK; +} + +NS_IMETHODIMP +TextEditRules::AfterEdit(EditAction action, + nsIEditor::EDirection aDirection) +{ + if (mLockRulesSniffing) { + return NS_OK; + } + + AutoLockRulesSniffing lockIt(this); + + NS_PRECONDITION(mActionNesting>0, "bad action nesting!"); + if (!--mActionNesting) { + NS_ENSURE_STATE(mTextEditor); + RefPtr<Selection> selection = mTextEditor->GetSelection(); + NS_ENSURE_STATE(selection); + + NS_ENSURE_STATE(mTextEditor); + nsresult rv = + mTextEditor->HandleInlineSpellCheck(action, selection, + mCachedSelectionNode, + mCachedSelectionOffset, + nullptr, 0, nullptr, 0); + NS_ENSURE_SUCCESS(rv, rv); + + // if only trailing <br> remaining remove it + rv = RemoveRedundantTrailingBR(); + if (NS_FAILED(rv)) { + return rv; + } + + // detect empty doc + rv = CreateBogusNodeIfNeeded(selection); + NS_ENSURE_SUCCESS(rv, rv); + + // ensure trailing br node + rv = CreateTrailingBRIfNeeded(); + NS_ENSURE_SUCCESS(rv, rv); + + // collapse the selection to the trailing BR if it's at the end of our text node + CollapseSelectionToTrailingBRIfNeeded(selection); + } + return NS_OK; +} + +NS_IMETHODIMP +TextEditRules::WillDoAction(Selection* aSelection, + RulesInfo* aInfo, + bool* aCancel, + bool* aHandled) +{ + // null selection is legal + MOZ_ASSERT(aInfo && aCancel && aHandled); + + *aCancel = false; + *aHandled = false; + + // my kingdom for dynamic cast + TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo); + + switch (info->action) { + case EditAction::insertBreak: + UndefineCaretBidiLevel(aSelection); + return WillInsertBreak(aSelection, aCancel, aHandled, info->maxLength); + case EditAction::insertText: + case EditAction::insertIMEText: + UndefineCaretBidiLevel(aSelection); + return WillInsertText(info->action, aSelection, aCancel, aHandled, + info->inString, info->outString, info->maxLength); + case EditAction::deleteSelection: + return WillDeleteSelection(aSelection, info->collapsedAction, + aCancel, aHandled); + case EditAction::undo: + return WillUndo(aSelection, aCancel, aHandled); + case EditAction::redo: + return WillRedo(aSelection, aCancel, aHandled); + case EditAction::setTextProperty: + return WillSetTextProperty(aSelection, aCancel, aHandled); + case EditAction::removeTextProperty: + return WillRemoveTextProperty(aSelection, aCancel, aHandled); + case EditAction::outputText: + return WillOutputText(aSelection, info->outputFormat, info->outString, + aCancel, aHandled); + case EditAction::insertElement: + // i had thought this would be html rules only. but we put pre elements + // into plaintext mail when doing quoting for reply! doh! + WillInsert(*aSelection, aCancel); + return NS_OK; + default: + return NS_ERROR_FAILURE; + } +} + +NS_IMETHODIMP +TextEditRules::DidDoAction(Selection* aSelection, + RulesInfo* aInfo, + nsresult aResult) +{ + NS_ENSURE_STATE(mTextEditor); + // don't let any txns in here move the selection around behind our back. + // Note that this won't prevent explicit selection setting from working. + AutoTransactionsConserveSelection dontSpazMySelection(mTextEditor); + + NS_ENSURE_TRUE(aSelection && aInfo, NS_ERROR_NULL_POINTER); + + // my kingdom for dynamic cast + TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo); + + switch (info->action) { + case EditAction::insertBreak: + return DidInsertBreak(aSelection, aResult); + case EditAction::insertText: + case EditAction::insertIMEText: + return DidInsertText(aSelection, aResult); + case EditAction::deleteSelection: + return DidDeleteSelection(aSelection, info->collapsedAction, aResult); + case EditAction::undo: + return DidUndo(aSelection, aResult); + case EditAction::redo: + return DidRedo(aSelection, aResult); + case EditAction::setTextProperty: + return DidSetTextProperty(aSelection, aResult); + case EditAction::removeTextProperty: + return DidRemoveTextProperty(aSelection, aResult); + case EditAction::outputText: + return DidOutputText(aSelection, aResult); + default: + // Don't fail on transactions we don't handle here! + return NS_OK; + } +} + +NS_IMETHODIMP +TextEditRules::DocumentIsEmpty(bool* aDocumentIsEmpty) +{ + NS_ENSURE_TRUE(aDocumentIsEmpty, NS_ERROR_NULL_POINTER); + + *aDocumentIsEmpty = (mBogusNode != nullptr); + return NS_OK; +} + +void +TextEditRules::WillInsert(Selection& aSelection, bool* aCancel) +{ + MOZ_ASSERT(aCancel); + + if (IsReadonly() || IsDisabled()) { + *aCancel = true; + return; + } + + // initialize out param + *aCancel = false; + + // check for the magic content node and delete it if it exists + if (mBogusNode) { + NS_ENSURE_TRUE_VOID(mTextEditor); + mTextEditor->DeleteNode(mBogusNode); + mBogusNode = nullptr; + } +} + +nsresult +TextEditRules::DidInsert(Selection* aSelection, + nsresult aResult) +{ + return NS_OK; +} + +nsresult +TextEditRules::WillInsertBreak(Selection* aSelection, + bool* aCancel, + bool* aHandled, + int32_t aMaxLength) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + CANCEL_OPERATION_IF_READONLY_OR_DISABLED + *aHandled = false; + if (IsSingleLineEditor()) { + *aCancel = true; + } else { + // handle docs with a max length + // NOTE, this function copies inString into outString for us. + NS_NAMED_LITERAL_STRING(inString, "\n"); + nsAutoString outString; + bool didTruncate; + nsresult rv = TruncateInsertionIfNeeded(aSelection, &inString, &outString, + aMaxLength, &didTruncate); + NS_ENSURE_SUCCESS(rv, rv); + if (didTruncate) { + *aCancel = true; + return NS_OK; + } + + *aCancel = false; + + // if the selection isn't collapsed, delete it. + bool bCollapsed; + rv = aSelection->GetIsCollapsed(&bCollapsed); + NS_ENSURE_SUCCESS(rv, rv); + if (!bCollapsed) { + NS_ENSURE_STATE(mTextEditor); + rv = mTextEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + WillInsert(*aSelection, aCancel); + // initialize out param + // we want to ignore result of WillInsert() + *aCancel = false; + } + return NS_OK; +} + +nsresult +TextEditRules::DidInsertBreak(Selection* aSelection, + nsresult aResult) +{ + return NS_OK; +} + +nsresult +TextEditRules::CollapseSelectionToTrailingBRIfNeeded(Selection* aSelection) +{ + // we only need to execute the stuff below if we are a plaintext editor. + // html editors have a different mechanism for putting in mozBR's + // (because there are a bunch more places you have to worry about it in html) + if (!IsPlaintextEditor()) { + return NS_OK; + } + + NS_ENSURE_STATE(mTextEditor); + + // If there is no selection ranges, we should set to the end of the editor. + // This is usually performed in TextEditRules::Init(), however, if the + // editor is reframed, this may be called by AfterEdit(). + if (!aSelection->RangeCount()) { + mTextEditor->EndOfDocument(); + } + + // if we are at the end of the textarea, we need to set the + // selection to stick to the mozBR at the end of the textarea. + int32_t selOffset; + nsCOMPtr<nsIDOMNode> selNode; + nsresult rv = + mTextEditor->GetStartNodeAndOffset(aSelection, + getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMText> nodeAsText = do_QueryInterface(selNode); + if (!nodeAsText) { + return NS_OK; // Nothing to do if we're not at a text node. + } + + uint32_t length; + rv = nodeAsText->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + + // nothing to do if we're not at the end of the text node + if (selOffset != int32_t(length)) { + return NS_OK; + } + + int32_t parentOffset; + nsCOMPtr<nsIDOMNode> parentNode = + EditorBase::GetNodeLocation(selNode, &parentOffset); + + NS_ENSURE_STATE(mTextEditor); + nsCOMPtr<nsIDOMNode> root = do_QueryInterface(mTextEditor->GetRoot()); + NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER); + if (parentNode != root) { + return NS_OK; + } + + nsCOMPtr<nsIDOMNode> nextNode = mTextEditor->GetChildAt(parentNode, + parentOffset + 1); + if (nextNode && TextEditUtils::IsMozBR(nextNode)) { + rv = aSelection->Collapse(parentNode, parentOffset + 1); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +static inline already_AddRefed<nsIDOMNode> +GetTextNode(Selection* selection, + EditorBase* editor) +{ + int32_t selOffset; + nsCOMPtr<nsIDOMNode> selNode; + nsresult rv = + editor->GetStartNodeAndOffset(selection, + getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, nullptr); + if (!editor->IsTextNode(selNode)) { + // Get an nsINode from the nsIDOMNode + nsCOMPtr<nsINode> node = do_QueryInterface(selNode); + // if node is null, return it to indicate there's no text + NS_ENSURE_TRUE(node, nullptr); + // This should be the root node, walk the tree looking for text nodes + RefPtr<NodeIterator> iter = + new NodeIterator(node, nsIDOMNodeFilter::SHOW_TEXT, NodeFilterHolder()); + while (!editor->IsTextNode(selNode)) { + if (NS_FAILED(iter->NextNode(getter_AddRefs(selNode))) || !selNode) { + return nullptr; + } + } + } + return selNode.forget(); +} +#ifdef DEBUG +#define ASSERT_PASSWORD_LENGTHS_EQUAL() \ + if (IsPasswordEditor() && mTextEditor->GetRoot()) { \ + int32_t txtLen; \ + mTextEditor->GetTextLength(&txtLen); \ + NS_ASSERTION(mPasswordText.Length() == uint32_t(txtLen), \ + "password length not equal to number of asterisks"); \ + } +#else +#define ASSERT_PASSWORD_LENGTHS_EQUAL() +#endif + +// static +void +TextEditRules::HandleNewLines(nsString& aString, + int32_t aNewlineHandling) +{ + if (aNewlineHandling < 0) { + int32_t caretStyle; + TextEditor::GetDefaultEditorPrefs(aNewlineHandling, caretStyle); + } + + switch(aNewlineHandling) { + case nsIPlaintextEditor::eNewlinesReplaceWithSpaces: + // Strip trailing newlines first so we don't wind up with trailing spaces + aString.Trim(CRLF, false, true); + aString.ReplaceChar(CRLF, ' '); + break; + case nsIPlaintextEditor::eNewlinesStrip: + aString.StripChars(CRLF); + break; + case nsIPlaintextEditor::eNewlinesPasteToFirst: + default: { + int32_t firstCRLF = aString.FindCharInSet(CRLF); + + // we get first *non-empty* line. + int32_t offset = 0; + while (firstCRLF == offset) { + offset++; + firstCRLF = aString.FindCharInSet(CRLF, offset); + } + if (firstCRLF > 0) { + aString.Truncate(firstCRLF); + } + if (offset > 0) { + aString.Cut(0, offset); + } + break; + } + case nsIPlaintextEditor::eNewlinesReplaceWithCommas: + aString.Trim(CRLF, true, true); + aString.ReplaceChar(CRLF, ','); + break; + case nsIPlaintextEditor::eNewlinesStripSurroundingWhitespace: { + nsAutoString result; + uint32_t offset = 0; + while (offset < aString.Length()) { + int32_t nextCRLF = aString.FindCharInSet(CRLF, offset); + if (nextCRLF < 0) { + result.Append(nsDependentSubstring(aString, offset)); + break; + } + uint32_t wsBegin = nextCRLF; + // look backwards for the first non-whitespace char + while (wsBegin > offset && NS_IS_SPACE(aString[wsBegin - 1])) { + --wsBegin; + } + result.Append(nsDependentSubstring(aString, offset, wsBegin - offset)); + offset = nextCRLF + 1; + while (offset < aString.Length() && NS_IS_SPACE(aString[offset])) { + ++offset; + } + } + aString = result; + break; + } + case nsIPlaintextEditor::eNewlinesPasteIntact: + // even if we're pasting newlines, don't paste leading/trailing ones + aString.Trim(CRLF, true, true); + break; + } +} + +nsresult +TextEditRules::WillInsertText(EditAction aAction, + Selection* aSelection, + bool* aCancel, + bool* aHandled, + const nsAString* inString, + nsAString* outString, + int32_t aMaxLength) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + + if (inString->IsEmpty() && aAction != EditAction::insertIMEText) { + // HACK: this is a fix for bug 19395 + // I can't outlaw all empty insertions + // because IME transaction depend on them + // There is more work to do to make the + // world safe for IME. + *aCancel = true; + *aHandled = false; + return NS_OK; + } + + // initialize out param + *aCancel = false; + *aHandled = true; + + // handle docs with a max length + // NOTE, this function copies inString into outString for us. + bool truncated = false; + nsresult rv = TruncateInsertionIfNeeded(aSelection, inString, outString, + aMaxLength, &truncated); + NS_ENSURE_SUCCESS(rv, rv); + // If we're exceeding the maxlength when composing IME, we need to clean up + // the composing text, so we shouldn't return early. + if (truncated && outString->IsEmpty() && + aAction != EditAction::insertIMEText) { + *aCancel = true; + return NS_OK; + } + + int32_t start = 0; + int32_t end = 0; + + // handle password field docs + if (IsPasswordEditor()) { + NS_ENSURE_STATE(mTextEditor); + nsContentUtils::GetSelectionInTextControl(aSelection, + mTextEditor->GetRoot(), + start, end); + } + + // if the selection isn't collapsed, delete it. + bool bCollapsed; + rv = aSelection->GetIsCollapsed(&bCollapsed); + NS_ENSURE_SUCCESS(rv, rv); + if (!bCollapsed) { + NS_ENSURE_STATE(mTextEditor); + rv = mTextEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + WillInsert(*aSelection, aCancel); + // initialize out param + // we want to ignore result of WillInsert() + *aCancel = false; + + // handle password field data + // this has the side effect of changing all the characters in aOutString + // to the replacement character + if (IsPasswordEditor() && + aAction == EditAction::insertIMEText) { + RemoveIMETextFromPWBuf(start, outString); + } + + // People have lots of different ideas about what text fields + // should do with multiline pastes. See bugs 21032, 23485, 23485, 50935. + // The six possible options are: + // 0. paste newlines intact + // 1. paste up to the first newline (default) + // 2. replace newlines with spaces + // 3. strip newlines + // 4. replace with commas + // 5. strip newlines and surrounding whitespace + // So find out what we're expected to do: + if (IsSingleLineEditor()) { + nsAutoString tString(*outString); + + NS_ENSURE_STATE(mTextEditor); + HandleNewLines(tString, mTextEditor->mNewlineHandling); + + outString->Assign(tString); + } + + if (IsPasswordEditor()) { + // manage the password buffer + mPasswordText.Insert(*outString, start); + + if (LookAndFeel::GetEchoPassword() && !DontEchoPassword()) { + HideLastPWInput(); + mLastStart = start; + mLastLength = outString->Length(); + if (mTimer) { + mTimer->Cancel(); + } else { + mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + mTimer->InitWithCallback(this, LookAndFeel::GetPasswordMaskDelay(), + nsITimer::TYPE_ONE_SHOT); + } else { + FillBufWithPWChars(outString, outString->Length()); + } + } + + // get the (collapsed) selection location + NS_ENSURE_STATE(aSelection->GetRangeAt(0)); + nsCOMPtr<nsINode> selNode = aSelection->GetRangeAt(0)->GetStartParent(); + int32_t selOffset = aSelection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_STATE(selNode); + + // don't put text in places that can't have it + NS_ENSURE_STATE(mTextEditor); + if (!mTextEditor->IsTextNode(selNode) && + !mTextEditor->CanContainTag(*selNode, *nsGkAtoms::textTagName)) { + return NS_ERROR_FAILURE; + } + + // we need to get the doc + NS_ENSURE_STATE(mTextEditor); + nsCOMPtr<nsIDocument> doc = mTextEditor->GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + + if (aAction == EditAction::insertIMEText) { + NS_ENSURE_STATE(mTextEditor); + // Find better insertion point to insert text. + mTextEditor->FindBetterInsertionPoint(selNode, selOffset); + // If there is one or more IME selections, its minimum offset should be + // the insertion point. + int32_t IMESelectionOffset = + mTextEditor->GetIMESelectionStartOffsetIn(selNode); + if (IMESelectionOffset >= 0) { + selOffset = IMESelectionOffset; + } + rv = mTextEditor->InsertTextImpl(*outString, address_of(selNode), + &selOffset, doc); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // aAction == EditAction::insertText; find where we are + nsCOMPtr<nsINode> curNode = selNode; + int32_t curOffset = selOffset; + + // don't spaz my selection in subtransactions + NS_ENSURE_STATE(mTextEditor); + AutoTransactionsConserveSelection dontSpazMySelection(mTextEditor); + + rv = mTextEditor->InsertTextImpl(*outString, address_of(curNode), + &curOffset, doc); + NS_ENSURE_SUCCESS(rv, rv); + + if (curNode) { + // Make the caret attach to the inserted text, unless this text ends with a LF, + // in which case make the caret attach to the next line. + bool endsWithLF = + !outString->IsEmpty() && outString->Last() == nsCRT::LF; + aSelection->SetInterlinePosition(endsWithLF); + + aSelection->Collapse(curNode, curOffset); + } + } + ASSERT_PASSWORD_LENGTHS_EQUAL() + return NS_OK; +} + +nsresult +TextEditRules::DidInsertText(Selection* aSelection, + nsresult aResult) +{ + return DidInsert(aSelection, aResult); +} + +nsresult +TextEditRules::WillSetTextProperty(Selection* aSelection, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + + // XXX: should probably return a success value other than NS_OK that means "not allowed" + if (IsPlaintextEditor()) { + *aCancel = true; + } + return NS_OK; +} + +nsresult +TextEditRules::DidSetTextProperty(Selection* aSelection, + nsresult aResult) +{ + return NS_OK; +} + +nsresult +TextEditRules::WillRemoveTextProperty(Selection* aSelection, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + + // XXX: should probably return a success value other than NS_OK that means "not allowed" + if (IsPlaintextEditor()) { + *aCancel = true; + } + return NS_OK; +} + +nsresult +TextEditRules::DidRemoveTextProperty(Selection* aSelection, + nsresult aResult) +{ + return NS_OK; +} + +nsresult +TextEditRules::WillDeleteSelection(Selection* aSelection, + nsIEditor::EDirection aCollapsedAction, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + CANCEL_OPERATION_IF_READONLY_OR_DISABLED + + // initialize out param + *aCancel = false; + *aHandled = false; + + // if there is only bogus content, cancel the operation + if (mBogusNode) { + *aCancel = true; + return NS_OK; + } + + // If the current selection is empty (e.g the user presses backspace with + // a collapsed selection), then we want to avoid sending the selectstart + // event to the user, so we hide selection changes. However, we still + // want to send a single selectionchange event to the document, so we + // batch the selectionchange events, such that a single event fires after + // the AutoHideSelectionChanges destructor has been run. + SelectionBatcher selectionBatcher(aSelection); + AutoHideSelectionChanges hideSelection(aSelection); + nsAutoScriptBlocker scriptBlocker; + + if (IsPasswordEditor()) { + NS_ENSURE_STATE(mTextEditor); + nsresult rv = + mTextEditor->ExtendSelectionForDelete(aSelection, &aCollapsedAction); + NS_ENSURE_SUCCESS(rv, rv); + + // manage the password buffer + int32_t start, end; + nsContentUtils::GetSelectionInTextControl(aSelection, + mTextEditor->GetRoot(), + start, end); + + if (LookAndFeel::GetEchoPassword()) { + HideLastPWInput(); + mLastStart = start; + mLastLength = 0; + if (mTimer) { + mTimer->Cancel(); + } + } + + // Collapsed selection. + if (end == start) { + // Deleting back. + if (nsIEditor::ePrevious == aCollapsedAction && 0<start) { + mPasswordText.Cut(start-1, 1); + } + // Deleting forward. + else if (nsIEditor::eNext == aCollapsedAction) { + mPasswordText.Cut(start, 1); + } + // Otherwise nothing to do for this collapsed selection. + } + // Extended selection. + else { + mPasswordText.Cut(start, end-start); + } + } else { + nsCOMPtr<nsIDOMNode> startNode; + int32_t startOffset; + NS_ENSURE_STATE(mTextEditor); + nsresult rv = + mTextEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(startNode), + &startOffset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); + + bool bCollapsed; + rv = aSelection->GetIsCollapsed(&bCollapsed); + NS_ENSURE_SUCCESS(rv, rv); + + if (!bCollapsed) { + return NS_OK; + } + + // Test for distance between caret and text that will be deleted + rv = CheckBidiLevelForDeletion(aSelection, startNode, startOffset, + aCollapsedAction, aCancel); + NS_ENSURE_SUCCESS(rv, rv); + if (*aCancel) { + return NS_OK; + } + + NS_ENSURE_STATE(mTextEditor); + rv = mTextEditor->ExtendSelectionForDelete(aSelection, &aCollapsedAction); + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_ENSURE_STATE(mTextEditor); + nsresult rv = + mTextEditor->DeleteSelectionImpl(aCollapsedAction, nsIEditor::eStrip); + NS_ENSURE_SUCCESS(rv, rv); + + *aHandled = true; + ASSERT_PASSWORD_LENGTHS_EQUAL() + return NS_OK; +} + +nsresult +TextEditRules::DidDeleteSelection(Selection* aSelection, + nsIEditor::EDirection aCollapsedAction, + nsresult aResult) +{ + nsCOMPtr<nsIDOMNode> startNode; + int32_t startOffset; + NS_ENSURE_STATE(mTextEditor); + nsresult rv = + mTextEditor->GetStartNodeAndOffset(aSelection, + getter_AddRefs(startNode), &startOffset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); + + // delete empty text nodes at selection + if (mTextEditor->IsTextNode(startNode)) { + nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(startNode); + uint32_t strLength; + rv = textNode->GetLength(&strLength); + NS_ENSURE_SUCCESS(rv, rv); + + // are we in an empty text node? + if (!strLength) { + rv = mTextEditor->DeleteNode(startNode); + NS_ENSURE_SUCCESS(rv, rv); + } + } + if (mDidExplicitlySetInterline) { + return NS_OK; + } + // We prevent the caret from sticking on the left of prior BR + // (i.e. the end of previous line) after this deletion. Bug 92124 + return aSelection->SetInterlinePosition(true); +} + +nsresult +TextEditRules::WillUndo(Selection* aSelection, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + CANCEL_OPERATION_IF_READONLY_OR_DISABLED + // initialize out param + *aCancel = false; + *aHandled = false; + return NS_OK; +} + +/** + * The idea here is to see if the magic empty node has suddenly reappeared as + * the result of the undo. If it has, set our state so we remember it. + * There is a tradeoff between doing here and at redo, or doing it everywhere + * else that might care. Since undo and redo are relatively rare, it makes + * sense to take the (small) performance hit here. + */ +nsresult +TextEditRules::DidUndo(Selection* aSelection, + nsresult aResult) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + // If aResult is an error, we return it. + NS_ENSURE_SUCCESS(aResult, aResult); + + NS_ENSURE_STATE(mTextEditor); + dom::Element* theRoot = mTextEditor->GetRoot(); + NS_ENSURE_TRUE(theRoot, NS_ERROR_FAILURE); + nsIContent* node = mTextEditor->GetLeftmostChild(theRoot); + if (node && mTextEditor->IsMozEditorBogusNode(node)) { + mBogusNode = do_QueryInterface(node); + } else { + mBogusNode = nullptr; + } + return aResult; +} + +nsresult +TextEditRules::WillRedo(Selection* aSelection, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + CANCEL_OPERATION_IF_READONLY_OR_DISABLED + // initialize out param + *aCancel = false; + *aHandled = false; + return NS_OK; +} + +nsresult +TextEditRules::DidRedo(Selection* aSelection, + nsresult aResult) +{ + if (!aSelection) { + return NS_ERROR_NULL_POINTER; + } + if (NS_FAILED(aResult)) { + return aResult; // if aResult is an error, we return it. + } + + NS_ENSURE_STATE(mTextEditor); + nsCOMPtr<nsIDOMElement> theRoot = do_QueryInterface(mTextEditor->GetRoot()); + NS_ENSURE_TRUE(theRoot, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMHTMLCollection> nodeList; + nsresult rv = theRoot->GetElementsByTagName(NS_LITERAL_STRING("br"), + getter_AddRefs(nodeList)); + NS_ENSURE_SUCCESS(rv, rv); + if (nodeList) { + uint32_t len; + nodeList->GetLength(&len); + + if (len != 1) { + // only in the case of one br could there be the bogus node + mBogusNode = nullptr; + return NS_OK; + } + + nsCOMPtr<nsIDOMNode> node; + nodeList->Item(0, getter_AddRefs(node)); + nsCOMPtr<nsIContent> content = do_QueryInterface(node); + MOZ_ASSERT(content); + if (mTextEditor->IsMozEditorBogusNode(content)) { + mBogusNode = node; + } else { + mBogusNode = nullptr; + } + } + return NS_OK; +} + +nsresult +TextEditRules::WillOutputText(Selection* aSelection, + const nsAString* aOutputFormat, + nsAString* aOutString, + bool* aCancel, + bool* aHandled) +{ + // null selection ok + if (!aOutString || !aOutputFormat || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + + // initialize out param + *aCancel = false; + *aHandled = false; + + nsAutoString outputFormat(*aOutputFormat); + ToLowerCase(outputFormat); + if (outputFormat.EqualsLiteral("text/plain")) { + // Only use these rules for plain text output. + if (IsPasswordEditor()) { + *aOutString = mPasswordText; + *aHandled = true; + } else if (mBogusNode) { + // This means there's no content, so output null string. + aOutString->Truncate(); + *aHandled = true; + } + } + return NS_OK; +} + +nsresult +TextEditRules::DidOutputText(Selection* aSelection, + nsresult aResult) +{ + return NS_OK; +} + +nsresult +TextEditRules::RemoveRedundantTrailingBR() +{ + // If the bogus node exists, we have no work to do + if (mBogusNode) { + return NS_OK; + } + + // Likewise, nothing to be done if we could never have inserted a trailing br + if (IsSingleLineEditor()) { + return NS_OK; + } + + NS_ENSURE_STATE(mTextEditor); + RefPtr<dom::Element> body = mTextEditor->GetRoot(); + if (!body) { + return NS_ERROR_NULL_POINTER; + } + + uint32_t childCount = body->GetChildCount(); + if (childCount > 1) { + // The trailing br is redundant if it is the only remaining child node + return NS_OK; + } + + RefPtr<nsIContent> child = body->GetFirstChild(); + if (!child || !child->IsElement()) { + return NS_OK; + } + + dom::Element* elem = child->AsElement(); + if (!TextEditUtils::IsMozBR(elem)) { + return NS_OK; + } + + // Rather than deleting this node from the DOM tree we should instead + // morph this br into the bogus node + elem->UnsetAttr(kNameSpaceID_None, nsGkAtoms::type, true); + + // set mBogusNode to be this <br> + mBogusNode = do_QueryInterface(elem); + + // give it the bogus node attribute + elem->SetAttr(kNameSpaceID_None, kMOZEditorBogusNodeAttrAtom, + kMOZEditorBogusNodeValue, false); + return NS_OK; +} + +nsresult +TextEditRules::CreateTrailingBRIfNeeded() +{ + // but only if we aren't a single line edit field + if (IsSingleLineEditor()) { + return NS_OK; + } + + NS_ENSURE_STATE(mTextEditor); + dom::Element* body = mTextEditor->GetRoot(); + NS_ENSURE_TRUE(body, NS_ERROR_NULL_POINTER); + + nsIContent* lastChild = body->GetLastChild(); + // assuming CreateBogusNodeIfNeeded() has been called first + NS_ENSURE_TRUE(lastChild, NS_ERROR_NULL_POINTER); + + if (!lastChild->IsHTMLElement(nsGkAtoms::br)) { + AutoTransactionsConserveSelection dontSpazMySelection(mTextEditor); + nsCOMPtr<nsIDOMNode> domBody = do_QueryInterface(body); + return CreateMozBR(domBody, body->Length()); + } + + // Check to see if the trailing BR is a former bogus node - this will have + // stuck around if we previously morphed a trailing node into a bogus node. + if (!mTextEditor->IsMozEditorBogusNode(lastChild)) { + return NS_OK; + } + + // Morph it back to a mozBR + lastChild->UnsetAttr(kNameSpaceID_None, kMOZEditorBogusNodeAttrAtom, false); + lastChild->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("_moz"), true); + return NS_OK; +} + +nsresult +TextEditRules::CreateBogusNodeIfNeeded(Selection* aSelection) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(mTextEditor, NS_ERROR_NULL_POINTER); + + if (mBogusNode) { + // Let's not create more than one, ok? + return NS_OK; + } + + // tell rules system to not do any post-processing + AutoRules beginRulesSniffing(mTextEditor, EditAction::ignore, + nsIEditor::eNone); + + nsCOMPtr<dom::Element> body = mTextEditor->GetRoot(); + if (!body) { + // We don't even have a body yet, don't insert any bogus nodes at + // this point. + return NS_OK; + } + + // Now we've got the body element. Iterate over the body element's children, + // looking for editable content. If no editable content is found, insert the + // bogus node. + for (nsCOMPtr<nsIContent> bodyChild = body->GetFirstChild(); + bodyChild; + bodyChild = bodyChild->GetNextSibling()) { + if (mTextEditor->IsMozEditorBogusNode(bodyChild) || + !mTextEditor->IsEditable(body) || // XXX hoist out of the loop? + mTextEditor->IsEditable(bodyChild) || + mTextEditor->IsBlockNode(bodyChild)) { + return NS_OK; + } + } + + // Skip adding the bogus node if body is read-only. + if (!mTextEditor->IsModifiableNode(body)) { + return NS_OK; + } + + // Create a br. + nsCOMPtr<Element> newContent = mTextEditor->CreateHTMLContent(nsGkAtoms::br); + NS_ENSURE_STATE(newContent); + + // set mBogusNode to be the newly created <br> + mBogusNode = do_QueryInterface(newContent); + NS_ENSURE_TRUE(mBogusNode, NS_ERROR_NULL_POINTER); + + // Give it a special attribute. + newContent->SetAttr(kNameSpaceID_None, kMOZEditorBogusNodeAttrAtom, + kMOZEditorBogusNodeValue, false); + + // Put the node in the document. + nsCOMPtr<nsIDOMNode> bodyNode = do_QueryInterface(body); + nsresult rv = mTextEditor->InsertNode(mBogusNode, bodyNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + + // Set selection. + aSelection->CollapseNative(body, 0); + return NS_OK; +} + + +nsresult +TextEditRules::TruncateInsertionIfNeeded(Selection* aSelection, + const nsAString* aInString, + nsAString* aOutString, + int32_t aMaxLength, + bool* aTruncated) +{ + if (!aSelection || !aInString || !aOutString) { + return NS_ERROR_NULL_POINTER; + } + + if (!aOutString->Assign(*aInString, mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + if (aTruncated) { + *aTruncated = false; + } + + NS_ENSURE_STATE(mTextEditor); + if (-1 != aMaxLength && IsPlaintextEditor() && + !mTextEditor->IsIMEComposing()) { + // Get the current text length. + // Get the length of inString. + // Get the length of the selection. + // If selection is collapsed, it is length 0. + // Subtract the length of the selection from the len(doc) + // since we'll delete the selection on insert. + // This is resultingDocLength. + // Get old length of IME composing string + // which will be replaced by new one. + // If (resultingDocLength) is at or over max, cancel the insert + // If (resultingDocLength) + (length of input) > max, + // set aOutString to subset of inString so length = max + int32_t docLength; + nsresult rv = mTextEditor->GetTextLength(&docLength); + if (NS_FAILED(rv)) { + return rv; + } + + int32_t start, end; + nsContentUtils::GetSelectionInTextControl(aSelection, + mTextEditor->GetRoot(), + start, end); + + TextComposition* composition = mTextEditor->GetComposition(); + int32_t oldCompStrLength = composition ? composition->String().Length() : 0; + + const int32_t selectionLength = end - start; + const int32_t resultingDocLength = docLength - selectionLength - oldCompStrLength; + if (resultingDocLength >= aMaxLength) { + // This call is guaranteed to reduce the capacity of the string, so it + // cannot cause an OOM. + aOutString->Truncate(); + if (aTruncated) { + *aTruncated = true; + } + } else { + int32_t oldLength = aOutString->Length(); + if (oldLength + resultingDocLength > aMaxLength) { + int32_t newLength = aMaxLength - resultingDocLength; + MOZ_ASSERT(newLength > 0); + char16_t newLastChar = aOutString->CharAt(newLength - 1); + char16_t removingFirstChar = aOutString->CharAt(newLength); + // Don't separate the string between a surrogate pair. + if (NS_IS_HIGH_SURROGATE(newLastChar) && + NS_IS_LOW_SURROGATE(removingFirstChar)) { + newLength--; + } + // XXX What should we do if we're removing IVS and its preceding + // character won't be removed? + // This call is guaranteed to reduce the capacity of the string, so it + // cannot cause an OOM. + aOutString->Truncate(newLength); + if (aTruncated) { + *aTruncated = true; + } + } + } + } + return NS_OK; +} + +void +TextEditRules::ResetIMETextPWBuf() +{ + mPasswordIMEText.Truncate(); +} + +void +TextEditRules::RemoveIMETextFromPWBuf(int32_t& aStart, + nsAString* aIMEString) +{ + MOZ_ASSERT(aIMEString); + + // initialize PasswordIME + if (mPasswordIMEText.IsEmpty()) { + mPasswordIMEIndex = aStart; + } else { + // manage the password buffer + mPasswordText.Cut(mPasswordIMEIndex, mPasswordIMEText.Length()); + aStart = mPasswordIMEIndex; + } + + mPasswordIMEText.Assign(*aIMEString); +} + +NS_IMETHODIMP +TextEditRules::Notify(nsITimer* aTimer) +{ + MOZ_ASSERT(mTimer); + + // Check whether our text editor's password flag was changed before this + // "hide password character" timer actually fires. + nsresult rv = IsPasswordEditor() ? HideLastPWInput() : NS_OK; + ASSERT_PASSWORD_LENGTHS_EQUAL(); + mLastLength = 0; + return rv; +} + +nsresult +TextEditRules::HideLastPWInput() +{ + if (!mLastLength) { + // Special case, we're trying to replace a range that no longer exists + return NS_OK; + } + + nsAutoString hiddenText; + FillBufWithPWChars(&hiddenText, mLastLength); + + NS_ENSURE_STATE(mTextEditor); + RefPtr<Selection> selection = mTextEditor->GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + int32_t start, end; + nsContentUtils::GetSelectionInTextControl(selection, mTextEditor->GetRoot(), + start, end); + + nsCOMPtr<nsIDOMNode> selNode = GetTextNode(selection, mTextEditor); + NS_ENSURE_TRUE(selNode, NS_OK); + + nsCOMPtr<nsIDOMCharacterData> nodeAsText(do_QueryInterface(selNode)); + NS_ENSURE_TRUE(nodeAsText, NS_OK); + + nodeAsText->ReplaceData(mLastStart, mLastLength, hiddenText); + selection->Collapse(selNode, start); + if (start != end) { + selection->Extend(selNode, end); + } + return NS_OK; +} + +// static +void +TextEditRules::FillBufWithPWChars(nsAString* aOutString, + int32_t aLength) +{ + MOZ_ASSERT(aOutString); + + // change the output to the platform password character + char16_t passwordChar = LookAndFeel::GetPasswordCharacter(); + + aOutString->Truncate(); + for (int32_t i = 0; i < aLength; i++) { + aOutString->Append(passwordChar); + } +} + +/** + * CreateMozBR() puts a BR node with moz attribute at {inParent, inOffset}. + */ +nsresult +TextEditRules::CreateMozBR(nsIDOMNode* inParent, + int32_t inOffset, + nsIDOMNode** outBRNode) +{ + NS_ENSURE_TRUE(inParent, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMNode> brNode; + NS_ENSURE_STATE(mTextEditor); + nsresult rv = mTextEditor->CreateBR(inParent, inOffset, address_of(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + + // give it special moz attr + nsCOMPtr<nsIDOMElement> brElem = do_QueryInterface(brNode); + if (brElem) { + rv = mTextEditor->SetAttribute(brElem, NS_LITERAL_STRING("type"), + NS_LITERAL_STRING("_moz")); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (outBRNode) { + brNode.forget(outBRNode); + } + return NS_OK; +} + +NS_IMETHODIMP +TextEditRules::DocumentModified() +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +bool +TextEditRules::IsPasswordEditor() const +{ + return mTextEditor ? mTextEditor->IsPasswordEditor() : false; +} + +bool +TextEditRules::IsSingleLineEditor() const +{ + return mTextEditor ? mTextEditor->IsSingleLineEditor() : false; +} + +bool +TextEditRules::IsPlaintextEditor() const +{ + return mTextEditor ? mTextEditor->IsPlaintextEditor() : false; +} + +bool +TextEditRules::IsReadonly() const +{ + return mTextEditor ? mTextEditor->IsReadonly() : false; +} + +bool +TextEditRules::IsDisabled() const +{ + return mTextEditor ? mTextEditor->IsDisabled() : false; +} +bool +TextEditRules::IsMailEditor() const +{ + return mTextEditor ? mTextEditor->IsMailEditor() : false; +} + +bool +TextEditRules::DontEchoPassword() const +{ + return mTextEditor ? mTextEditor->DontEchoPassword() : false; +} + +} // namespace mozilla |