diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /editor/libeditor | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'editor/libeditor')
404 files changed, 89558 insertions, 0 deletions
diff --git a/editor/libeditor/CSSEditUtils.cpp b/editor/libeditor/CSSEditUtils.cpp new file mode 100644 index 000000000..5199838c0 --- /dev/null +++ b/editor/libeditor/CSSEditUtils.cpp @@ -0,0 +1,1422 @@ +/* -*- 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/CSSEditUtils.h" + +#include "mozilla/Assertions.h" +#include "mozilla/ChangeStyleTransaction.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/Preferences.h" +#include "mozilla/DeclarationBlockInlines.h" +#include "mozilla/css/StyleRule.h" +#include "mozilla/dom/Element.h" +#include "mozilla/mozalloc.h" +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsColor.h" +#include "nsComputedDOMStyle.h" +#include "nsDebug.h" +#include "nsDependentSubstring.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsIDOMCSSStyleDeclaration.h" +#include "nsIDOMElement.h" +#include "nsIDOMNode.h" +#include "nsIDOMWindow.h" +#include "nsIDocument.h" +#include "nsIEditor.h" +#include "nsINode.h" +#include "nsISupportsImpl.h" +#include "nsISupportsUtils.h" +#include "nsLiteralString.h" +#include "nsPIDOMWindow.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsStringIterator.h" +#include "nsStyledElement.h" +#include "nsSubstringTuple.h" +#include "nsUnicharUtils.h" + +namespace mozilla { + +using namespace dom; + +static +void ProcessBValue(const nsAString* aInputString, + nsAString& aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString) +{ + if (aInputString && aInputString->EqualsLiteral("-moz-editor-invert-value")) { + aOutputString.AssignLiteral("normal"); + } + else { + aOutputString.AssignLiteral("bold"); + } +} + +static +void ProcessDefaultValue(const nsAString* aInputString, + nsAString& aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString) +{ + CopyASCIItoUTF16(aDefaultValueString, aOutputString); +} + +static +void ProcessSameValue(const nsAString* aInputString, + nsAString & aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString) +{ + if (aInputString) { + aOutputString.Assign(*aInputString); + } + else + aOutputString.Truncate(); +} + +static +void ProcessExtendedValue(const nsAString* aInputString, + nsAString& aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString) +{ + aOutputString.Truncate(); + if (aInputString) { + if (aPrependString) { + AppendASCIItoUTF16(aPrependString, aOutputString); + } + aOutputString.Append(*aInputString); + if (aAppendString) { + AppendASCIItoUTF16(aAppendString, aOutputString); + } + } +} + +static +void ProcessLengthValue(const nsAString* aInputString, + nsAString& aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString) +{ + aOutputString.Truncate(); + if (aInputString) { + aOutputString.Append(*aInputString); + if (-1 == aOutputString.FindChar(char16_t('%'))) { + aOutputString.AppendLiteral("px"); + } + } +} + +static +void ProcessListStyleTypeValue(const nsAString* aInputString, + nsAString& aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString) +{ + aOutputString.Truncate(); + if (aInputString) { + if (aInputString->EqualsLiteral("1")) { + aOutputString.AppendLiteral("decimal"); + } + else if (aInputString->EqualsLiteral("a")) { + aOutputString.AppendLiteral("lower-alpha"); + } + else if (aInputString->EqualsLiteral("A")) { + aOutputString.AppendLiteral("upper-alpha"); + } + else if (aInputString->EqualsLiteral("i")) { + aOutputString.AppendLiteral("lower-roman"); + } + else if (aInputString->EqualsLiteral("I")) { + aOutputString.AppendLiteral("upper-roman"); + } + else if (aInputString->EqualsLiteral("square") + || aInputString->EqualsLiteral("circle") + || aInputString->EqualsLiteral("disc")) { + aOutputString.Append(*aInputString); + } + } +} + +static +void ProcessMarginLeftValue(const nsAString* aInputString, + nsAString& aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString) +{ + aOutputString.Truncate(); + if (aInputString) { + if (aInputString->EqualsLiteral("center") || + aInputString->EqualsLiteral("-moz-center")) { + aOutputString.AppendLiteral("auto"); + } + else if (aInputString->EqualsLiteral("right") || + aInputString->EqualsLiteral("-moz-right")) { + aOutputString.AppendLiteral("auto"); + } + else { + aOutputString.AppendLiteral("0px"); + } + } +} + +static +void ProcessMarginRightValue(const nsAString* aInputString, + nsAString& aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString) +{ + aOutputString.Truncate(); + if (aInputString) { + if (aInputString->EqualsLiteral("center") || + aInputString->EqualsLiteral("-moz-center")) { + aOutputString.AppendLiteral("auto"); + } + else if (aInputString->EqualsLiteral("left") || + aInputString->EqualsLiteral("-moz-left")) { + aOutputString.AppendLiteral("auto"); + } + else { + aOutputString.AppendLiteral("0px"); + } + } +} + +const CSSEditUtils::CSSEquivTable boldEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_font_weight, ProcessBValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable italicEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_font_style, ProcessDefaultValue, "italic", nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable underlineEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_text_decoration, ProcessDefaultValue, "underline", nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable strikeEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_text_decoration, ProcessDefaultValue, "line-through", nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable ttEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_font_family, ProcessDefaultValue, "monospace", nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable fontColorEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_color, ProcessSameValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable fontFaceEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_font_family, ProcessSameValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable bgcolorEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_background_color, ProcessSameValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable backgroundImageEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_background_image, ProcessExtendedValue, nullptr, "url(", ")", true, true }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable textColorEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_color, ProcessSameValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable borderEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_border, ProcessExtendedValue, nullptr, nullptr, "px solid", true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable textAlignEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_text_align, ProcessSameValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable captionAlignEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_caption_side, ProcessSameValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable verticalAlignEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_vertical_align, ProcessSameValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable nowrapEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_whitespace, ProcessDefaultValue, "nowrap", nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable widthEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_width, ProcessLengthValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable heightEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_height, ProcessLengthValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable listStyleTypeEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_list_style_type, ProcessListStyleTypeValue, nullptr, nullptr, nullptr, true, true }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable tableAlignEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_text_align, ProcessDefaultValue, "left", nullptr, nullptr, false, false }, + { CSSEditUtils::eCSSEditableProperty_margin_left, ProcessMarginLeftValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_margin_right, ProcessMarginRightValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +const CSSEditUtils::CSSEquivTable hrAlignEquivTable[] = { + { CSSEditUtils::eCSSEditableProperty_margin_left, ProcessMarginLeftValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_margin_right, ProcessMarginRightValue, nullptr, nullptr, nullptr, true, false }, + { CSSEditUtils::eCSSEditableProperty_NONE, 0 } +}; + +CSSEditUtils::CSSEditUtils(HTMLEditor* aHTMLEditor) + : mHTMLEditor(aHTMLEditor) + , mIsCSSPrefChecked(true) +{ + // let's retrieve the value of the "CSS editing" pref + mIsCSSPrefChecked = Preferences::GetBool("editor.use_css", mIsCSSPrefChecked); +} + +CSSEditUtils::~CSSEditUtils() +{ +} + +// Answers true if we have some CSS equivalence for the HTML style defined +// by aProperty and/or aAttribute for the node aNode +bool +CSSEditUtils::IsCSSEditableProperty(nsINode* aNode, + nsIAtom* aProperty, + const nsAString* aAttribute) +{ + MOZ_ASSERT(aNode); + + nsINode* node = aNode; + // we need an element node here + if (node->NodeType() == nsIDOMNode::TEXT_NODE) { + node = node->GetParentNode(); + NS_ENSURE_TRUE(node, false); + } + + // html inline styles B I TT U STRIKE and COLOR/FACE on FONT + if (nsGkAtoms::b == aProperty || + nsGkAtoms::i == aProperty || + nsGkAtoms::tt == aProperty || + nsGkAtoms::u == aProperty || + nsGkAtoms::strike == aProperty || + (nsGkAtoms::font == aProperty && aAttribute && + (aAttribute->EqualsLiteral("color") || + aAttribute->EqualsLiteral("face")))) { + return true; + } + + // ALIGN attribute on elements supporting it + if (aAttribute && (aAttribute->EqualsLiteral("align")) && + node->IsAnyOfHTMLElements(nsGkAtoms::div, + nsGkAtoms::p, + nsGkAtoms::h1, + nsGkAtoms::h2, + nsGkAtoms::h3, + nsGkAtoms::h4, + nsGkAtoms::h5, + nsGkAtoms::h6, + nsGkAtoms::td, + nsGkAtoms::th, + nsGkAtoms::table, + nsGkAtoms::hr, + // For the above, why not use + // HTMLEditUtils::SupportsAlignAttr? + // It also checks for tbody, tfoot, thead. + // Let's add the following elements here even + // if "align" has a different meaning for them + nsGkAtoms::legend, + nsGkAtoms::caption)) { + return true; + } + + if (aAttribute && (aAttribute->EqualsLiteral("valign")) && + node->IsAnyOfHTMLElements(nsGkAtoms::col, + nsGkAtoms::colgroup, + nsGkAtoms::tbody, + nsGkAtoms::td, + nsGkAtoms::th, + nsGkAtoms::tfoot, + nsGkAtoms::thead, + nsGkAtoms::tr)) { + return true; + } + + // attributes TEXT, BACKGROUND and BGCOLOR on BODY + if (aAttribute && node->IsHTMLElement(nsGkAtoms::body) && + (aAttribute->EqualsLiteral("text") + || aAttribute->EqualsLiteral("background") + || aAttribute->EqualsLiteral("bgcolor"))) { + return true; + } + + // attribute BGCOLOR on other elements + if (aAttribute && aAttribute->EqualsLiteral("bgcolor")) { + return true; + } + + // attributes HEIGHT, WIDTH and NOWRAP on TD and TH + if (aAttribute && + node->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th) && + (aAttribute->EqualsLiteral("height") + || aAttribute->EqualsLiteral("width") + || aAttribute->EqualsLiteral("nowrap"))) { + return true; + } + + // attributes HEIGHT and WIDTH on TABLE + if (aAttribute && node->IsHTMLElement(nsGkAtoms::table) && + (aAttribute->EqualsLiteral("height") + || aAttribute->EqualsLiteral("width"))) { + return true; + } + + // attributes SIZE and WIDTH on HR + if (aAttribute && node->IsHTMLElement(nsGkAtoms::hr) && + (aAttribute->EqualsLiteral("size") + || aAttribute->EqualsLiteral("width"))) { + return true; + } + + // attribute TYPE on OL UL LI + if (aAttribute && + node->IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul, + nsGkAtoms::li) && + aAttribute->EqualsLiteral("type")) { + return true; + } + + if (aAttribute && node->IsHTMLElement(nsGkAtoms::img) && + (aAttribute->EqualsLiteral("border") + || aAttribute->EqualsLiteral("width") + || aAttribute->EqualsLiteral("height"))) { + return true; + } + + // other elements that we can align using CSS even if they + // can't carry the html ALIGN attribute + if (aAttribute && aAttribute->EqualsLiteral("align") && + node->IsAnyOfHTMLElements(nsGkAtoms::ul, + nsGkAtoms::ol, + nsGkAtoms::dl, + nsGkAtoms::li, + nsGkAtoms::dd, + nsGkAtoms::dt, + nsGkAtoms::address, + nsGkAtoms::pre)) { + return true; + } + + return false; +} + +// The lowest level above the transaction; adds the CSS declaration +// "aProperty : aValue" to the inline styles carried by aElement +nsresult +CSSEditUtils::SetCSSProperty(Element& aElement, + nsIAtom& aProperty, + const nsAString& aValue, + bool aSuppressTxn) +{ + RefPtr<ChangeStyleTransaction> transaction = + CreateCSSPropertyTxn(aElement, aProperty, aValue, + ChangeStyleTransaction::eSet); + if (aSuppressTxn) { + return transaction->DoTransaction(); + } + return mHTMLEditor->DoTransaction(transaction); +} + +nsresult +CSSEditUtils::SetCSSPropertyPixels(Element& aElement, + nsIAtom& aProperty, + int32_t aIntValue) +{ + nsAutoString s; + s.AppendInt(aIntValue); + return SetCSSProperty(aElement, aProperty, s + NS_LITERAL_STRING("px"), + /* suppress txn */ false); +} + +// The lowest level above the transaction; removes the value aValue from the +// list of values specified for the CSS property aProperty, or totally remove +// the declaration if this property accepts only one value +nsresult +CSSEditUtils::RemoveCSSProperty(Element& aElement, + nsIAtom& aProperty, + const nsAString& aValue, + bool aSuppressTxn) +{ + RefPtr<ChangeStyleTransaction> transaction = + CreateCSSPropertyTxn(aElement, aProperty, aValue, + ChangeStyleTransaction::eRemove); + if (aSuppressTxn) { + return transaction->DoTransaction(); + } + return mHTMLEditor->DoTransaction(transaction); +} + +already_AddRefed<ChangeStyleTransaction> +CSSEditUtils::CreateCSSPropertyTxn( + Element& aElement, + nsIAtom& aAttribute, + const nsAString& aValue, + ChangeStyleTransaction::EChangeType aChangeType) +{ + RefPtr<ChangeStyleTransaction> transaction = + new ChangeStyleTransaction(aElement, aAttribute, aValue, aChangeType); + return transaction.forget(); +} + +nsresult +CSSEditUtils::GetSpecifiedProperty(nsINode& aNode, + nsIAtom& aProperty, + nsAString& aValue) +{ + return GetCSSInlinePropertyBase(&aNode, &aProperty, aValue, eSpecified); +} + +nsresult +CSSEditUtils::GetComputedProperty(nsINode& aNode, + nsIAtom& aProperty, + nsAString& aValue) +{ + return GetCSSInlinePropertyBase(&aNode, &aProperty, aValue, eComputed); +} + +nsresult +CSSEditUtils::GetCSSInlinePropertyBase(nsINode* aNode, + nsIAtom* aProperty, + nsAString& aValue, + StyleType aStyleType) +{ + MOZ_ASSERT(aNode && aProperty); + aValue.Truncate(); + + nsCOMPtr<Element> element = GetElementContainerOrSelf(aNode); + NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER); + + if (aStyleType == eComputed) { + // Get the all the computed css styles attached to the element node + RefPtr<nsComputedDOMStyle> cssDecl = GetComputedStyle(element); + NS_ENSURE_STATE(cssDecl); + + // from these declarations, get the one we want and that one only + MOZ_ALWAYS_SUCCEEDS( + cssDecl->GetPropertyValue(nsDependentAtomString(aProperty), aValue)); + + return NS_OK; + } + + MOZ_ASSERT(aStyleType == eSpecified); + RefPtr<DeclarationBlock> decl = element->GetInlineStyleDeclaration(); + if (!decl) { + return NS_OK; + } + if (decl->IsServo()) { + MOZ_CRASH("stylo: not implemented"); + return NS_ERROR_NOT_IMPLEMENTED; + } + nsCSSPropertyID prop = + nsCSSProps::LookupProperty(nsDependentAtomString(aProperty), + CSSEnabledState::eForAllContent); + MOZ_ASSERT(prop != eCSSProperty_UNKNOWN); + decl->AsGecko()->GetPropertyValueByID(prop, aValue); + + return NS_OK; +} + +already_AddRefed<nsComputedDOMStyle> +CSSEditUtils::GetComputedStyle(Element* aElement) +{ + MOZ_ASSERT(aElement); + + nsIDocument* doc = aElement->GetUncomposedDoc(); + NS_ENSURE_TRUE(doc, nullptr); + + nsIPresShell* presShell = doc->GetShell(); + NS_ENSURE_TRUE(presShell, nullptr); + + RefPtr<nsComputedDOMStyle> style = + NS_NewComputedDOMStyle(aElement, EmptyString(), presShell); + + return style.forget(); +} + +// remove the CSS style "aProperty : aPropertyValue" and possibly remove the whole node +// if it is a span and if its only attribute is _moz_dirty +nsresult +CSSEditUtils::RemoveCSSInlineStyle(nsIDOMNode* aNode, + nsIAtom* aProperty, + const nsAString& aPropertyValue) +{ + nsCOMPtr<Element> element = do_QueryInterface(aNode); + NS_ENSURE_STATE(element); + + // remove the property from the style attribute + nsresult rv = RemoveCSSProperty(*element, *aProperty, aPropertyValue); + NS_ENSURE_SUCCESS(rv, rv); + + if (!element->IsHTMLElement(nsGkAtoms::span) || + HTMLEditor::HasAttributes(element)) { + return NS_OK; + } + + return mHTMLEditor->RemoveContainer(element); +} + +// Answers true if the property can be removed by setting a "none" CSS value +// on a node +bool +CSSEditUtils::IsCSSInvertible(nsIAtom& aProperty, + const nsAString* aAttribute) +{ + return nsGkAtoms::b == &aProperty; +} + +// Get the default browser background color if we need it for GetCSSBackgroundColorState +void +CSSEditUtils::GetDefaultBackgroundColor(nsAString& aColor) +{ + if (Preferences::GetBool("editor.use_custom_colors", false)) { + nsresult rv = Preferences::GetString("editor.background_color", &aColor); + // XXX Why don't you validate the pref value? + if (NS_FAILED(rv)) { + NS_WARNING("failed to get editor.background_color"); + aColor.AssignLiteral("#ffffff"); // Default to white + } + return; + } + + if (Preferences::GetBool("browser.display.use_system_colors", false)) { + return; + } + + nsresult rv = + Preferences::GetString("browser.display.background_color", &aColor); + // XXX Why don't you validate the pref value? + if (NS_FAILED(rv)) { + NS_WARNING("failed to get browser.display.background_color"); + aColor.AssignLiteral("#ffffff"); // Default to white + } +} + +// Get the default length unit used for CSS Indent/Outdent +void +CSSEditUtils::GetDefaultLengthUnit(nsAString& aLengthUnit) +{ + nsresult rv = + Preferences::GetString("editor.css.default_length_unit", &aLengthUnit); + // XXX Why don't you validate the pref value? + if (NS_FAILED(rv)) { + aLengthUnit.AssignLiteral("px"); + } +} + +// Unfortunately, CSSStyleDeclaration::GetPropertyCSSValue is not yet +// implemented... We need then a way to determine the number part and the unit +// from aString, aString being the result of a GetPropertyValue query... +void +CSSEditUtils::ParseLength(const nsAString& aString, + float* aValue, + nsIAtom** aUnit) +{ + if (aString.IsEmpty()) { + *aValue = 0; + *aUnit = NS_Atomize(aString).take(); + return; + } + + nsAString::const_iterator iter; + aString.BeginReading(iter); + + float a = 10.0f , b = 1.0f, value = 0; + int8_t sign = 1; + int32_t i = 0, j = aString.Length(); + char16_t c; + bool floatingPointFound = false; + c = *iter; + if (char16_t('-') == c) { sign = -1; iter++; i++; } + else if (char16_t('+') == c) { iter++; i++; } + while (i < j) { + c = *iter; + if ((char16_t('0') == c) || + (char16_t('1') == c) || + (char16_t('2') == c) || + (char16_t('3') == c) || + (char16_t('4') == c) || + (char16_t('5') == c) || + (char16_t('6') == c) || + (char16_t('7') == c) || + (char16_t('8') == c) || + (char16_t('9') == c)) { + value = (value * a) + (b * (c - char16_t('0'))); + b = b / 10 * a; + } + else if (!floatingPointFound && (char16_t('.') == c)) { + floatingPointFound = true; + a = 1.0f; b = 0.1f; + } + else break; + iter++; + i++; + } + *aValue = value * sign; + *aUnit = NS_Atomize(StringTail(aString, j-i)).take(); +} + +void +CSSEditUtils::GetCSSPropertyAtom(nsCSSEditableProperty aProperty, + nsIAtom** aAtom) +{ + *aAtom = nullptr; + switch (aProperty) { + case eCSSEditableProperty_background_color: + *aAtom = nsGkAtoms::backgroundColor; + break; + case eCSSEditableProperty_background_image: + *aAtom = nsGkAtoms::background_image; + break; + case eCSSEditableProperty_border: + *aAtom = nsGkAtoms::border; + break; + case eCSSEditableProperty_caption_side: + *aAtom = nsGkAtoms::caption_side; + break; + case eCSSEditableProperty_color: + *aAtom = nsGkAtoms::color; + break; + case eCSSEditableProperty_float: + *aAtom = nsGkAtoms::_float; + break; + case eCSSEditableProperty_font_family: + *aAtom = nsGkAtoms::font_family; + break; + case eCSSEditableProperty_font_size: + *aAtom = nsGkAtoms::font_size; + break; + case eCSSEditableProperty_font_style: + *aAtom = nsGkAtoms::font_style; + break; + case eCSSEditableProperty_font_weight: + *aAtom = nsGkAtoms::fontWeight; + break; + case eCSSEditableProperty_height: + *aAtom = nsGkAtoms::height; + break; + case eCSSEditableProperty_list_style_type: + *aAtom = nsGkAtoms::list_style_type; + break; + case eCSSEditableProperty_margin_left: + *aAtom = nsGkAtoms::marginLeft; + break; + case eCSSEditableProperty_margin_right: + *aAtom = nsGkAtoms::marginRight; + break; + case eCSSEditableProperty_text_align: + *aAtom = nsGkAtoms::textAlign; + break; + case eCSSEditableProperty_text_decoration: + *aAtom = nsGkAtoms::text_decoration; + break; + case eCSSEditableProperty_vertical_align: + *aAtom = nsGkAtoms::vertical_align; + break; + case eCSSEditableProperty_whitespace: + *aAtom = nsGkAtoms::white_space; + break; + case eCSSEditableProperty_width: + *aAtom = nsGkAtoms::width; + break; + case eCSSEditableProperty_NONE: + // intentionally empty + break; + } +} + +// Populate aProperty and aValueArray with the CSS declarations equivalent to the +// value aValue according to the equivalence table aEquivTable +void +CSSEditUtils::BuildCSSDeclarations(nsTArray<nsIAtom*>& aPropertyArray, + nsTArray<nsString>& aValueArray, + const CSSEquivTable* aEquivTable, + const nsAString* aValue, + bool aGetOrRemoveRequest) +{ + // clear arrays + aPropertyArray.Clear(); + aValueArray.Clear(); + + // if we have an input value, let's use it + nsAutoString value, lowerCasedValue; + if (aValue) { + value.Assign(*aValue); + lowerCasedValue.Assign(*aValue); + ToLowerCase(lowerCasedValue); + } + + int8_t index = 0; + nsCSSEditableProperty cssProperty = aEquivTable[index].cssProperty; + while (cssProperty) { + if (!aGetOrRemoveRequest|| aEquivTable[index].gettable) { + nsAutoString cssValue, cssPropertyString; + nsIAtom * cssPropertyAtom; + // find the equivalent css value for the index-th property in + // the equivalence table + (*aEquivTable[index].processValueFunctor) ((!aGetOrRemoveRequest || aEquivTable[index].caseSensitiveValue) ? &value : &lowerCasedValue, + cssValue, + aEquivTable[index].defaultValue, + aEquivTable[index].prependValue, + aEquivTable[index].appendValue); + GetCSSPropertyAtom(cssProperty, &cssPropertyAtom); + aPropertyArray.AppendElement(cssPropertyAtom); + aValueArray.AppendElement(cssValue); + } + index++; + cssProperty = aEquivTable[index].cssProperty; + } +} + +// Populate cssPropertyArray and cssValueArray with the declarations equivalent +// to aHTMLProperty/aAttribute/aValue for the node aNode +void +CSSEditUtils::GenerateCSSDeclarationsFromHTMLStyle( + Element* aElement, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + const nsAString* aValue, + nsTArray<nsIAtom*>& cssPropertyArray, + nsTArray<nsString>& cssValueArray, + bool aGetOrRemoveRequest) +{ + MOZ_ASSERT(aElement); + const CSSEditUtils::CSSEquivTable* equivTable = nullptr; + + if (nsGkAtoms::b == aHTMLProperty) { + equivTable = boldEquivTable; + } else if (nsGkAtoms::i == aHTMLProperty) { + equivTable = italicEquivTable; + } else if (nsGkAtoms::u == aHTMLProperty) { + equivTable = underlineEquivTable; + } else if (nsGkAtoms::strike == aHTMLProperty) { + equivTable = strikeEquivTable; + } else if (nsGkAtoms::tt == aHTMLProperty) { + equivTable = ttEquivTable; + } else if (aAttribute) { + if (nsGkAtoms::font == aHTMLProperty && + aAttribute->EqualsLiteral("color")) { + equivTable = fontColorEquivTable; + } else if (nsGkAtoms::font == aHTMLProperty && + aAttribute->EqualsLiteral("face")) { + equivTable = fontFaceEquivTable; + } else if (aAttribute->EqualsLiteral("bgcolor")) { + equivTable = bgcolorEquivTable; + } else if (aAttribute->EqualsLiteral("background")) { + equivTable = backgroundImageEquivTable; + } else if (aAttribute->EqualsLiteral("text")) { + equivTable = textColorEquivTable; + } else if (aAttribute->EqualsLiteral("border")) { + equivTable = borderEquivTable; + } else if (aAttribute->EqualsLiteral("align")) { + if (aElement->IsHTMLElement(nsGkAtoms::table)) { + equivTable = tableAlignEquivTable; + } else if (aElement->IsHTMLElement(nsGkAtoms::hr)) { + equivTable = hrAlignEquivTable; + } else if (aElement->IsAnyOfHTMLElements(nsGkAtoms::legend, + nsGkAtoms::caption)) { + equivTable = captionAlignEquivTable; + } else { + equivTable = textAlignEquivTable; + } + } else if (aAttribute->EqualsLiteral("valign")) { + equivTable = verticalAlignEquivTable; + } else if (aAttribute->EqualsLiteral("nowrap")) { + equivTable = nowrapEquivTable; + } else if (aAttribute->EqualsLiteral("width")) { + equivTable = widthEquivTable; + } else if (aAttribute->EqualsLiteral("height") || + (aElement->IsHTMLElement(nsGkAtoms::hr) && + aAttribute->EqualsLiteral("size"))) { + equivTable = heightEquivTable; + } else if (aAttribute->EqualsLiteral("type") && + aElement->IsAnyOfHTMLElements(nsGkAtoms::ol, + nsGkAtoms::ul, + nsGkAtoms::li)) { + equivTable = listStyleTypeEquivTable; + } + } + if (equivTable) { + BuildCSSDeclarations(cssPropertyArray, cssValueArray, equivTable, + aValue, aGetOrRemoveRequest); + } +} + +// Add to aNode the CSS inline style equivalent to HTMLProperty/aAttribute/ +// aValue for the node, and return in aCount the number of CSS properties set +// by the call. The Element version returns aCount instead. +int32_t +CSSEditUtils::SetCSSEquivalentToHTMLStyle(Element* aElement, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool aSuppressTransaction) +{ + MOZ_ASSERT(aElement && aProperty); + MOZ_ASSERT_IF(aAttribute, aValue); + int32_t count; + // This can only fail if SetCSSProperty fails, which should only happen if + // something is pretty badly wrong. In this case we assert so that hopefully + // someone will notice, but there's nothing more sensible to do than just + // return the count and carry on. + nsresult rv = SetCSSEquivalentToHTMLStyle(aElement->AsDOMNode(), + aProperty, aAttribute, + aValue, &count, + aSuppressTransaction); + NS_ASSERTION(NS_SUCCEEDED(rv), "SetCSSEquivalentToHTMLStyle failed"); + NS_ENSURE_SUCCESS(rv, count); + return count; +} + +nsresult +CSSEditUtils::SetCSSEquivalentToHTMLStyle(nsIDOMNode* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + const nsAString* aValue, + int32_t* aCount, + bool aSuppressTransaction) +{ + nsCOMPtr<Element> element = do_QueryInterface(aNode); + *aCount = 0; + if (!element || !IsCSSEditableProperty(element, aHTMLProperty, aAttribute)) { + return NS_OK; + } + + // we can apply the styles only if the node is an element and if we have + // an equivalence for the requested HTML style in this implementation + + // Find the CSS equivalence to the HTML style + nsTArray<nsIAtom*> cssPropertyArray; + nsTArray<nsString> cssValueArray; + GenerateCSSDeclarationsFromHTMLStyle(element, aHTMLProperty, aAttribute, + aValue, cssPropertyArray, cssValueArray, + false); + + // set the individual CSS inline styles + *aCount = cssPropertyArray.Length(); + for (int32_t index = 0; index < *aCount; index++) { + nsresult rv = SetCSSProperty(*element, *cssPropertyArray[index], + cssValueArray[index], aSuppressTransaction); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +// Remove from aNode the CSS inline style equivalent to HTMLProperty/aAttribute/aValue for the node +nsresult +CSSEditUtils::RemoveCSSEquivalentToHTMLStyle(nsIDOMNode* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool aSuppressTransaction) +{ + nsCOMPtr<Element> element = do_QueryInterface(aNode); + NS_ENSURE_TRUE(element, NS_OK); + + return RemoveCSSEquivalentToHTMLStyle(element, aHTMLProperty, aAttribute, + aValue, aSuppressTransaction); +} + +nsresult +CSSEditUtils::RemoveCSSEquivalentToHTMLStyle(Element* aElement, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool aSuppressTransaction) +{ + MOZ_ASSERT(aElement); + + if (!IsCSSEditableProperty(aElement, aHTMLProperty, aAttribute)) { + return NS_OK; + } + + // we can apply the styles only if the node is an element and if we have + // an equivalence for the requested HTML style in this implementation + + // Find the CSS equivalence to the HTML style + nsTArray<nsIAtom*> cssPropertyArray; + nsTArray<nsString> cssValueArray; + GenerateCSSDeclarationsFromHTMLStyle(aElement, aHTMLProperty, aAttribute, + aValue, cssPropertyArray, cssValueArray, + true); + + // remove the individual CSS inline styles + int32_t count = cssPropertyArray.Length(); + for (int32_t index = 0; index < count; index++) { + nsresult rv = RemoveCSSProperty(*aElement, + *cssPropertyArray[index], + cssValueArray[index], + aSuppressTransaction); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +// returns in aValueString the list of values for the CSS equivalences to +// the HTML style aHTMLProperty/aAttribute/aValueString for the node aNode; +// the value of aStyleType controls the styles we retrieve : specified or +// computed. +nsresult +CSSEditUtils::GetCSSEquivalentToHTMLInlineStyleSet(nsINode* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + nsAString& aValueString, + StyleType aStyleType) +{ + aValueString.Truncate(); + nsCOMPtr<Element> theElement = GetElementContainerOrSelf(aNode); + NS_ENSURE_TRUE(theElement, NS_ERROR_NULL_POINTER); + + if (!theElement || !IsCSSEditableProperty(theElement, aHTMLProperty, aAttribute)) { + return NS_OK; + } + + // Yes, the requested HTML style has a CSS equivalence in this implementation + nsTArray<nsIAtom*> cssPropertyArray; + nsTArray<nsString> cssValueArray; + // get the CSS equivalence with last param true indicating we want only the + // "gettable" properties + GenerateCSSDeclarationsFromHTMLStyle(theElement, aHTMLProperty, aAttribute, nullptr, + cssPropertyArray, cssValueArray, true); + int32_t count = cssPropertyArray.Length(); + for (int32_t index = 0; index < count; index++) { + nsAutoString valueString; + // retrieve the specified/computed value of the property + nsresult rv = GetCSSInlinePropertyBase(theElement, cssPropertyArray[index], + valueString, aStyleType); + NS_ENSURE_SUCCESS(rv, rv); + // append the value to aValueString (possibly with a leading whitespace) + if (index) { + aValueString.Append(char16_t(' ')); + } + aValueString.Append(valueString); + } + return NS_OK; +} + +// Does the node aNode (or its parent, if it's not an element node) have a CSS +// style equivalent to the HTML style aHTMLProperty/aHTMLAttribute/valueString? +// The value of aStyleType controls the styles we retrieve: specified or +// computed. The return value aIsSet is true if the CSS styles are set. +// +// The nsIContent variant returns aIsSet instead of using an out parameter, and +// does not modify aValue. +bool +CSSEditUtils::IsCSSEquivalentToHTMLInlineStyleSet(nsINode* aNode, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString& aValue, + StyleType aStyleType) +{ + // Use aValue as only an in param, not in-out + nsAutoString value(aValue); + return IsCSSEquivalentToHTMLInlineStyleSet(aNode, aProperty, aAttribute, + value, aStyleType); +} + +bool +CSSEditUtils::IsCSSEquivalentToHTMLInlineStyleSet(nsINode* aNode, + nsIAtom* aProperty, + const nsAString* aAttribute, + nsAString& aValue, + StyleType aStyleType) +{ + MOZ_ASSERT(aNode && aProperty); + bool isSet; + nsresult rv = IsCSSEquivalentToHTMLInlineStyleSet(aNode->AsDOMNode(), + aProperty, aAttribute, + isSet, aValue, aStyleType); + NS_ENSURE_SUCCESS(rv, false); + return isSet; +} + +nsresult +CSSEditUtils::IsCSSEquivalentToHTMLInlineStyleSet( + nsIDOMNode* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aHTMLAttribute, + bool& aIsSet, + nsAString& valueString, + StyleType aStyleType) +{ + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + + nsAutoString htmlValueString(valueString); + aIsSet = false; + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + do { + valueString.Assign(htmlValueString); + // get the value of the CSS equivalent styles + nsresult rv = + GetCSSEquivalentToHTMLInlineStyleSet(node, aHTMLProperty, aHTMLAttribute, + valueString, aStyleType); + NS_ENSURE_SUCCESS(rv, rv); + + // early way out if we can + if (valueString.IsEmpty()) { + return NS_OK; + } + + if (nsGkAtoms::b == aHTMLProperty) { + if (valueString.EqualsLiteral("bold")) { + aIsSet = true; + } else if (valueString.EqualsLiteral("normal")) { + aIsSet = false; + } else if (valueString.EqualsLiteral("bolder")) { + aIsSet = true; + valueString.AssignLiteral("bold"); + } else { + int32_t weight = 0; + nsresult errorCode; + nsAutoString value(valueString); + weight = value.ToInteger(&errorCode); + if (400 < weight) { + aIsSet = true; + valueString.AssignLiteral("bold"); + } else { + aIsSet = false; + valueString.AssignLiteral("normal"); + } + } + } else if (nsGkAtoms::i == aHTMLProperty) { + if (valueString.EqualsLiteral("italic") || + valueString.EqualsLiteral("oblique")) { + aIsSet = true; + } + } else if (nsGkAtoms::u == aHTMLProperty) { + nsAutoString val; + val.AssignLiteral("underline"); + aIsSet = ChangeStyleTransaction::ValueIncludes(valueString, val); + } else if (nsGkAtoms::strike == aHTMLProperty) { + nsAutoString val; + val.AssignLiteral("line-through"); + aIsSet = ChangeStyleTransaction::ValueIncludes(valueString, val); + } else if (aHTMLAttribute && + ((nsGkAtoms::font == aHTMLProperty && + aHTMLAttribute->EqualsLiteral("color")) || + aHTMLAttribute->EqualsLiteral("bgcolor"))) { + if (htmlValueString.IsEmpty()) { + aIsSet = true; + } else { + nscolor rgba; + nsAutoString subStr; + htmlValueString.Right(subStr, htmlValueString.Length() - 1); + if (NS_ColorNameToRGB(htmlValueString, &rgba) || + NS_HexToRGBA(subStr, nsHexColorType::NoAlpha, &rgba)) { + nsAutoString htmlColor, tmpStr; + + if (NS_GET_A(rgba) != 255) { + // This should only be hit by the "transparent" keyword, which + // currently serializes to "transparent" (not "rgba(0, 0, 0, 0)"). + MOZ_ASSERT(NS_GET_R(rgba) == 0 && NS_GET_G(rgba) == 0 && + NS_GET_B(rgba) == 0 && NS_GET_A(rgba) == 0); + htmlColor.AppendLiteral("transparent"); + } else { + htmlColor.AppendLiteral("rgb("); + + NS_NAMED_LITERAL_STRING(comma, ", "); + + tmpStr.AppendInt(NS_GET_R(rgba), 10); + htmlColor.Append(tmpStr + comma); + + tmpStr.Truncate(); + tmpStr.AppendInt(NS_GET_G(rgba), 10); + htmlColor.Append(tmpStr + comma); + + tmpStr.Truncate(); + tmpStr.AppendInt(NS_GET_B(rgba), 10); + htmlColor.Append(tmpStr); + + htmlColor.Append(char16_t(')')); + } + + aIsSet = htmlColor.Equals(valueString, + nsCaseInsensitiveStringComparator()); + } else { + aIsSet = htmlValueString.Equals(valueString, + nsCaseInsensitiveStringComparator()); + } + } + } else if (nsGkAtoms::tt == aHTMLProperty) { + aIsSet = StringBeginsWith(valueString, NS_LITERAL_STRING("monospace")); + } else if (nsGkAtoms::font == aHTMLProperty && aHTMLAttribute && + aHTMLAttribute->EqualsLiteral("face")) { + if (!htmlValueString.IsEmpty()) { + const char16_t commaSpace[] = { char16_t(','), char16_t(' '), 0 }; + const char16_t comma[] = { char16_t(','), 0 }; + htmlValueString.ReplaceSubstring(commaSpace, comma); + nsAutoString valueStringNorm(valueString); + valueStringNorm.ReplaceSubstring(commaSpace, comma); + aIsSet = htmlValueString.Equals(valueStringNorm, + nsCaseInsensitiveStringComparator()); + } else { + aIsSet = true; + } + return NS_OK; + } else if (aHTMLAttribute && aHTMLAttribute->EqualsLiteral("align")) { + aIsSet = true; + } else { + aIsSet = false; + return NS_OK; + } + + if (!htmlValueString.IsEmpty() && + htmlValueString.Equals(valueString, + nsCaseInsensitiveStringComparator())) { + aIsSet = true; + } + + if (htmlValueString.EqualsLiteral("-moz-editor-invert-value")) { + aIsSet = !aIsSet; + } + + if (nsGkAtoms::u == aHTMLProperty || nsGkAtoms::strike == aHTMLProperty) { + // unfortunately, the value of the text-decoration property is not inherited. + // that means that we have to look at ancestors of node to see if they are underlined + node = node->GetParentElement(); // set to null if it's not a dom element + } + } while ((nsGkAtoms::u == aHTMLProperty || + nsGkAtoms::strike == aHTMLProperty) && !aIsSet && node); + return NS_OK; +} + +void +CSSEditUtils::SetCSSEnabled(bool aIsCSSPrefChecked) +{ + mIsCSSPrefChecked = aIsCSSPrefChecked; +} + +bool +CSSEditUtils::IsCSSPrefChecked() +{ + return mIsCSSPrefChecked ; +} + +// ElementsSameStyle compares two elements and checks if they have the same +// specified CSS declarations in the STYLE attribute +// The answer is always negative if at least one of them carries an ID or a class +bool +CSSEditUtils::ElementsSameStyle(nsIDOMNode* aFirstNode, + nsIDOMNode* aSecondNode) +{ + nsCOMPtr<Element> firstElement = do_QueryInterface(aFirstNode); + nsCOMPtr<Element> secondElement = do_QueryInterface(aSecondNode); + + NS_ASSERTION((firstElement && secondElement), "Non element nodes passed to ElementsSameStyle."); + NS_ENSURE_TRUE(firstElement, false); + NS_ENSURE_TRUE(secondElement, false); + + return ElementsSameStyle(firstElement, secondElement); +} + +bool +CSSEditUtils::ElementsSameStyle(Element* aFirstElement, + Element* aSecondElement) +{ + MOZ_ASSERT(aFirstElement); + MOZ_ASSERT(aSecondElement); + + if (aFirstElement->HasAttr(kNameSpaceID_None, nsGkAtoms::id) || + aSecondElement->HasAttr(kNameSpaceID_None, nsGkAtoms::id)) { + // at least one of the spans carries an ID ; suspect a CSS rule applies to it and + // refuse to merge the nodes + return false; + } + + nsAutoString firstClass, secondClass; + bool isFirstClassSet = aFirstElement->GetAttr(kNameSpaceID_None, nsGkAtoms::_class, firstClass); + bool isSecondClassSet = aSecondElement->GetAttr(kNameSpaceID_None, nsGkAtoms::_class, secondClass); + if (isFirstClassSet && isSecondClassSet) { + // both spans carry a class, let's compare them + if (!firstClass.Equals(secondClass)) { + // WARNING : technically, the comparison just above is questionable : + // from a pure HTML/CSS point of view class="a b" is NOT the same than + // class="b a" because a CSS rule could test the exact value of the class + // attribute to be "a b" for instance ; from a user's point of view, a + // wysiwyg editor should probably NOT make any difference. CSS people + // need to discuss this issue before any modification. + return false; + } + } else if (isFirstClassSet || isSecondClassSet) { + // one span only carries a class, early way out + return false; + } + + nsCOMPtr<nsIDOMCSSStyleDeclaration> firstCSSDecl, secondCSSDecl; + uint32_t firstLength, secondLength; + nsresult rv = GetInlineStyles(aFirstElement, getter_AddRefs(firstCSSDecl), &firstLength); + if (NS_FAILED(rv) || !firstCSSDecl) { + return false; + } + rv = GetInlineStyles(aSecondElement, getter_AddRefs(secondCSSDecl), &secondLength); + if (NS_FAILED(rv) || !secondCSSDecl) { + return false; + } + + if (firstLength != secondLength) { + // early way out if we can + return false; + } + + if (!firstLength) { + // no inline style ! + return true; + } + + nsAutoString propertyNameString; + nsAutoString firstValue, secondValue; + for (uint32_t i = 0; i < firstLength; i++) { + firstCSSDecl->Item(i, propertyNameString); + firstCSSDecl->GetPropertyValue(propertyNameString, firstValue); + secondCSSDecl->GetPropertyValue(propertyNameString, secondValue); + if (!firstValue.Equals(secondValue)) { + return false; + } + } + for (uint32_t i = 0; i < secondLength; i++) { + secondCSSDecl->Item(i, propertyNameString); + secondCSSDecl->GetPropertyValue(propertyNameString, secondValue); + firstCSSDecl->GetPropertyValue(propertyNameString, firstValue); + if (!firstValue.Equals(secondValue)) { + return false; + } + } + + return true; +} + +nsresult +CSSEditUtils::GetInlineStyles(Element* aElement, + nsIDOMCSSStyleDeclaration** aCssDecl, + uint32_t* aLength) +{ + return GetInlineStyles(static_cast<nsISupports*>(aElement), aCssDecl, aLength); +} + +nsresult +CSSEditUtils::GetInlineStyles(nsIDOMElement* aElement, + nsIDOMCSSStyleDeclaration** aCssDecl, + uint32_t* aLength) +{ + return GetInlineStyles(static_cast<nsISupports*>(aElement), aCssDecl, aLength); +} + +nsresult +CSSEditUtils::GetInlineStyles(nsISupports* aElement, + nsIDOMCSSStyleDeclaration** aCssDecl, + uint32_t* aLength) +{ + NS_ENSURE_TRUE(aElement && aLength, NS_ERROR_NULL_POINTER); + *aLength = 0; + nsCOMPtr<nsStyledElement> inlineStyles = do_QueryInterface(aElement); + NS_ENSURE_TRUE(inlineStyles, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMCSSStyleDeclaration> cssDecl = + do_QueryInterface(inlineStyles->Style()); + MOZ_ASSERT(cssDecl); + + cssDecl.forget(aCssDecl); + (*aCssDecl)->GetLength(aLength); + return NS_OK; +} + +already_AddRefed<nsIDOMElement> +CSSEditUtils::GetElementContainerOrSelf(nsIDOMNode* aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, nullptr); + nsCOMPtr<nsIDOMElement> element = + do_QueryInterface(GetElementContainerOrSelf(node)); + return element.forget(); +} + +Element* +CSSEditUtils::GetElementContainerOrSelf(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + if (nsIDOMNode::DOCUMENT_NODE == aNode->NodeType()) { + return nullptr; + } + + nsINode* node = aNode; + // Loop until we find an element. + while (node && !node->IsElement()) { + node = node->GetParentNode(); + } + + NS_ENSURE_TRUE(node, nullptr); + return node->AsElement(); +} + +nsresult +CSSEditUtils::SetCSSProperty(nsIDOMElement* aElement, + const nsAString& aProperty, + const nsAString& aValue) +{ + nsCOMPtr<nsIDOMCSSStyleDeclaration> cssDecl; + uint32_t length; + nsresult rv = GetInlineStyles(aElement, getter_AddRefs(cssDecl), &length); + if (NS_FAILED(rv) || !cssDecl) { + return rv; + } + + return cssDecl->SetProperty(aProperty, + aValue, + EmptyString()); +} + +nsresult +CSSEditUtils::SetCSSPropertyPixels(nsIDOMElement* aElement, + const nsAString& aProperty, + int32_t aIntValue) +{ + nsAutoString s; + s.AppendInt(aIntValue); + return SetCSSProperty(aElement, aProperty, s + NS_LITERAL_STRING("px")); +} + +} // namespace mozilla diff --git a/editor/libeditor/CSSEditUtils.h b/editor/libeditor/CSSEditUtils.h new file mode 100644 index 000000000..0b9a12952 --- /dev/null +++ b/editor/libeditor/CSSEditUtils.h @@ -0,0 +1,473 @@ +/* -*- 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/. */ + +#ifndef mozilla_CSSEditUtils_h +#define mozilla_CSSEditUtils_h + +#include "mozilla/ChangeStyleTransaction.h" // for ChangeStyleTransaction +#include "nsCOMPtr.h" // for already_AddRefed +#include "nsTArray.h" // for nsTArray +#include "nscore.h" // for nsAString, nsresult, nullptr + +class nsComputedDOMStyle; +class nsIAtom; +class nsIContent; +class nsIDOMCSSStyleDeclaration; +class nsIDOMElement; +class nsIDOMNode; +class nsINode; +class nsString; + +namespace mozilla { + +class HTMLEditor; +namespace dom { +class Element; +} // namespace dom + +typedef void (*nsProcessValueFunc)(const nsAString* aInputString, + nsAString& aOutputString, + const char* aDefaultValueString, + const char* aPrependString, + const char* aAppendString); + +class CSSEditUtils final +{ +public: + explicit CSSEditUtils(HTMLEditor* aEditor); + ~CSSEditUtils(); + + enum nsCSSEditableProperty + { + eCSSEditableProperty_NONE=0, + eCSSEditableProperty_background_color, + eCSSEditableProperty_background_image, + eCSSEditableProperty_border, + eCSSEditableProperty_caption_side, + eCSSEditableProperty_color, + eCSSEditableProperty_float, + eCSSEditableProperty_font_family, + eCSSEditableProperty_font_size, + eCSSEditableProperty_font_style, + eCSSEditableProperty_font_weight, + eCSSEditableProperty_height, + eCSSEditableProperty_list_style_type, + eCSSEditableProperty_margin_left, + eCSSEditableProperty_margin_right, + eCSSEditableProperty_text_align, + eCSSEditableProperty_text_decoration, + eCSSEditableProperty_vertical_align, + eCSSEditableProperty_whitespace, + eCSSEditableProperty_width + }; + + enum StyleType { eSpecified, eComputed }; + + + struct CSSEquivTable + { + nsCSSEditableProperty cssProperty; + nsProcessValueFunc processValueFunctor; + const char* defaultValue; + const char* prependValue; + const char* appendValue; + bool gettable; + bool caseSensitiveValue; + }; + + /** + * Answers true if the given combination element_name/attribute_name + * has a CSS equivalence in this implementation. + * + * @param aNode [IN] A DOM node. + * @param aProperty [IN] An atom containing a HTML tag name. + * @param aAttribute [IN] A string containing the name of a HTML + * attribute carried by the element above. + * @return A boolean saying if the tag/attribute has a CSS + * equiv. + */ + bool IsCSSEditableProperty(nsINode* aNode, nsIAtom* aProperty, + const nsAString* aAttribute); + + /** + * Adds/remove a CSS declaration to the STYLE atrribute carried by a given + * element. + * + * @param aElement [IN] A DOM element. + * @param aProperty [IN] An atom containing the CSS property to set. + * @param aValue [IN] A string containing the value of the CSS + * property. + * @param aSuppressTransaction [IN] A boolean indicating, when true, + * that no transaction should be recorded. + */ + nsresult SetCSSProperty(dom::Element& aElement, nsIAtom& aProperty, + const nsAString& aValue, bool aSuppressTxn = false); + nsresult SetCSSPropertyPixels(dom::Element& aElement, + nsIAtom& aProperty, int32_t aIntValue); + nsresult RemoveCSSProperty(dom::Element& aElement, + nsIAtom& aProperty, + const nsAString& aPropertyValue, + bool aSuppressTxn = false); + + /** + * Directly adds/remove a CSS declaration to the STYLE atrribute carried by + * a given element without going through the transaction manager. + * + * @param aElement [IN] A DOM element. + * @param aProperty [IN] A string containing the CSS property to + * set/remove. + * @param aValue [IN] A string containing the new value of the CSS + * property. + */ + nsresult SetCSSProperty(nsIDOMElement* aElement, + const nsAString& aProperty, + const nsAString& aValue); + nsresult SetCSSPropertyPixels(nsIDOMElement* aElement, + const nsAString& aProperty, + int32_t aIntValue); + + /** + * Gets the specified/computed style value of a CSS property for a given + * node (or its element ancestor if it is not an element). + * + * @param aNode [IN] A DOM node. + * @param aProperty [IN] An atom containing the CSS property to get. + * @param aPropertyValue [OUT] The retrieved value of the property. + */ + nsresult GetSpecifiedProperty(nsINode& aNode, nsIAtom& aProperty, + nsAString& aValue); + nsresult GetComputedProperty(nsINode& aNode, nsIAtom& aProperty, + nsAString& aValue); + + /** + * Removes a CSS property from the specified declarations in STYLE attribute + * and removes the node if it is an useless span. + * + * @param aNode [IN] The specific node we want to remove a style + * from. + * @param aProperty [IN] The CSS property atom to remove. + * @param aPropertyValue [IN] The value of the property we have to remove + * if the property accepts more than one value. + */ + nsresult RemoveCSSInlineStyle(nsIDOMNode* aNode, nsIAtom* aProperty, + const nsAString& aPropertyValue); + + /** + * Answers true is the property can be removed by setting a "none" CSS value + * on a node. + * + * @param aProperty [IN] An atom containing a CSS property. + * @param aAttribute [IN] Pointer to an attribute name or null if this + * information is irrelevant. + * @return A boolean saying if the property can be remove by + * setting a "none" value. + */ + bool IsCSSInvertible(nsIAtom& aProperty, const nsAString* aAttribute); + + /** + * Get the default browser background color if we need it for + * GetCSSBackgroundColorState(). + * + * @param aColor [OUT] The default color as it is defined in prefs. + */ + void GetDefaultBackgroundColor(nsAString& aColor); + + /** + * Get the default length unit used for CSS Indent/Outdent. + * + * @param aLengthUnit [OUT] The default length unit as it is defined in + * prefs. + */ + void GetDefaultLengthUnit(nsAString & aLengthUnit); + + /** + * Returns the list of values for the CSS equivalences to + * the passed HTML style for the passed node. + * + * @param aNode [IN] A DOM node. + * @param aHTMLProperty [IN] An atom containing an HTML property. + * @param aAttribute [IN] A pointer to an attribute name or nullptr if + * irrelevant. + * @param aValueString [OUT] The list of CSS values. + * @param aStyleType [IN] eSpecified or eComputed. + */ + nsresult GetCSSEquivalentToHTMLInlineStyleSet(nsINode* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + nsAString& aValueString, + StyleType aStyleType); + + /** + * Does the node aNode (or his parent if it is not an element node) carries + * the CSS equivalent styles to the HTML style for this node ? + * + * @param aNode [IN] A DOM node. + * @param aHTMLProperty [IN] An atom containing an HTML property. + * @param aAttribute [IN] A pointer to an attribute name or nullptr if + * irrelevant. + * @param aIsSet [OUT] A boolean being true if the css properties are + * set. + * @param aValueString [IN/OUT] The attribute value (in) the list of CSS + * values (out). + * @param aStyleType [IN] eSpecified or eComputed. + * + * The nsIContent variant returns aIsSet instead of using an out parameter. + */ + bool IsCSSEquivalentToHTMLInlineStyleSet(nsINode* aContent, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString& aValue, + StyleType aStyleType); + + bool IsCSSEquivalentToHTMLInlineStyleSet(nsINode* aContent, + nsIAtom* aProperty, + const nsAString* aAttribute, + nsAString& aValue, + StyleType aStyleType); + + nsresult IsCSSEquivalentToHTMLInlineStyleSet(nsIDOMNode* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + bool& aIsSet, + nsAString& aValueString, + StyleType aStyleType); + + /** + * Adds to the node the CSS inline styles equivalent to the HTML style + * and return the number of CSS properties set by the call. + * + * @param aNode [IN] A DOM node. + * @param aHTMLProperty [IN] An atom containing an HTML property. + * @param aAttribute [IN] A pointer to an attribute name or nullptr if + * irrelevant. + * @param aValue [IN] The attribute value. + * @param aCount [OUT] The number of CSS properties set by the call. + * @param aSuppressTransaction [IN] A boolean indicating, when true, + * that no transaction should be recorded. + * + * aCount is returned by the dom::Element variant instead of being an out + * parameter. + */ + int32_t SetCSSEquivalentToHTMLStyle(dom::Element* aElement, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool aSuppressTransaction); + nsresult SetCSSEquivalentToHTMLStyle(nsIDOMNode* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + const nsAString* aValue, + int32_t* aCount, + bool aSuppressTransaction); + + /** + * Removes from the node the CSS inline styles equivalent to the HTML style. + * + * @param aNode [IN] A DOM node. + * @param aHTMLProperty [IN] An atom containing an HTML property. + * @param aAttribute [IN] A pointer to an attribute name or nullptr if + * irrelevant. + * @param aValue [IN] The attribute value. + * @param aSuppressTransaction [IN] A boolean indicating, when true, + * that no transaction should be recorded. + */ + nsresult RemoveCSSEquivalentToHTMLStyle(nsIDOMNode* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool aSuppressTransaction); + + /** + * Removes from the node the CSS inline styles equivalent to the HTML style. + * + * @param aElement [IN] A DOM Element (must not be null). + * @param aHTMLProperty [IN] An atom containing an HTML property. + * @param aAttribute [IN] A pointer to an attribute name or nullptr if + * irrelevant. + * @param aValue [IN] The attribute value. + * @param aSuppressTransaction [IN] A boolean indicating, when true, + * that no transaction should be recorded. + */ + nsresult RemoveCSSEquivalentToHTMLStyle(dom::Element* aElement, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool aSuppressTransaction); + + /** + * Parses a "xxxx.xxxxxuuu" string where x is a digit and u an alpha char + * we need such a parser because + * nsIDOMCSSStyleDeclaration::GetPropertyCSSValue() is not implemented. + * + * @param aString [IN] Input string to parse. + * @param aValue [OUT] Numeric part. + * @param aUnit [OUT] Unit part. + */ + void ParseLength(const nsAString& aString, float* aValue, nsIAtom** aUnit); + + /** + * Sets the mIsCSSPrefChecked private member; used as callback from observer + * when the CSS pref state is changed. + * + * @param aIsCSSPrefChecked [IN] The new boolean state for the pref. + */ + void SetCSSEnabled(bool aIsCSSPrefChecked); + + /** + * Retrieves the mIsCSSPrefChecked private member, true if the CSS pref is + * checked, false if it is not. + * + * @return the boolean value of the CSS pref. + */ + bool IsCSSPrefChecked(); + + /** + * ElementsSameStyle compares two elements and checks if they have the same + * specified CSS declarations in the STYLE attribute. + * The answer is always false if at least one of them carries an ID or a + * class. + * + * @param aFirstNode [IN] A DOM node. + * @param aSecondNode [IN] A DOM node. + * @return true if the two elements are considered to + * have same styles. + */ + bool ElementsSameStyle(dom::Element* aFirstNode, + dom::Element* aSecondNode); + bool ElementsSameStyle(nsIDOMNode* aFirstNode, nsIDOMNode* aSecondNode); + + /** + * Get the specified inline styles (style attribute) for an element. + * + * @param aElement [IN] The element node. + * @param aCssDecl [OUT] The CSS declaration corresponding to the + * style attribute. + * @param aLength [OUT] The number of declarations in aCssDecl. + */ + nsresult GetInlineStyles(dom::Element* aElement, + nsIDOMCSSStyleDeclaration** aCssDecl, + uint32_t* aLength); + nsresult GetInlineStyles(nsIDOMElement* aElement, + nsIDOMCSSStyleDeclaration** aCssDecl, + uint32_t* aLength); +private: + nsresult GetInlineStyles(nsISupports* aElement, + nsIDOMCSSStyleDeclaration** aCssDecl, + uint32_t* aLength); + +public: + /** + * Returns aNode itself if it is an element node, or the first ancestors + * being an element node if aNode is not one itself. + * + * @param aNode [IN] A node + * @param aElement [OUT] The deepest element node containing aNode + * (possibly aNode itself) + */ + dom::Element* GetElementContainerOrSelf(nsINode* aNode); + already_AddRefed<nsIDOMElement> GetElementContainerOrSelf(nsIDOMNode* aNode); + + /** + * Gets the computed style for a given element. Can return null. + */ + already_AddRefed<nsComputedDOMStyle> GetComputedStyle(dom::Element* aElement); + +private: + /** + * Retrieves the CSS property atom from an enum. + * + * @param aProperty [IN] The enum value for the property. + * @param aAtom [OUT] The corresponding atom. + */ + void GetCSSPropertyAtom(nsCSSEditableProperty aProperty, nsIAtom** aAtom); + + /** + * Retrieves the CSS declarations equivalent to a HTML style value for + * a given equivalence table. + * + * @param aPropertyArray [OUT] The array of css properties. + * @param aValueArray [OUT] The array of values for the CSS properties + * above. + * @param aEquivTable [IN] The equivalence table. + * @param aValue [IN] The HTML style value. + * @param aGetOrRemoveRequest [IN] A boolean value being true if the call to + * the current method is made for + * GetCSSEquivalentToHTMLInlineStyleSet() or + * RemoveCSSEquivalentToHTMLInlineStyleSet(). + */ + void BuildCSSDeclarations(nsTArray<nsIAtom*>& aPropertyArray, + nsTArray<nsString>& cssValueArray, + const CSSEquivTable* aEquivTable, + const nsAString* aValue, + bool aGetOrRemoveRequest); + + /** + * Retrieves the CSS declarations equivalent to the given HTML + * property/attribute/value for a given node. + * + * @param aNode [IN] The DOM node. + * @param aHTMLProperty [IN] An atom containing an HTML property. + * @param aAttribute [IN] A pointer to an attribute name or nullptr + * if irrelevant + * @param aValue [IN] The attribute value. + * @param aPropertyArray [OUT] The array of CSS properties. + * @param aValueArray [OUT] The array of values for the CSS properties + * above. + * @param aGetOrRemoveRequest [IN] A boolean value being true if the call to + * the current method is made for + * GetCSSEquivalentToHTMLInlineStyleSet() or + * RemoveCSSEquivalentToHTMLInlineStyleSet(). + */ + void GenerateCSSDeclarationsFromHTMLStyle(dom::Element* aNode, + nsIAtom* aHTMLProperty, + const nsAString* aAttribute, + const nsAString* aValue, + nsTArray<nsIAtom*>& aPropertyArray, + nsTArray<nsString>& aValueArray, + bool aGetOrRemoveRequest); + + /** + * Creates a Transaction for setting or removing a CSS property. Never + * returns null. + * + * @param aElement [IN] A DOM element. + * @param aProperty [IN] A CSS property. + * @param aValue [IN] The value to set for this CSS property. + * @param aChangeType [IN] eSet to set, eRemove to remove. + */ + already_AddRefed<ChangeStyleTransaction> + CreateCSSPropertyTxn(dom::Element& aElement, + nsIAtom& aProperty, const nsAString& aValue, + ChangeStyleTransaction::EChangeType aChangeType); + + /** + * Back-end for GetSpecifiedProperty and GetComputedProperty. + * + * @param aNode [IN] A DOM node. + * @param aProperty [IN] A CSS property. + * @param aValue [OUT] The retrieved value for this property. + * @param aStyleType [IN] eSpecified or eComputed. + */ + nsresult GetCSSInlinePropertyBase(nsINode* aNode, nsIAtom* aProperty, + nsAString& aValue, StyleType aStyleType); + +private: + HTMLEditor* mHTMLEditor; + bool mIsCSSPrefChecked; +}; + +#define NS_EDITOR_INDENT_INCREMENT_IN 0.4134f +#define NS_EDITOR_INDENT_INCREMENT_CM 1.05f +#define NS_EDITOR_INDENT_INCREMENT_MM 10.5f +#define NS_EDITOR_INDENT_INCREMENT_PT 29.76f +#define NS_EDITOR_INDENT_INCREMENT_PC 2.48f +#define NS_EDITOR_INDENT_INCREMENT_EM 3 +#define NS_EDITOR_INDENT_INCREMENT_EX 6 +#define NS_EDITOR_INDENT_INCREMENT_PX 40 +#define NS_EDITOR_INDENT_INCREMENT_PERCENT 4 + +} // namespace mozilla + +#endif // #ifndef mozilla_CSSEditUtils_h diff --git a/editor/libeditor/ChangeAttributeTransaction.cpp b/editor/libeditor/ChangeAttributeTransaction.cpp new file mode 100644 index 000000000..04f539856 --- /dev/null +++ b/editor/libeditor/ChangeAttributeTransaction.cpp @@ -0,0 +1,98 @@ +/* -*- 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 "ChangeAttributeTransaction.h" + +#include "mozilla/dom/Element.h" // for Element + +#include "nsAString.h" +#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc. + +namespace mozilla { + +using namespace dom; + +ChangeAttributeTransaction::ChangeAttributeTransaction(Element& aElement, + nsIAtom& aAttribute, + const nsAString* aValue) + : EditTransactionBase() + , mElement(&aElement) + , mAttribute(&aAttribute) + , mValue(aValue ? *aValue : EmptyString()) + , mRemoveAttribute(!aValue) + , mAttributeWasSet(false) + , mUndoValue() +{ +} + +ChangeAttributeTransaction::~ChangeAttributeTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ChangeAttributeTransaction, + EditTransactionBase, + mElement) + +NS_IMPL_ADDREF_INHERITED(ChangeAttributeTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(ChangeAttributeTransaction, EditTransactionBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChangeAttributeTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +NS_IMETHODIMP +ChangeAttributeTransaction::DoTransaction() +{ + // Need to get the current value of the attribute and save it, and set + // mAttributeWasSet + mAttributeWasSet = mElement->GetAttr(kNameSpaceID_None, mAttribute, + mUndoValue); + + // XXX: hack until attribute-was-set code is implemented + if (!mUndoValue.IsEmpty()) { + mAttributeWasSet = true; + } + // XXX: end hack + + // Now set the attribute to the new value + if (mRemoveAttribute) { + return mElement->UnsetAttr(kNameSpaceID_None, mAttribute, true); + } + + return mElement->SetAttr(kNameSpaceID_None, mAttribute, mValue, true); +} + +NS_IMETHODIMP +ChangeAttributeTransaction::UndoTransaction() +{ + if (mAttributeWasSet) { + return mElement->SetAttr(kNameSpaceID_None, mAttribute, mUndoValue, true); + } + return mElement->UnsetAttr(kNameSpaceID_None, mAttribute, true); +} + +NS_IMETHODIMP +ChangeAttributeTransaction::RedoTransaction() +{ + if (mRemoveAttribute) { + return mElement->UnsetAttr(kNameSpaceID_None, mAttribute, true); + } + + return mElement->SetAttr(kNameSpaceID_None, mAttribute, mValue, true); +} + +NS_IMETHODIMP +ChangeAttributeTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("ChangeAttributeTransaction: [mRemoveAttribute == "); + + if (mRemoveAttribute) { + aString.AppendLiteral("true] "); + } else { + aString.AppendLiteral("false] "); + } + aString += nsDependentAtomString(mAttribute); + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/ChangeAttributeTransaction.h b/editor/libeditor/ChangeAttributeTransaction.h new file mode 100644 index 000000000..bb0c26c38 --- /dev/null +++ b/editor/libeditor/ChangeAttributeTransaction.h @@ -0,0 +1,72 @@ +/* -*- 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/. */ + +#ifndef ChangeAttributeTransaction_h +#define ChangeAttributeTransaction_h + +#include "mozilla/Attributes.h" // override +#include "mozilla/EditTransactionBase.h" // base class +#include "nsCOMPtr.h" // nsCOMPtr members +#include "nsCycleCollectionParticipant.h" // NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED +#include "nsISupportsImpl.h" // NS_DECL_ISUPPORTS_INHERITED +#include "nsString.h" // nsString members + +class nsIAtom; + +namespace mozilla { + +namespace dom { +class Element; +} // namespace dom + +/** + * A transaction that changes an attribute of a content node. This transaction + * covers add, remove, and change attribute. + */ +class ChangeAttributeTransaction final : public EditTransactionBase +{ +public: + /** + * @param aElement the element whose attribute will be changed + * @param aAttribute the name of the attribute to change + * @param aValue the new value for aAttribute, or null to remove + */ + ChangeAttributeTransaction(dom::Element& aElement, + nsIAtom& aAttribute, + const nsAString* aValue); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ChangeAttributeTransaction, + EditTransactionBase) + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + +private: + virtual ~ChangeAttributeTransaction(); + + // The element to operate upon + nsCOMPtr<dom::Element> mElement; + + // The attribute to change + nsCOMPtr<nsIAtom> mAttribute; + + // The value to set the attribute to (ignored if mRemoveAttribute==true) + nsString mValue; + + // True if the operation is to remove mAttribute from mElement + bool mRemoveAttribute; + + // True if the mAttribute was set on mElement at the time of execution + bool mAttributeWasSet; + + // The value to set the attribute to for undo + nsString mUndoValue; +}; + +} // namespace mozilla + +#endif // #ifndef ChangeAttributeTransaction_h diff --git a/editor/libeditor/ChangeStyleTransaction.cpp b/editor/libeditor/ChangeStyleTransaction.cpp new file mode 100644 index 000000000..103d9c455 --- /dev/null +++ b/editor/libeditor/ChangeStyleTransaction.cpp @@ -0,0 +1,287 @@ +/* -*- 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/ChangeStyleTransaction.h" + +#include "mozilla/dom/Element.h" // for Element +#include "nsAString.h" // for nsAString_internal::Append, etc. +#include "nsCRT.h" // for nsCRT::IsAsciiSpace +#include "nsDebug.h" // for NS_ENSURE_SUCCESS, etc. +#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc. +#include "nsGkAtoms.h" // for nsGkAtoms, etc. +#include "nsICSSDeclaration.h" // for nsICSSDeclaration. +#include "nsLiteralString.h" // for NS_LITERAL_STRING, etc. +#include "nsReadableUtils.h" // for ToNewUnicode +#include "nsString.h" // for nsAutoString, nsString, etc. +#include "nsStyledElement.h" // for nsStyledElement. +#include "nsUnicharUtils.h" // for nsCaseInsensitiveStringComparator + +namespace mozilla { + +using namespace dom; + +#define kNullCh (char16_t('\0')) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ChangeStyleTransaction, EditTransactionBase, + mElement) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChangeStyleTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +NS_IMPL_ADDREF_INHERITED(ChangeStyleTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(ChangeStyleTransaction, EditTransactionBase) + +ChangeStyleTransaction::~ChangeStyleTransaction() +{ +} + +// Answers true if aValue is in the string list of white-space separated values +// aValueList. +bool +ChangeStyleTransaction::ValueIncludes(const nsAString& aValueList, + const nsAString& aValue) +{ + nsAutoString valueList(aValueList); + bool result = false; + + // put an extra null at the end + valueList.Append(kNullCh); + + char16_t* value = ToNewUnicode(aValue); + char16_t* start = valueList.BeginWriting(); + char16_t* end = start; + + while (kNullCh != *start) { + while (kNullCh != *start && nsCRT::IsAsciiSpace(*start)) { + // skip leading space + start++; + } + end = start; + + while (kNullCh != *end && !nsCRT::IsAsciiSpace(*end)) { + // look for space or end + end++; + } + // end string here + *end = kNullCh; + + if (start < end) { + if (nsDependentString(value).Equals(nsDependentString(start), + nsCaseInsensitiveStringComparator())) { + result = true; + break; + } + } + start = ++end; + } + free(value); + return result; +} + +// Removes the value aRemoveValue from the string list of white-space separated +// values aValueList +void +ChangeStyleTransaction::RemoveValueFromListOfValues( + nsAString& aValues, + const nsAString& aRemoveValue) +{ + nsAutoString classStr(aValues); + nsAutoString outString; + // put an extra null at the end + classStr.Append(kNullCh); + + char16_t* start = classStr.BeginWriting(); + char16_t* end = start; + + while (kNullCh != *start) { + while (kNullCh != *start && nsCRT::IsAsciiSpace(*start)) { + // skip leading space + start++; + } + end = start; + + while (kNullCh != *end && !nsCRT::IsAsciiSpace(*end)) { + // look for space or end + end++; + } + // end string here + *end = kNullCh; + + if (start < end && !aRemoveValue.Equals(start)) { + outString.Append(start); + outString.Append(char16_t(' ')); + } + + start = ++end; + } + aValues.Assign(outString); +} + +ChangeStyleTransaction::ChangeStyleTransaction(Element& aElement, + nsIAtom& aProperty, + const nsAString& aValue, + EChangeType aChangeType) + : EditTransactionBase() + , mElement(&aElement) + , mProperty(&aProperty) + , mValue(aValue) + , mRemoveProperty(aChangeType == eRemove) + , mUndoValue() + , mRedoValue() + , mUndoAttributeWasSet(false) + , mRedoAttributeWasSet(false) +{ +} + +NS_IMETHODIMP +ChangeStyleTransaction::DoTransaction() +{ + nsCOMPtr<nsStyledElement> inlineStyles = do_QueryInterface(mElement); + NS_ENSURE_TRUE(inlineStyles, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsICSSDeclaration> cssDecl = inlineStyles->Style(); + + nsAutoString propertyNameString; + mProperty->ToString(propertyNameString); + + mUndoAttributeWasSet = mElement->HasAttr(kNameSpaceID_None, + nsGkAtoms::style); + + nsAutoString values; + nsresult rv = cssDecl->GetPropertyValue(propertyNameString, values); + NS_ENSURE_SUCCESS(rv, rv); + mUndoValue.Assign(values); + + // Does this property accept more than one value? (bug 62682) + bool multiple = AcceptsMoreThanOneValue(*mProperty); + + if (mRemoveProperty) { + nsAutoString returnString; + if (multiple) { + // Let's remove only the value we have to remove and not the others + + // The two lines below are a workaround because + // nsDOMCSSDeclaration::GetPropertyCSSValue is not yet implemented (bug + // 62682) + RemoveValueFromListOfValues(values, NS_LITERAL_STRING("none")); + RemoveValueFromListOfValues(values, mValue); + if (values.IsEmpty()) { + rv = cssDecl->RemoveProperty(propertyNameString, returnString); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsAutoString priority; + cssDecl->GetPropertyPriority(propertyNameString, priority); + rv = cssDecl->SetProperty(propertyNameString, values, priority); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + rv = cssDecl->RemoveProperty(propertyNameString, returnString); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + nsAutoString priority; + cssDecl->GetPropertyPriority(propertyNameString, priority); + if (multiple) { + // Let's add the value we have to add to the others + + // The line below is a workaround because + // nsDOMCSSDeclaration::GetPropertyCSSValue is not yet implemented (bug + // 62682) + AddValueToMultivalueProperty(values, mValue); + } else { + values.Assign(mValue); + } + rv = cssDecl->SetProperty(propertyNameString, values, priority); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Let's be sure we don't keep an empty style attribute + uint32_t length; + rv = cssDecl->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + if (!length) { + rv = mElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::style, true); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mRedoAttributeWasSet = true; + } + + return cssDecl->GetPropertyValue(propertyNameString, mRedoValue); +} + +nsresult +ChangeStyleTransaction::SetStyle(bool aAttributeWasSet, + nsAString& aValue) +{ + if (aAttributeWasSet) { + // The style attribute was not empty, let's recreate the declaration + nsAutoString propertyNameString; + mProperty->ToString(propertyNameString); + + nsCOMPtr<nsStyledElement> inlineStyles = do_QueryInterface(mElement); + NS_ENSURE_TRUE(inlineStyles, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsICSSDeclaration> cssDecl = inlineStyles->Style(); + + if (aValue.IsEmpty()) { + // An empty value means we have to remove the property + nsAutoString returnString; + return cssDecl->RemoveProperty(propertyNameString, returnString); + } + // Let's recreate the declaration as it was + nsAutoString priority; + cssDecl->GetPropertyPriority(propertyNameString, priority); + return cssDecl->SetProperty(propertyNameString, aValue, priority); + } + return mElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::style, true); +} + +NS_IMETHODIMP +ChangeStyleTransaction::UndoTransaction() +{ + return SetStyle(mUndoAttributeWasSet, mUndoValue); +} + +NS_IMETHODIMP +ChangeStyleTransaction::RedoTransaction() +{ + return SetStyle(mRedoAttributeWasSet, mRedoValue); +} + +NS_IMETHODIMP +ChangeStyleTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("ChangeStyleTransaction: [mRemoveProperty == "); + + if (mRemoveProperty) { + aString.AppendLiteral("true] "); + } else { + aString.AppendLiteral("false] "); + } + aString += nsDependentAtomString(mProperty); + return NS_OK; +} + +// True if the CSS property accepts more than one value +bool +ChangeStyleTransaction::AcceptsMoreThanOneValue(nsIAtom& aCSSProperty) +{ + return &aCSSProperty == nsGkAtoms::text_decoration; +} + +// Adds the value aNewValue to the list of white-space separated values aValues +void +ChangeStyleTransaction::AddValueToMultivalueProperty(nsAString& aValues, + const nsAString& aNewValue) +{ + if (aValues.IsEmpty() || aValues.LowerCaseEqualsLiteral("none")) { + aValues.Assign(aNewValue); + } else if (!ValueIncludes(aValues, aNewValue)) { + // We already have another value but not this one; add it + aValues.Append(char16_t(' ')); + aValues.Append(aNewValue); + } +} + +} // namespace mozilla diff --git a/editor/libeditor/ChangeStyleTransaction.h b/editor/libeditor/ChangeStyleTransaction.h new file mode 100644 index 000000000..14c2cdcb5 --- /dev/null +++ b/editor/libeditor/ChangeStyleTransaction.h @@ -0,0 +1,123 @@ +/* -*- 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/. */ + +#ifndef mozilla_ChangeStyleTransaction_h +#define mozilla_ChangeStyleTransaction_h + +#include "mozilla/EditTransactionBase.h" // base class +#include "nsCOMPtr.h" // nsCOMPtr members +#include "nsCycleCollectionParticipant.h" // various macros +#include "nsString.h" // nsString members + +class nsAString; +class nsIAtom; + +namespace mozilla { + +namespace dom { +class Element; +} // namespace dom + +/** + * A transaction that changes the value of a CSS inline style of a content + * node. This transaction covers add, remove, and change a property's value. + */ +class ChangeStyleTransaction final : public EditTransactionBase +{ +public: + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ChangeStyleTransaction, + EditTransactionBase) + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + + enum EChangeType { eSet, eRemove }; + + /** + * @param aNode [IN] the node whose style attribute will be changed + * @param aProperty [IN] the name of the property to change + * @param aValue [IN] new value for aProperty, or value to remove + * @param aChangeType [IN] whether to set or remove + */ + ChangeStyleTransaction(dom::Element& aElement, + nsIAtom& aProperty, + const nsAString& aValue, + EChangeType aChangeType); + + /** + * Returns true if the list of white-space separated values contains aValue + * + * @param aValueList [IN] a list of white-space separated values + * @param aValue [IN] the value to look for in the list + * @return true if the value is in the list of values + */ + static bool ValueIncludes(const nsAString& aValueList, + const nsAString& aValue); + +private: + virtual ~ChangeStyleTransaction(); + + /* + * Adds the value aNewValue to list of white-space separated values aValues. + * + * @param aValues [IN/OUT] a list of wite-space separated values + * @param aNewValue [IN] a value this code adds to aValues if it is not + * already in + */ + void AddValueToMultivalueProperty(nsAString& aValues, + const nsAString& aNewValue); + + /** + * Returns true if the property accepts more than one value. + * + * @param aCSSProperty [IN] the CSS property + * @return true if the property accepts more than one value + */ + bool AcceptsMoreThanOneValue(nsIAtom& aCSSProperty); + + /** + * Remove a value from a list of white-space separated values. + * @param aValues [IN] a list of white-space separated values + * @param aRemoveValue [IN] the value to remove from the list + */ + void RemoveValueFromListOfValues(nsAString& aValues, + const nsAString& aRemoveValue); + + /** + * If the boolean is true and if the value is not the empty string, + * set the property in the transaction to that value; if the value + * is empty, remove the property from element's styles. If the boolean + * is false, just remove the style attribute. + */ + nsresult SetStyle(bool aAttributeWasSet, nsAString& aValue); + + // The element to operate upon. + nsCOMPtr<dom::Element> mElement; + + // The CSS property to change. + nsCOMPtr<nsIAtom> mProperty; + + // The value to set the property to (ignored if mRemoveProperty==true). + nsString mValue; + + // true if the operation is to remove mProperty from mElement. + bool mRemoveProperty; + + // The value to set the property to for undo. + nsString mUndoValue; + // The value to set the property to for redo. + nsString mRedoValue; + // True if the style attribute was present and not empty before DoTransaction. + bool mUndoAttributeWasSet; + // True if the style attribute is present and not empty after DoTransaction. + bool mRedoAttributeWasSet; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_ChangeStyleTransaction_h diff --git a/editor/libeditor/CompositionTransaction.cpp b/editor/libeditor/CompositionTransaction.cpp new file mode 100644 index 000000000..25938fa60 --- /dev/null +++ b/editor/libeditor/CompositionTransaction.cpp @@ -0,0 +1,332 @@ +/* -*- 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 "CompositionTransaction.h" + +#include "mozilla/EditorBase.h" // mEditorBase +#include "mozilla/SelectionState.h" // RangeUpdater +#include "mozilla/dom/Selection.h" // local var +#include "mozilla/dom/Text.h" // mTextNode +#include "nsAString.h" // params +#include "nsDebug.h" // for NS_ASSERTION, etc +#include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc +#include "nsIPresShell.h" // nsISelectionController constants +#include "nsRange.h" // local var +#include "nsQueryObject.h" // for do_QueryObject + +namespace mozilla { + +using namespace dom; + +CompositionTransaction::CompositionTransaction( + Text& aTextNode, + uint32_t aOffset, + uint32_t aReplaceLength, + TextRangeArray* aTextRangeArray, + const nsAString& aStringToInsert, + EditorBase& aEditorBase, + RangeUpdater* aRangeUpdater) + : mTextNode(&aTextNode) + , mOffset(aOffset) + , mReplaceLength(aReplaceLength) + , mRanges(aTextRangeArray) + , mStringToInsert(aStringToInsert) + , mEditorBase(aEditorBase) + , mRangeUpdater(aRangeUpdater) + , mFixed(false) +{ + MOZ_ASSERT(mTextNode->TextLength() >= mOffset); +} + +CompositionTransaction::~CompositionTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction, EditTransactionBase, + mTextNode) +// mRangeList can't lead to cycles + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction) + if (aIID.Equals(NS_GET_IID(CompositionTransaction))) { + foundInterface = static_cast<nsITransaction*>(this); + } else +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +NS_IMPL_ADDREF_INHERITED(CompositionTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(CompositionTransaction, EditTransactionBase) + +NS_IMETHODIMP +CompositionTransaction::DoTransaction() +{ + // Fail before making any changes if there's no selection controller + nsCOMPtr<nsISelectionController> selCon; + mEditorBase.GetSelectionController(getter_AddRefs(selCon)); + NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED); + + // Advance caret: This requires the presentation shell to get the selection. + if (mReplaceLength == 0) { + nsresult rv = mTextNode->InsertData(mOffset, mStringToInsert); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + mRangeUpdater->SelAdjInsertText(*mTextNode, mOffset, mStringToInsert); + } else { + uint32_t replaceableLength = mTextNode->TextLength() - mOffset; + nsresult rv = + mTextNode->ReplaceData(mOffset, mReplaceLength, mStringToInsert); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + mRangeUpdater->SelAdjDeleteText(mTextNode, mOffset, mReplaceLength); + mRangeUpdater->SelAdjInsertText(*mTextNode, mOffset, mStringToInsert); + + // If IME text node is multiple node, ReplaceData doesn't remove all IME + // text. So we need remove remained text into other text node. + if (replaceableLength < mReplaceLength) { + int32_t remainLength = mReplaceLength - replaceableLength; + nsCOMPtr<nsINode> node = mTextNode->GetNextSibling(); + while (node && node->IsNodeOfType(nsINode::eTEXT) && + remainLength > 0) { + Text* text = static_cast<Text*>(node.get()); + uint32_t textLength = text->TextLength(); + text->DeleteData(0, remainLength); + mRangeUpdater->SelAdjDeleteText(text, 0, remainLength); + remainLength -= textLength; + node = node->GetNextSibling(); + } + } + } + + nsresult rv = SetSelectionForRanges(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +CompositionTransaction::UndoTransaction() +{ + // Get the selection first so we'll fail before making any changes if we + // can't get it + RefPtr<Selection> selection = mEditorBase.GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + + nsresult rv = mTextNode->DeleteData(mOffset, mStringToInsert.Length()); + NS_ENSURE_SUCCESS(rv, rv); + + // set the selection to the insertion point where the string was removed + rv = selection->Collapse(mTextNode, mOffset); + NS_ASSERTION(NS_SUCCEEDED(rv), + "Selection could not be collapsed after undo of IME insert."); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +CompositionTransaction::Merge(nsITransaction* aTransaction, + bool* aDidMerge) +{ + NS_ENSURE_ARG_POINTER(aTransaction && aDidMerge); + + // Check to make sure we aren't fixed, if we are then nothing gets absorbed + if (mFixed) { + *aDidMerge = false; + return NS_OK; + } + + // If aTransaction is another CompositionTransaction then absorb it + RefPtr<CompositionTransaction> otherTransaction = + do_QueryObject(aTransaction); + if (otherTransaction) { + // We absorb the next IME transaction by adopting its insert string + mStringToInsert = otherTransaction->mStringToInsert; + mRanges = otherTransaction->mRanges; + *aDidMerge = true; + return NS_OK; + } + + *aDidMerge = false; + return NS_OK; +} + +void +CompositionTransaction::MarkFixed() +{ + mFixed = true; +} + +NS_IMETHODIMP +CompositionTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("CompositionTransaction: "); + aString += mStringToInsert; + return NS_OK; +} + +/* ============ private methods ================== */ + +nsresult +CompositionTransaction::SetSelectionForRanges() +{ + return SetIMESelection(mEditorBase, mTextNode, mOffset, + mStringToInsert.Length(), mRanges); +} + +// static +nsresult +CompositionTransaction::SetIMESelection(EditorBase& aEditorBase, + Text* aTextNode, + uint32_t aOffsetInNode, + uint32_t aLengthOfCompositionString, + const TextRangeArray* aRanges) +{ + RefPtr<Selection> selection = aEditorBase.GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + + nsresult rv = selection->StartBatchChanges(); + NS_ENSURE_SUCCESS(rv, rv); + + // First, remove all selections of IME composition. + static const RawSelectionType kIMESelections[] = { + nsISelectionController::SELECTION_IME_RAWINPUT, + nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT, + nsISelectionController::SELECTION_IME_CONVERTEDTEXT, + nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT + }; + + nsCOMPtr<nsISelectionController> selCon; + aEditorBase.GetSelectionController(getter_AddRefs(selCon)); + NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED); + + for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) { + nsCOMPtr<nsISelection> selectionOfIME; + if (NS_FAILED(selCon->GetSelection(kIMESelections[i], + getter_AddRefs(selectionOfIME)))) { + continue; + } + rv = selectionOfIME->RemoveAllRanges(); + NS_ASSERTION(NS_SUCCEEDED(rv), + "Failed to remove all ranges of IME selection"); + } + + // Set caret position and selection of IME composition with TextRangeArray. + bool setCaret = false; + uint32_t countOfRanges = aRanges ? aRanges->Length() : 0; + +#ifdef DEBUG + // Bounds-checking on debug builds + uint32_t maxOffset = aTextNode->Length(); +#endif + + // NOTE: composition string may be truncated when it's committed and + // maxlength attribute value doesn't allow input of all text of this + // composition. + for (uint32_t i = 0; i < countOfRanges; ++i) { + const TextRange& textRange = aRanges->ElementAt(i); + + // Caret needs special handling since its length may be 0 and if it's not + // specified explicitly, we need to handle it ourselves later. + if (textRange.mRangeType == TextRangeType::eCaret) { + NS_ASSERTION(!setCaret, "The ranges already has caret position"); + NS_ASSERTION(!textRange.Length(), + "EditorBase doesn't support wide caret"); + int32_t caretOffset = static_cast<int32_t>( + aOffsetInNode + + std::min(textRange.mStartOffset, aLengthOfCompositionString)); + MOZ_ASSERT(caretOffset >= 0 && + static_cast<uint32_t>(caretOffset) <= maxOffset); + rv = selection->Collapse(aTextNode, caretOffset); + setCaret = setCaret || NS_SUCCEEDED(rv); + if (NS_WARN_IF(!setCaret)) { + continue; + } + // If caret range is specified explicitly, we should show the caret if + // it should be so. + aEditorBase.HideCaret(false); + continue; + } + + // If the clause length is 0, it should be a bug. + if (!textRange.Length()) { + NS_WARNING("Any clauses must not be empty"); + continue; + } + + RefPtr<nsRange> clauseRange; + int32_t startOffset = static_cast<int32_t>( + aOffsetInNode + + std::min(textRange.mStartOffset, aLengthOfCompositionString)); + MOZ_ASSERT(startOffset >= 0 && + static_cast<uint32_t>(startOffset) <= maxOffset); + int32_t endOffset = static_cast<int32_t>( + aOffsetInNode + + std::min(textRange.mEndOffset, aLengthOfCompositionString)); + MOZ_ASSERT(endOffset >= startOffset && + static_cast<uint32_t>(endOffset) <= maxOffset); + rv = nsRange::CreateRange(aTextNode, startOffset, + aTextNode, endOffset, + getter_AddRefs(clauseRange)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to create a DOM range for a clause of composition"); + break; + } + + // Set the range of the clause to selection. + nsCOMPtr<nsISelection> selectionOfIME; + rv = selCon->GetSelection(ToRawSelectionType(textRange.mRangeType), + getter_AddRefs(selectionOfIME)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to get IME selection"); + break; + } + + rv = selectionOfIME->AddRange(clauseRange); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to add selection range for a clause of composition"); + break; + } + + // Set the style of the clause. + nsCOMPtr<nsISelectionPrivate> selectionOfIMEPriv = + do_QueryInterface(selectionOfIME); + if (!selectionOfIMEPriv) { + NS_WARNING("Failed to get nsISelectionPrivate interface from selection"); + continue; // Since this is additional feature, we can continue this job. + } + rv = selectionOfIMEPriv->SetTextRangeStyle(clauseRange, + textRange.mRangeStyle); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to set selection style"); + break; // but this is unexpected... + } + } + + // If the ranges doesn't include explicit caret position, let's set the + // caret to the end of composition string. + if (!setCaret) { + int32_t caretOffset = + static_cast<int32_t>(aOffsetInNode + aLengthOfCompositionString); + MOZ_ASSERT(caretOffset >= 0 && + static_cast<uint32_t>(caretOffset) <= maxOffset); + rv = selection->Collapse(aTextNode, caretOffset); + NS_ASSERTION(NS_SUCCEEDED(rv), + "Failed to set caret at the end of composition string"); + + // If caret range isn't specified explicitly, we should hide the caret. + // Hiding the caret benefits a Windows build (see bug 555642 comment #6). + // However, when there is no range, we should keep showing caret. + if (countOfRanges) { + aEditorBase.HideCaret(true); + } + } + + rv = selection->EndBatchChangesInternal(); + NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to end batch changes"); + + return rv; +} + +} // namespace mozilla diff --git a/editor/libeditor/CompositionTransaction.h b/editor/libeditor/CompositionTransaction.h new file mode 100644 index 000000000..acb3d8beb --- /dev/null +++ b/editor/libeditor/CompositionTransaction.h @@ -0,0 +1,104 @@ +/* -*- 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/. */ + +#ifndef CompositionTransaction_h +#define CompositionTransaction_h + +#include "mozilla/EditTransactionBase.h" // base class +#include "nsCycleCollectionParticipant.h" // various macros +#include "nsString.h" // mStringToInsert + +#define NS_IMETEXTTXN_IID \ + { 0xb391355d, 0x346c, 0x43d1, \ + { 0x85, 0xed, 0x9e, 0x65, 0xbe, 0xe7, 0x7e, 0x48 } } + +namespace mozilla { + +class EditorBase; +class RangeUpdater; +class TextRangeArray; + +namespace dom { +class Text; +} // namespace dom + +/** + * CompositionTransaction stores all edit for a composition, i.e., + * from compositionstart event to compositionend event. E.g., inserting a + * composition string, modifying the composition string or its IME selection + * ranges and commit or cancel the composition. + */ +class CompositionTransaction final : public EditTransactionBase +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMETEXTTXN_IID) + + /** + * @param aTextNode The start node of text content. + * @param aOffset The location in aTextNode to do the insertion. + * @param aReplaceLength The length of text to replace. 0 means not + * replacing existing text. + * @param aTextRangeArray Clauses and/or caret information. This may be + * null. + * @param aString The new text to insert. + * @param aEditorBase Used to get and set the selection. + * @param aRangeUpdater The range updater + */ + CompositionTransaction(dom::Text& aTextNode, + uint32_t aOffset, uint32_t aReplaceLength, + TextRangeArray* aTextRangeArray, + const nsAString& aString, + EditorBase& aEditorBase, + RangeUpdater* aRangeUpdater); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CompositionTransaction, + EditTransactionBase) + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override; + + void MarkFixed(); + + static nsresult SetIMESelection(EditorBase& aEditorBase, + dom::Text* aTextNode, + uint32_t aOffsetInNode, + uint32_t aLengthOfCompositionString, + const TextRangeArray* aRanges); + +private: + ~CompositionTransaction(); + + nsresult SetSelectionForRanges(); + + // The text element to operate upon. + RefPtr<dom::Text> mTextNode; + + // The offsets into mTextNode where the insertion should be placed. + uint32_t mOffset; + + uint32_t mReplaceLength; + + // The range list. + RefPtr<TextRangeArray> mRanges; + + // The text to insert into mTextNode at mOffset. + nsString mStringToInsert; + + // The editor, which is used to get the selection controller. + EditorBase& mEditorBase; + + RangeUpdater* mRangeUpdater; + + bool mFixed; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(CompositionTransaction, NS_IMETEXTTXN_IID) + +} // namespace mozilla + +#endif // #ifndef CompositionTransaction_h diff --git a/editor/libeditor/CreateElementTransaction.cpp b/editor/libeditor/CreateElementTransaction.cpp new file mode 100644 index 000000000..5e4bd961c --- /dev/null +++ b/editor/libeditor/CreateElementTransaction.cpp @@ -0,0 +1,147 @@ +/* -*- 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 "CreateElementTransaction.h" + +#include <algorithm> +#include <stdio.h> + +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Selection.h" + +#include "mozilla/Casting.h" +#include "mozilla/EditorBase.h" + +#include "nsAlgorithm.h" +#include "nsAString.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIContent.h" +#include "nsIDOMCharacterData.h" +#include "nsIEditor.h" +#include "nsINode.h" +#include "nsISupportsUtils.h" +#include "nsMemory.h" +#include "nsReadableUtils.h" +#include "nsStringFwd.h" +#include "nsString.h" + +namespace mozilla { + +using namespace dom; + +CreateElementTransaction::CreateElementTransaction(EditorBase& aEditorBase, + nsIAtom& aTag, + nsINode& aParent, + int32_t aOffsetInParent) + : EditTransactionBase() + , mEditorBase(&aEditorBase) + , mTag(&aTag) + , mParent(&aParent) + , mOffsetInParent(aOffsetInParent) +{ +} + +CreateElementTransaction::~CreateElementTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(CreateElementTransaction, + EditTransactionBase, + mParent, + mNewNode, + mRefNode) + +NS_IMPL_ADDREF_INHERITED(CreateElementTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(CreateElementTransaction, EditTransactionBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CreateElementTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + + +NS_IMETHODIMP +CreateElementTransaction::DoTransaction() +{ + MOZ_ASSERT(mEditorBase && mTag && mParent); + + mNewNode = mEditorBase->CreateHTMLContent(mTag); + NS_ENSURE_STATE(mNewNode); + + // Try to insert formatting whitespace for the new node: + mEditorBase->MarkNodeDirty(GetAsDOMNode(mNewNode)); + + // Insert the new node + ErrorResult rv; + if (mOffsetInParent == -1) { + mParent->AppendChild(*mNewNode, rv); + return rv.StealNSResult(); + } + + mOffsetInParent = std::min(mOffsetInParent, + static_cast<int32_t>(mParent->GetChildCount())); + + // Note, it's ok for mRefNode to be null. That means append + mRefNode = mParent->GetChildAt(mOffsetInParent); + + nsCOMPtr<nsIContent> refNode = mRefNode; + mParent->InsertBefore(*mNewNode, refNode, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + + // Only set selection to insertion point if editor gives permission + if (!mEditorBase->GetShouldTxnSetSelection()) { + // Do nothing - DOM range gravity will adjust selection + return NS_OK; + } + + RefPtr<Selection> selection = mEditorBase->GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + rv = selection->CollapseNative(mParent, mParent->IndexOf(mNewNode) + 1); + NS_ASSERTION(!rv.Failed(), + "selection could not be collapsed after insert"); + return NS_OK; +} + +NS_IMETHODIMP +CreateElementTransaction::UndoTransaction() +{ + MOZ_ASSERT(mEditorBase && mParent); + + ErrorResult rv; + mParent->RemoveChild(*mNewNode, rv); + + return rv.StealNSResult(); +} + +NS_IMETHODIMP +CreateElementTransaction::RedoTransaction() +{ + MOZ_ASSERT(mEditorBase && mParent); + + // First, reset mNewNode so it has no attributes or content + // XXX We never actually did this, we only cleared mNewNode's contents if it + // was a CharacterData node (which it's not, it's an Element) + + // Now, reinsert mNewNode + ErrorResult rv; + nsCOMPtr<nsIContent> refNode = mRefNode; + mParent->InsertBefore(*mNewNode, refNode, rv); + return rv.StealNSResult(); +} + +NS_IMETHODIMP +CreateElementTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("CreateElementTransaction: "); + aString += nsDependentAtomString(mTag); + return NS_OK; +} + +already_AddRefed<Element> +CreateElementTransaction::GetNewNode() +{ + return nsCOMPtr<Element>(mNewNode).forget(); +} + +} // namespace mozilla diff --git a/editor/libeditor/CreateElementTransaction.h b/editor/libeditor/CreateElementTransaction.h new file mode 100644 index 000000000..70fecceae --- /dev/null +++ b/editor/libeditor/CreateElementTransaction.h @@ -0,0 +1,80 @@ +/* -*- 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/. */ + +#ifndef CreateElementTransaction_h +#define CreateElementTransaction_h + +#include "mozilla/EditTransactionBase.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" + +class nsIAtom; +class nsIContent; +class nsINode; + +/** + * A transaction that creates a new node in the content tree. + */ +namespace mozilla { + +class EditorBase; +namespace dom { +class Element; +} // namespace dom + +class CreateElementTransaction final : public EditTransactionBase +{ +public: + /** + * Initialize the transaction. + * @param aEditorBase The provider of basic editing functionality. + * @param aTag The tag (P, HR, TABLE, etc.) for the new element. + * @param aParent The node into which the new element will be + * inserted. + * @param aOffsetInParent The location in aParent to insert the new element. + * If eAppend, the new element is appended as the last + * child. + */ + CreateElementTransaction(EditorBase& aEditorBase, + nsIAtom& aTag, + nsINode& aParent, + int32_t aOffsetInParent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CreateElementTransaction, + EditTransactionBase) + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + + already_AddRefed<dom::Element> GetNewNode(); + +protected: + virtual ~CreateElementTransaction(); + + // The document into which the new node will be inserted. + EditorBase* mEditorBase; + + // The tag (mapping to object type) for the new element. + nsCOMPtr<nsIAtom> mTag; + + // The node into which the new node will be inserted. + nsCOMPtr<nsINode> mParent; + + // The index in mParent for the new node. + int32_t mOffsetInParent; + + // The new node to insert. + nsCOMPtr<dom::Element> mNewNode; + + // The node we will insert mNewNode before. We compute this ourselves. + nsCOMPtr<nsIContent> mRefNode; +}; + +} // namespace mozilla + +#endif // #ifndef CreateElementTransaction_h diff --git a/editor/libeditor/DeleteNodeTransaction.cpp b/editor/libeditor/DeleteNodeTransaction.cpp new file mode 100644 index 000000000..7f485b066 --- /dev/null +++ b/editor/libeditor/DeleteNodeTransaction.cpp @@ -0,0 +1,123 @@ +/* -*- 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 "DeleteNodeTransaction.h" +#include "mozilla/EditorBase.h" +#include "mozilla/SelectionState.h" // RangeUpdater +#include "nsDebug.h" +#include "nsError.h" +#include "nsAString.h" + +namespace mozilla { + +DeleteNodeTransaction::DeleteNodeTransaction() + : mEditorBase(nullptr) + , mRangeUpdater(nullptr) +{ +} + +DeleteNodeTransaction::~DeleteNodeTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteNodeTransaction, EditTransactionBase, + mNode, + mParent, + mRefNode) + +NS_IMPL_ADDREF_INHERITED(DeleteNodeTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(DeleteNodeTransaction, EditTransactionBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteNodeTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +nsresult +DeleteNodeTransaction::Init(EditorBase* aEditorBase, + nsINode* aNode, + RangeUpdater* aRangeUpdater) +{ + NS_ENSURE_TRUE(aEditorBase && aNode, NS_ERROR_NULL_POINTER); + mEditorBase = aEditorBase; + mNode = aNode; + mParent = aNode->GetParentNode(); + + // do nothing if the node has a parent and it's read-only + NS_ENSURE_TRUE(!mParent || mEditorBase->IsModifiableNode(mParent), + NS_ERROR_FAILURE); + + mRangeUpdater = aRangeUpdater; + return NS_OK; +} + +NS_IMETHODIMP +DeleteNodeTransaction::DoTransaction() +{ + NS_ENSURE_TRUE(mNode, NS_ERROR_NOT_INITIALIZED); + + if (!mParent) { + // this is a no-op, there's no parent to delete mNode from + return NS_OK; + } + + // remember which child mNode was (by remembering which child was next); + // mRefNode can be null + mRefNode = mNode->GetNextSibling(); + + // give range updater a chance. SelAdjDeleteNode() needs to be called + // *before* we do the action, unlike some of the other RangeItem update + // methods. + if (mRangeUpdater) { + mRangeUpdater->SelAdjDeleteNode(mNode->AsDOMNode()); + } + + ErrorResult error; + mParent->RemoveChild(*mNode, error); + return error.StealNSResult(); +} + +NS_IMETHODIMP +DeleteNodeTransaction::UndoTransaction() +{ + if (!mParent) { + // this is a legal state, the txn is a no-op + return NS_OK; + } + if (!mNode) { + return NS_ERROR_NULL_POINTER; + } + + ErrorResult error; + nsCOMPtr<nsIContent> refNode = mRefNode; + mParent->InsertBefore(*mNode, refNode, error); + return error.StealNSResult(); +} + +NS_IMETHODIMP +DeleteNodeTransaction::RedoTransaction() +{ + if (!mParent) { + // this is a legal state, the txn is a no-op + return NS_OK; + } + if (!mNode) { + return NS_ERROR_NULL_POINTER; + } + + if (mRangeUpdater) { + mRangeUpdater->SelAdjDeleteNode(mNode->AsDOMNode()); + } + + ErrorResult error; + mParent->RemoveChild(*mNode, error); + return error.StealNSResult(); +} + +NS_IMETHODIMP +DeleteNodeTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("DeleteNodeTransaction"); + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/DeleteNodeTransaction.h b/editor/libeditor/DeleteNodeTransaction.h new file mode 100644 index 000000000..d0bc0dd46 --- /dev/null +++ b/editor/libeditor/DeleteNodeTransaction.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +#ifndef DeleteNodeTransaction_h +#define DeleteNodeTransaction_h + +#include "mozilla/EditTransactionBase.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIContent.h" +#include "nsINode.h" +#include "nsISupportsImpl.h" +#include "nscore.h" + +namespace mozilla { + +class EditorBase; +class RangeUpdater; + +/** + * A transaction that deletes a single element + */ +class DeleteNodeTransaction final : public EditTransactionBase +{ +public: + /** + * Initialize the transaction. + * @param aElement The node to delete. + */ + nsresult Init(EditorBase* aEditorBase, nsINode* aNode, + RangeUpdater* aRangeUpdater); + + DeleteNodeTransaction(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteNodeTransaction, + EditTransactionBase) + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + +protected: + virtual ~DeleteNodeTransaction(); + + // The element to delete. + nsCOMPtr<nsINode> mNode; + + // Parent of node to delete. + nsCOMPtr<nsINode> mParent; + + // Next sibling to remember for undo/redo purposes. + nsCOMPtr<nsIContent> mRefNode; + + // The editor for this transaction. + EditorBase* mEditorBase; + + // Range updater object. + RangeUpdater* mRangeUpdater; +}; + +} // namespace mozilla + +#endif // #ifndef DeleteNodeTransaction_h diff --git a/editor/libeditor/DeleteRangeTransaction.cpp b/editor/libeditor/DeleteRangeTransaction.cpp new file mode 100644 index 000000000..977de4873 --- /dev/null +++ b/editor/libeditor/DeleteRangeTransaction.cpp @@ -0,0 +1,239 @@ +/* -*- 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 "DeleteRangeTransaction.h" + +#include "DeleteNodeTransaction.h" +#include "DeleteTextTransaction.h" +#include "mozilla/Assertions.h" +#include "mozilla/EditorBase.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/mozalloc.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIContent.h" +#include "nsIContentIterator.h" +#include "nsIDOMCharacterData.h" +#include "nsINode.h" +#include "nsAString.h" + +namespace mozilla { + +using namespace dom; + +// note that aEditorBase is not refcounted +DeleteRangeTransaction::DeleteRangeTransaction() + : mEditorBase(nullptr) + , mRangeUpdater(nullptr) +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteRangeTransaction, + EditAggregateTransaction, + mRange) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteRangeTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditAggregateTransaction) + +nsresult +DeleteRangeTransaction::Init(EditorBase* aEditorBase, + nsRange* aRange, + RangeUpdater* aRangeUpdater) +{ + MOZ_ASSERT(aEditorBase && aRange); + + mEditorBase = aEditorBase; + mRange = aRange->CloneRange(); + mRangeUpdater = aRangeUpdater; + + NS_ENSURE_TRUE(mEditorBase->IsModifiableNode(mRange->GetStartParent()), + NS_ERROR_FAILURE); + NS_ENSURE_TRUE(mEditorBase->IsModifiableNode(mRange->GetEndParent()), + NS_ERROR_FAILURE); + NS_ENSURE_TRUE(mEditorBase->IsModifiableNode(mRange->GetCommonAncestor()), + NS_ERROR_FAILURE); + + return NS_OK; +} + +NS_IMETHODIMP +DeleteRangeTransaction::DoTransaction() +{ + MOZ_ASSERT(mRange && mEditorBase); + + // build the child transactions + nsCOMPtr<nsINode> startParent = mRange->GetStartParent(); + int32_t startOffset = mRange->StartOffset(); + nsCOMPtr<nsINode> endParent = mRange->GetEndParent(); + int32_t endOffset = mRange->EndOffset(); + MOZ_ASSERT(startParent && endParent); + + if (startParent == endParent) { + // the selection begins and ends in the same node + nsresult rv = + CreateTxnsToDeleteBetween(startParent, startOffset, endOffset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // the selection ends in a different node from where it started. delete + // the relevant content in the start node + nsresult rv = + CreateTxnsToDeleteContent(startParent, startOffset, nsIEditor::eNext); + NS_ENSURE_SUCCESS(rv, rv); + // delete the intervening nodes + rv = CreateTxnsToDeleteNodesBetween(); + NS_ENSURE_SUCCESS(rv, rv); + // delete the relevant content in the end node + rv = CreateTxnsToDeleteContent(endParent, endOffset, nsIEditor::ePrevious); + NS_ENSURE_SUCCESS(rv, rv); + } + + // if we've successfully built this aggregate transaction, then do it. + nsresult rv = EditAggregateTransaction::DoTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + + // only set selection to deletion point if editor gives permission + bool bAdjustSelection; + mEditorBase->ShouldTxnSetSelection(&bAdjustSelection); + if (bAdjustSelection) { + RefPtr<Selection> selection = mEditorBase->GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + rv = selection->Collapse(startParent, startOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + // else do nothing - dom range gravity will adjust selection + + return NS_OK; +} + +NS_IMETHODIMP +DeleteRangeTransaction::UndoTransaction() +{ + MOZ_ASSERT(mRange && mEditorBase); + + return EditAggregateTransaction::UndoTransaction(); +} + +NS_IMETHODIMP +DeleteRangeTransaction::RedoTransaction() +{ + MOZ_ASSERT(mRange && mEditorBase); + + return EditAggregateTransaction::RedoTransaction(); +} + +NS_IMETHODIMP +DeleteRangeTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("DeleteRangeTransaction"); + return NS_OK; +} + +nsresult +DeleteRangeTransaction::CreateTxnsToDeleteBetween(nsINode* aNode, + int32_t aStartOffset, + int32_t aEndOffset) +{ + // see what kind of node we have + if (aNode->IsNodeOfType(nsINode::eDATA_NODE)) { + // if the node is a chardata node, then delete chardata content + int32_t numToDel; + if (aStartOffset == aEndOffset) { + numToDel = 1; + } else { + numToDel = aEndOffset - aStartOffset; + } + + RefPtr<nsGenericDOMDataNode> charDataNode = + static_cast<nsGenericDOMDataNode*>(aNode); + + RefPtr<DeleteTextTransaction> transaction = + new DeleteTextTransaction(*mEditorBase, *charDataNode, aStartOffset, + numToDel, mRangeUpdater); + + nsresult rv = transaction->Init(); + NS_ENSURE_SUCCESS(rv, rv); + + AppendChild(transaction); + return NS_OK; + } + + nsCOMPtr<nsIContent> child = aNode->GetChildAt(aStartOffset); + NS_ENSURE_STATE(child); + + // XXX This looks odd. Only when the last transaction causes error at + // calling Init(), the result becomes error. Otherwise, always NS_OK. + nsresult rv = NS_OK; + for (int32_t i = aStartOffset; i < aEndOffset; ++i) { + RefPtr<DeleteNodeTransaction> transaction = new DeleteNodeTransaction(); + rv = transaction->Init(mEditorBase, child, mRangeUpdater); + if (NS_SUCCEEDED(rv)) { + AppendChild(transaction); + } + + child = child->GetNextSibling(); + } + + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult +DeleteRangeTransaction::CreateTxnsToDeleteContent(nsINode* aNode, + int32_t aOffset, + nsIEditor::EDirection aAction) +{ + // see what kind of node we have + if (aNode->IsNodeOfType(nsINode::eDATA_NODE)) { + // if the node is a chardata node, then delete chardata content + uint32_t start, numToDelete; + if (nsIEditor::eNext == aAction) { + start = aOffset; + numToDelete = aNode->Length() - aOffset; + } else { + start = 0; + numToDelete = aOffset; + } + + if (numToDelete) { + RefPtr<nsGenericDOMDataNode> dataNode = + static_cast<nsGenericDOMDataNode*>(aNode); + RefPtr<DeleteTextTransaction> transaction = + new DeleteTextTransaction(*mEditorBase, *dataNode, start, numToDelete, + mRangeUpdater); + + nsresult rv = transaction->Init(); + NS_ENSURE_SUCCESS(rv, rv); + + AppendChild(transaction); + } + } + + return NS_OK; +} + +nsresult +DeleteRangeTransaction::CreateTxnsToDeleteNodesBetween() +{ + nsCOMPtr<nsIContentIterator> iter = NS_NewContentSubtreeIterator(); + + nsresult rv = iter->Init(mRange); + NS_ENSURE_SUCCESS(rv, rv); + + while (!iter->IsDone()) { + nsCOMPtr<nsINode> node = iter->GetCurrentNode(); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + + RefPtr<DeleteNodeTransaction> transaction = new DeleteNodeTransaction(); + rv = transaction->Init(mEditorBase, node, mRangeUpdater); + NS_ENSURE_SUCCESS(rv, rv); + AppendChild(transaction); + + iter->Next(); + } + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/DeleteRangeTransaction.h b/editor/libeditor/DeleteRangeTransaction.h new file mode 100644 index 000000000..9b60a5ba2 --- /dev/null +++ b/editor/libeditor/DeleteRangeTransaction.h @@ -0,0 +1,78 @@ +/* -*- 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/. */ + +#ifndef DeleteRangeTransaction_h +#define DeleteRangeTransaction_h + +#include "EditAggregateTransaction.h" +#include "nsCycleCollectionParticipant.h" +#include "nsID.h" +#include "nsIEditor.h" +#include "nsISupportsImpl.h" +#include "nsRange.h" +#include "nscore.h" + +class nsINode; + +namespace mozilla { + +class EditorBase; +class RangeUpdater; + +/** + * A transaction that deletes an entire range in the content tree + */ +class DeleteRangeTransaction final : public EditAggregateTransaction +{ +public: + /** + * Initialize the transaction. + * @param aEditorBase The object providing basic editing operations. + * @param aRange The range to delete. + */ + nsresult Init(EditorBase* aEditorBase, + nsRange* aRange, + RangeUpdater* aRangeUpdater); + + DeleteRangeTransaction(); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteRangeTransaction, + EditAggregateTransaction) + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + + virtual void LastRelease() override + { + mRange = nullptr; + EditAggregateTransaction::LastRelease(); + } + +protected: + nsresult CreateTxnsToDeleteBetween(nsINode* aNode, + int32_t aStartOffset, + int32_t aEndOffset); + + nsresult CreateTxnsToDeleteNodesBetween(); + + nsresult CreateTxnsToDeleteContent(nsINode* aParent, + int32_t aOffset, + nsIEditor::EDirection aAction); + + // P1 in the range. + RefPtr<nsRange> mRange; + + // The editor for this transaction. + EditorBase* mEditorBase; + + // Range updater object. + RangeUpdater* mRangeUpdater; +}; + +} // namespace mozilla + +#endif // #ifndef DeleteRangeTransaction_h diff --git a/editor/libeditor/DeleteTextTransaction.cpp b/editor/libeditor/DeleteTextTransaction.cpp new file mode 100644 index 000000000..6de3181da --- /dev/null +++ b/editor/libeditor/DeleteTextTransaction.cpp @@ -0,0 +1,102 @@ +/* -*- 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 "DeleteTextTransaction.h" + +#include "mozilla/Assertions.h" +#include "mozilla/EditorBase.h" +#include "mozilla/SelectionState.h" +#include "mozilla/dom/Selection.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIEditor.h" +#include "nsISupportsImpl.h" +#include "nsAString.h" + +namespace mozilla { + +using namespace dom; + +DeleteTextTransaction::DeleteTextTransaction( + EditorBase& aEditorBase, + nsGenericDOMDataNode& aCharData, + uint32_t aOffset, + uint32_t aNumCharsToDelete, + RangeUpdater* aRangeUpdater) + : mEditorBase(aEditorBase) + , mCharData(&aCharData) + , mOffset(aOffset) + , mNumCharsToDelete(aNumCharsToDelete) + , mRangeUpdater(aRangeUpdater) +{ + NS_ASSERTION(mCharData->Length() >= aOffset + aNumCharsToDelete, + "Trying to delete more characters than in node"); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteTextTransaction, EditTransactionBase, + mCharData) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteTextTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +nsresult +DeleteTextTransaction::Init() +{ + // Do nothing if the node is read-only + if (!mEditorBase.IsModifiableNode(mCharData)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +DeleteTextTransaction::DoTransaction() +{ + MOZ_ASSERT(mCharData); + + // Get the text that we're about to delete + nsresult rv = mCharData->SubstringData(mOffset, mNumCharsToDelete, + mDeletedText); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = mCharData->DeleteData(mOffset, mNumCharsToDelete); + NS_ENSURE_SUCCESS(rv, rv); + + if (mRangeUpdater) { + mRangeUpdater->SelAdjDeleteText(mCharData, mOffset, mNumCharsToDelete); + } + + // Only set selection to deletion point if editor gives permission + if (mEditorBase.GetShouldTxnSetSelection()) { + RefPtr<Selection> selection = mEditorBase.GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + rv = selection->Collapse(mCharData, mOffset); + NS_ASSERTION(NS_SUCCEEDED(rv), + "Selection could not be collapsed after undo of deletetext"); + NS_ENSURE_SUCCESS(rv, rv); + } + // Else do nothing - DOM Range gravity will adjust selection + return NS_OK; +} + +//XXX: We may want to store the selection state and restore it properly. Was +// it an insertion point or an extended selection? +NS_IMETHODIMP +DeleteTextTransaction::UndoTransaction() +{ + MOZ_ASSERT(mCharData); + + return mCharData->InsertData(mOffset, mDeletedText); +} + +NS_IMETHODIMP +DeleteTextTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("DeleteTextTransaction: "); + aString += mDeletedText; + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/DeleteTextTransaction.h b/editor/libeditor/DeleteTextTransaction.h new file mode 100644 index 000000000..855d14349 --- /dev/null +++ b/editor/libeditor/DeleteTextTransaction.h @@ -0,0 +1,76 @@ +/* -*- 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/. */ + +#ifndef DeleteTextTransaction_h +#define DeleteTextTransaction_h + +#include "mozilla/EditTransactionBase.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsGenericDOMDataNode.h" +#include "nsID.h" +#include "nsString.h" +#include "nscore.h" + +namespace mozilla { + +class EditorBase; +class RangeUpdater; + +/** + * A transaction that removes text from a content node. + */ +class DeleteTextTransaction final : public EditTransactionBase +{ +public: + /** + * Initialize the transaction. + * @param aEditorBase The provider of basic editing operations. + * @param aElement The content node to remove text from. + * @param aOffset The location in aElement to begin the deletion. + * @param aNumCharsToDelete The number of characters to delete. Not the + * number of bytes! + */ + DeleteTextTransaction(EditorBase& aEditorBase, + nsGenericDOMDataNode& aCharData, + uint32_t aOffset, + uint32_t aNumCharsToDelete, + RangeUpdater* aRangeUpdater); + + nsresult Init(); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteTextTransaction, + EditTransactionBase) + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; + + NS_DECL_EDITTRANSACTIONBASE + + uint32_t GetOffset() { return mOffset; } + + uint32_t GetNumCharsToDelete() { return mNumCharsToDelete; } + +protected: + // The provider of basic editing operations. + EditorBase& mEditorBase; + + // The CharacterData node to operate upon. + RefPtr<nsGenericDOMDataNode> mCharData; + + // The offset into mCharData where the deletion is to take place. + uint32_t mOffset; + + // The number of characters to delete. + uint32_t mNumCharsToDelete; + + // The text that was deleted. + nsString mDeletedText; + + // Range updater object. + RangeUpdater* mRangeUpdater; +}; + +} // namespace mozilla + +#endif // #ifndef DeleteTextTransaction_h diff --git a/editor/libeditor/EditActionListener.h b/editor/libeditor/EditActionListener.h new file mode 100644 index 000000000..834ec76b9 --- /dev/null +++ b/editor/libeditor/EditActionListener.h @@ -0,0 +1,17 @@ +/* -*- 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/. */ + +#ifndef __editActionListener_h__ +#define __editActionListener_h__ + +class EditActionListener +{ +public: + + virtual void EditAction() = 0; + +}; + +#endif /* __editActionListener_h__ */ diff --git a/editor/libeditor/EditAggregateTransaction.cpp b/editor/libeditor/EditAggregateTransaction.cpp new file mode 100644 index 000000000..f50e8a67c --- /dev/null +++ b/editor/libeditor/EditAggregateTransaction.cpp @@ -0,0 +1,145 @@ +/* -*- 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 "EditAggregateTransaction.h" +#include "nsAString.h" +#include "nsCOMPtr.h" // for nsCOMPtr +#include "nsError.h" // for NS_OK, etc. +#include "nsISupportsUtils.h" // for NS_ADDREF +#include "nsITransaction.h" // for nsITransaction +#include "nsString.h" // for nsAutoString + +namespace mozilla { + +EditAggregateTransaction::EditAggregateTransaction() +{ +} + +EditAggregateTransaction::~EditAggregateTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(EditAggregateTransaction, + EditTransactionBase, + mChildren) + +NS_IMPL_ADDREF_INHERITED(EditAggregateTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(EditAggregateTransaction, EditTransactionBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditAggregateTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +NS_IMETHODIMP +EditAggregateTransaction::DoTransaction() +{ + // FYI: It's legal (but not very useful) to have an empty child list. + for (uint32_t i = 0, length = mChildren.Length(); i < length; ++i) { + nsITransaction *txn = mChildren[i]; + if (!txn) { + return NS_ERROR_NULL_POINTER; + } + nsresult rv = txn->DoTransaction(); + if (NS_FAILED(rv)) { + return rv; + } + } + return NS_OK; +} + +NS_IMETHODIMP +EditAggregateTransaction::UndoTransaction() +{ + // FYI: It's legal (but not very useful) to have an empty child list. + // Undo goes through children backwards. + for (uint32_t i = mChildren.Length(); i--; ) { + nsITransaction *txn = mChildren[i]; + if (!txn) { + return NS_ERROR_NULL_POINTER; + } + nsresult rv = txn->UndoTransaction(); + if (NS_FAILED(rv)) { + return rv; + } + } + return NS_OK; +} + +NS_IMETHODIMP +EditAggregateTransaction::RedoTransaction() +{ + // It's legal (but not very useful) to have an empty child list. + for (uint32_t i = 0, length = mChildren.Length(); i < length; ++i) { + nsITransaction *txn = mChildren[i]; + if (!txn) { + return NS_ERROR_NULL_POINTER; + } + nsresult rv = txn->RedoTransaction(); + if (NS_FAILED(rv)) { + return rv; + } + } + return NS_OK; +} + +NS_IMETHODIMP +EditAggregateTransaction::Merge(nsITransaction* aTransaction, + bool* aDidMerge) +{ + if (aDidMerge) { + *aDidMerge = false; + } + if (mChildren.IsEmpty()) { + return NS_OK; + } + // FIXME: Is this really intended not to loop? It looks like the code + // that used to be here sort of intended to loop, but didn't. + nsITransaction *txn = mChildren[0]; + if (!txn) { + return NS_ERROR_NULL_POINTER; + } + return txn->Merge(aTransaction, aDidMerge); +} + +NS_IMETHODIMP +EditAggregateTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("EditAggregateTransaction: "); + + if (mName) { + nsAutoString name; + mName->ToString(name); + aString += name; + } + + return NS_OK; +} + +NS_IMETHODIMP +EditAggregateTransaction::AppendChild(EditTransactionBase* aTransaction) +{ + if (!aTransaction) { + return NS_ERROR_NULL_POINTER; + } + + RefPtr<EditTransactionBase>* slot = mChildren.AppendElement(); + if (!slot) { + return NS_ERROR_OUT_OF_MEMORY; + } + + *slot = aTransaction; + return NS_OK; +} + +NS_IMETHODIMP +EditAggregateTransaction::GetName(nsIAtom** aName) +{ + if (aName && mName) { + *aName = mName; + NS_ADDREF(*aName); + return NS_OK; + } + return NS_ERROR_NULL_POINTER; +} + +} // namespace mozilla diff --git a/editor/libeditor/EditAggregateTransaction.h b/editor/libeditor/EditAggregateTransaction.h new file mode 100644 index 000000000..6ba27a01f --- /dev/null +++ b/editor/libeditor/EditAggregateTransaction.h @@ -0,0 +1,58 @@ +/* -*- 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/. */ + +#ifndef EditAggregateTransaction_h +#define EditAggregateTransaction_h + +#include "mozilla/EditTransactionBase.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIAtom.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" +#include "nscore.h" + +class nsITransaction; + +namespace mozilla { + +/** + * base class for all document editing transactions that require aggregation. + * provides a list of child transactions. + */ +class EditAggregateTransaction : public EditTransactionBase +{ +public: + EditAggregateTransaction(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(EditAggregateTransaction, + EditTransactionBase) + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override; + + /** + * Append a transaction to this aggregate. + */ + NS_IMETHOD AppendChild(EditTransactionBase* aTransaction); + + /** + * Get the name assigned to this transaction. + */ + NS_IMETHOD GetName(nsIAtom** aName); + +protected: + virtual ~EditAggregateTransaction(); + + nsTArray<RefPtr<EditTransactionBase>> mChildren; + nsCOMPtr<nsIAtom> mName; +}; + +} // namespace mozilla + +#endif // #ifndef EditAggregateTransaction_h diff --git a/editor/libeditor/EditTransactionBase.cpp b/editor/libeditor/EditTransactionBase.cpp new file mode 100644 index 000000000..2905baa8c --- /dev/null +++ b/editor/libeditor/EditTransactionBase.cpp @@ -0,0 +1,55 @@ +/* -*- 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/EditTransactionBase.h" +#include "nsError.h" +#include "nsISupportsBase.h" + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTION_CLASS(EditTransactionBase) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_0(EditTransactionBase) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditTransactionBase) + // We don't have anything to traverse, but some of our subclasses do. +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditTransactionBase) + NS_INTERFACE_MAP_ENTRY(nsITransaction) + NS_INTERFACE_MAP_ENTRY(nsPIEditorTransaction) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITransaction) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(EditTransactionBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(EditTransactionBase, + LastRelease()) + +EditTransactionBase::~EditTransactionBase() +{ +} + +NS_IMETHODIMP +EditTransactionBase::RedoTransaction() +{ + return DoTransaction(); +} + +NS_IMETHODIMP +EditTransactionBase::GetIsTransient(bool* aIsTransient) +{ + *aIsTransient = false; + + return NS_OK; +} + +NS_IMETHODIMP +EditTransactionBase::Merge(nsITransaction* aTransaction, bool* aDidMerge) +{ + *aDidMerge = false; + + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/EditTransactionBase.h b/editor/libeditor/EditTransactionBase.h new file mode 100644 index 000000000..f09449f07 --- /dev/null +++ b/editor/libeditor/EditTransactionBase.h @@ -0,0 +1,44 @@ +/* -*- 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/. */ + +#ifndef mozilla_EditTransactionBase_h +#define mozilla_EditTransactionBase_h + +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" +#include "nsITransaction.h" +#include "nsPIEditorTransaction.h" +#include "nscore.h" + +namespace mozilla { + +/** + * Base class for all document editing transactions. + */ +class EditTransactionBase : public nsITransaction + , public nsPIEditorTransaction +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(EditTransactionBase, nsITransaction) + + virtual void LastRelease() {} + + NS_IMETHOD RedoTransaction(void) override; + NS_IMETHOD GetIsTransient(bool* aIsTransient) override; + NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override; + +protected: + virtual ~EditTransactionBase(); +}; + +} // namespace mozilla + +#define NS_DECL_EDITTRANSACTIONBASE \ + NS_IMETHOD DoTransaction() override; \ + NS_IMETHOD UndoTransaction() override; \ + NS_IMETHOD GetTxnDescription(nsAString& aTransactionDescription) override; + +#endif // #ifndef mozilla_EditTransactionBase_h diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp new file mode 100644 index 000000000..13505b2d3 --- /dev/null +++ b/editor/libeditor/EditorBase.cpp @@ -0,0 +1,5244 @@ +/* -*- 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/EditorBase.h" + +#include "mozilla/DebugOnly.h" // for DebugOnly + +#include <stdio.h> // for nullptr, stdout +#include <string.h> // for strcmp + +#include "ChangeAttributeTransaction.h" // for ChangeAttributeTransaction +#include "CompositionTransaction.h" // for CompositionTransaction +#include "CreateElementTransaction.h" // for CreateElementTransaction +#include "DeleteNodeTransaction.h" // for DeleteNodeTransaction +#include "DeleteRangeTransaction.h" // for DeleteRangeTransaction +#include "DeleteTextTransaction.h" // for DeleteTextTransaction +#include "EditAggregateTransaction.h" // for EditAggregateTransaction +#include "EditorEventListener.h" // for EditorEventListener +#include "InsertNodeTransaction.h" // for InsertNodeTransaction +#include "InsertTextTransaction.h" // for InsertTextTransaction +#include "JoinNodeTransaction.h" // for JoinNodeTransaction +#include "PlaceholderTransaction.h" // for PlaceholderTransaction +#include "SplitNodeTransaction.h" // for SplitNodeTransaction +#include "StyleSheetTransactions.h" // for AddStyleSheetTransaction, etc. +#include "TextEditUtils.h" // for TextEditUtils +#include "mozFlushType.h" // for mozFlushType::Flush_Frames +#include "mozInlineSpellChecker.h" // for mozInlineSpellChecker +#include "mozilla/CheckedInt.h" // for CheckedInt +#include "mozilla/EditorUtils.h" // for AutoRules, etc. +#include "mozilla/EditTransactionBase.h" // for EditTransactionBase +#include "mozilla/IMEStateManager.h" // for IMEStateManager +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/dom/Selection.h" // for Selection, etc. +#include "mozilla/Services.h" // for GetObserverService +#include "mozilla/TextComposition.h" // for TextComposition +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement +#include "mozilla/dom/Text.h" +#include "mozilla/dom/Event.h" +#include "mozilla/mozalloc.h" // for operator new, etc. +#include "nsAString.h" // for nsAString_internal::Length, etc. +#include "nsCCUncollectableMarker.h" // for nsCCUncollectableMarker +#include "nsCaret.h" // for nsCaret +#include "nsCaseTreatment.h" +#include "nsCharTraits.h" // for NS_IS_HIGH_SURROGATE, etc. +#include "nsComponentManagerUtils.h" // for do_CreateInstance +#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle +#include "nsContentUtils.h" // for nsContentUtils +#include "nsDOMString.h" // for DOMStringIsNull +#include "nsDebug.h" // for NS_ENSURE_TRUE, etc. +#include "nsError.h" // for NS_OK, etc. +#include "nsFocusManager.h" // for nsFocusManager +#include "nsFrameSelection.h" // for nsFrameSelection +#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::dir +#include "nsIAbsorbingTransaction.h" // for nsIAbsorbingTransaction +#include "nsIAtom.h" // for nsIAtom +#include "nsIContent.h" // for nsIContent +#include "nsIDOMAttr.h" // for nsIDOMAttr +#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData +#include "nsIDOMDocument.h" // for nsIDOMDocument +#include "nsIDOMElement.h" // for nsIDOMElement +#include "nsIDOMEvent.h" // for nsIDOMEvent +#include "nsIDOMEventListener.h" // for nsIDOMEventListener +#include "nsIDOMEventTarget.h" // for nsIDOMEventTarget +#include "nsIDOMHTMLElement.h" // for nsIDOMHTMLElement +#include "nsIDOMKeyEvent.h" // for nsIDOMKeyEvent, etc. +#include "nsIDOMMozNamedAttrMap.h" // for nsIDOMMozNamedAttrMap +#include "nsIDOMMouseEvent.h" // for nsIDOMMouseEvent +#include "nsIDOMNode.h" // for nsIDOMNode, etc. +#include "nsIDOMNodeList.h" // for nsIDOMNodeList +#include "nsIDOMText.h" // for nsIDOMText +#include "nsIDocument.h" // for nsIDocument +#include "nsIDocumentStateListener.h" // for nsIDocumentStateListener +#include "nsIEditActionListener.h" // for nsIEditActionListener +#include "nsIEditorObserver.h" // for nsIEditorObserver +#include "nsIEditorSpellCheck.h" // for nsIEditorSpellCheck +#include "nsIFrame.h" // for nsIFrame +#include "nsIHTMLDocument.h" // for nsIHTMLDocument +#include "nsIInlineSpellChecker.h" // for nsIInlineSpellChecker, etc. +#include "nsNameSpaceManager.h" // for kNameSpaceID_None, etc. +#include "nsINode.h" // for nsINode, etc. +#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc. +#include "nsIPresShell.h" // for nsIPresShell +#include "nsISelectionController.h" // for nsISelectionController, etc. +#include "nsISelectionDisplay.h" // for nsISelectionDisplay, etc. +#include "nsISupportsBase.h" // for nsISupports +#include "nsISupportsUtils.h" // for NS_ADDREF, NS_IF_ADDREF +#include "nsITransaction.h" // for nsITransaction +#include "nsITransactionManager.h" +#include "nsIWeakReference.h" // for nsISupportsWeakReference +#include "nsIWidget.h" // for nsIWidget, IMEState, etc. +#include "nsPIDOMWindow.h" // for nsPIDOMWindow +#include "nsPresContext.h" // for nsPresContext +#include "nsRange.h" // for nsRange +#include "nsReadableUtils.h" // for EmptyString, ToNewCString +#include "nsString.h" // for nsAutoString, nsString, etc. +#include "nsStringFwd.h" // for nsAFlatString +#include "nsStyleConsts.h" // for NS_STYLE_DIRECTION_RTL, etc. +#include "nsStyleContext.h" // for nsStyleContext +#include "nsStyleStruct.h" // for nsStyleDisplay, nsStyleText, etc. +#include "nsStyleStructFwd.h" // for nsIFrame::StyleUIReset, etc. +#include "nsTextNode.h" // for nsTextNode +#include "nsThreadUtils.h" // for nsRunnable +#include "nsTransactionManager.h" // for nsTransactionManager +#include "prtime.h" // for PR_Now + +class nsIOutputStream; +class nsIParserService; +class nsITransferable; + +#ifdef DEBUG +#include "nsIDOMHTMLDocument.h" // for nsIDOMHTMLDocument +#endif + +// Defined in nsEditorRegistration.cpp +extern nsIParserService *sParserService; + +namespace mozilla { + +using namespace dom; +using namespace widget; + +/***************************************************************************** + * mozilla::EditorBase + *****************************************************************************/ + +EditorBase::EditorBase() + : mPlaceHolderName(nullptr) + , mSelState(nullptr) + , mPhonetic(nullptr) + , mModCount(0) + , mFlags(0) + , mUpdateCount(0) + , mPlaceHolderBatch(0) + , mAction(EditAction::none) + , mIMETextOffset(0) + , mIMETextLength(0) + , mDirection(eNone) + , mDocDirtyState(-1) + , mSpellcheckCheckboxState(eTriUnset) + , mShouldTxnSetSelection(true) + , mDidPreDestroy(false) + , mDidPostCreate(false) + , mDispatchInputEvent(true) + , mIsInEditAction(false) + , mHidingCaret(false) +{ +} + +EditorBase::~EditorBase() +{ + NS_ASSERTION(!mDocWeak || mDidPreDestroy, "Why PreDestroy hasn't been called?"); + + if (mComposition) { + mComposition->OnEditorDestroyed(); + mComposition = nullptr; + } + // If this editor is still hiding the caret, we need to restore it. + HideCaret(false); + mTxnMgr = nullptr; + + delete mPhonetic; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(EditorBase) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EditorBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInlineSpellChecker) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTxnMgr) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIMETextNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionListeners) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditorObservers) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocStateListeners) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventListener) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSavedSel); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRangeUpdater); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditorBase) + nsIDocument* currentDoc = + tmp->mRootElement ? tmp->mRootElement->GetUncomposedDoc() : nullptr; + if (currentDoc && + nsCCUncollectableMarker::InGeneration(cb, currentDoc->GetMarkedCCGeneration())) { + return NS_SUCCESS_INTERRUPTED_TRAVERSE; + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineSpellChecker) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTxnMgr) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMETextNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionListeners) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditorObservers) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocStateListeners) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSavedSel); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRangeUpdater); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditorBase) + NS_INTERFACE_MAP_ENTRY(nsIPhonetic) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIEditorIMESupport) + NS_INTERFACE_MAP_ENTRY(nsIEditor) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditor) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorBase) + + +NS_IMETHODIMP +EditorBase::Init(nsIDOMDocument* aDoc, + nsIContent* aRoot, + nsISelectionController* aSelCon, + uint32_t aFlags, + const nsAString& aValue) +{ + MOZ_ASSERT(mAction == EditAction::none, + "Initializing during an edit action is an error"); + MOZ_ASSERT(aDoc); + if (!aDoc) + return NS_ERROR_NULL_POINTER; + + // First only set flags, but other stuff shouldn't be initialized now. + // Don't move this call after initializing mDocWeak. + // SetFlags() can check whether it's called during initialization or not by + // them. Note that SetFlags() will be called by PostCreate(). +#ifdef DEBUG + nsresult rv = +#endif + SetFlags(aFlags); + NS_ASSERTION(NS_SUCCEEDED(rv), "SetFlags() failed"); + + mDocWeak = do_GetWeakReference(aDoc); // weak reference to doc + // HTML editors currently don't have their own selection controller, + // so they'll pass null as aSelCon, and we'll get the selection controller + // off of the presshell. + nsCOMPtr<nsISelectionController> selCon; + if (aSelCon) { + mSelConWeak = do_GetWeakReference(aSelCon); // weak reference to selectioncontroller + selCon = aSelCon; + } else { + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + selCon = do_QueryInterface(presShell); + } + NS_ASSERTION(selCon, "Selection controller should be available at this point"); + + //set up root element if we are passed one. + if (aRoot) + mRootElement = do_QueryInterface(aRoot); + + mUpdateCount=0; + + // If this is an editor for <input> or <textarea>, mIMETextNode is always + // recreated with same content. Therefore, we need to forget mIMETextNode, + // but we need to keep storing mIMETextOffset and mIMETextLength becuase + // they are necessary to restore IME selection and replacing composing string + // when this receives eCompositionChange event next time. + if (mIMETextNode && !mIMETextNode->IsInComposedDoc()) { + mIMETextNode = nullptr; + } + + /* Show the caret */ + selCon->SetCaretReadOnly(false); + selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON); + + selCon->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);//we want to see all the selection reflected to user + + NS_POSTCONDITION(mDocWeak, "bad state"); + + // Make sure that the editor will be destroyed properly + mDidPreDestroy = false; + // Make sure that the ediotr will be created properly + mDidPostCreate = false; + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::PostCreate() +{ + // Synchronize some stuff for the flags. SetFlags() will initialize + // something by the flag difference. This is first time of that, so, all + // initializations must be run. For such reason, we need to invert mFlags + // value first. + mFlags = ~mFlags; + nsresult rv = SetFlags(~mFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // These operations only need to happen on the first PostCreate call + if (!mDidPostCreate) { + mDidPostCreate = true; + + // Set up listeners + CreateEventListeners(); + rv = InstallEventListeners(); + NS_ENSURE_SUCCESS(rv, rv); + + // nuke the modification count, so the doc appears unmodified + // do this before we notify listeners + ResetModificationCount(); + + // update the UI with our state + NotifyDocumentListeners(eDocumentCreated); + NotifyDocumentListeners(eDocumentStateChanged); + } + + // update nsTextStateManager and caret if we have focus + nsCOMPtr<nsIContent> focusedContent = GetFocusedContent(); + if (focusedContent) { + nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(focusedContent); + if (target) { + InitializeSelection(target); + } + + // If the text control gets reframed during focus, Focus() would not be + // called, so take a chance here to see if we need to spell check the text + // control. + EditorEventListener* listener = + reinterpret_cast<EditorEventListener*>(mEventListener.get()); + listener->SpellCheckIfNeeded(); + + IMEState newState; + rv = GetPreferredIMEState(&newState); + NS_ENSURE_SUCCESS(rv, NS_OK); + nsCOMPtr<nsIContent> content = GetFocusedContentForIME(); + IMEStateManager::UpdateIMEState(newState, content, *this); + } + + // FYI: This call might cause destroying this editor. + IMEStateManager::OnEditorInitialized(this); + + return NS_OK; +} + +void +EditorBase::CreateEventListeners() +{ + // Don't create the handler twice + if (!mEventListener) { + mEventListener = new EditorEventListener(); + } +} + +nsresult +EditorBase::InstallEventListeners() +{ + NS_ENSURE_TRUE(mDocWeak && mEventListener, + NS_ERROR_NOT_INITIALIZED); + + // Initialize the event target. + nsCOMPtr<nsIContent> rootContent = GetRoot(); + NS_ENSURE_TRUE(rootContent, NS_ERROR_NOT_AVAILABLE); + mEventTarget = do_QueryInterface(rootContent->GetParent()); + NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_AVAILABLE); + + EditorEventListener* listener = + reinterpret_cast<EditorEventListener*>(mEventListener.get()); + nsresult rv = listener->Connect(this); + if (mComposition) { + // Restart to handle composition with new editor contents. + mComposition->StartHandlingComposition(this); + } + return rv; +} + +void +EditorBase::RemoveEventListeners() +{ + if (!mDocWeak || !mEventListener) { + return; + } + reinterpret_cast<EditorEventListener*>(mEventListener.get())->Disconnect(); + if (mComposition) { + // Even if this is called, don't release mComposition because this is + // may be reused after reframing. + mComposition->EndHandlingComposition(this); + } + mEventTarget = nullptr; +} + +bool +EditorBase::GetDesiredSpellCheckState() +{ + // Check user override on this element + if (mSpellcheckCheckboxState != eTriUnset) { + return (mSpellcheckCheckboxState == eTriTrue); + } + + // Check user preferences + int32_t spellcheckLevel = Preferences::GetInt("layout.spellcheckDefault", 1); + + if (!spellcheckLevel) { + return false; // Spellchecking forced off globally + } + + if (!CanEnableSpellCheck()) { + return false; + } + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + if (presShell) { + nsPresContext* context = presShell->GetPresContext(); + if (context && !context->IsDynamic()) { + return false; + } + } + + // Check DOM state + nsCOMPtr<nsIContent> content = GetExposedRoot(); + if (!content) { + return false; + } + + nsCOMPtr<nsIDOMHTMLElement> element = do_QueryInterface(content); + if (!element) { + return false; + } + + if (!IsPlaintextEditor()) { + // Some of the page content might be editable and some not, if spellcheck= + // is explicitly set anywhere, so if there's anything editable on the page, + // return true and let the spellchecker figure it out. + nsCOMPtr<nsIHTMLDocument> doc = do_QueryInterface(content->GetUncomposedDoc()); + return doc && doc->IsEditingOn(); + } + + bool enable; + element->GetSpellcheck(&enable); + + return enable; +} + +NS_IMETHODIMP +EditorBase::PreDestroy(bool aDestroyingFrames) +{ + if (mDidPreDestroy) + return NS_OK; + + IMEStateManager::OnEditorDestroying(this); + + // Let spellchecker clean up its observers etc. It is important not to + // actually free the spellchecker here, since the spellchecker could have + // caused flush notifications, which could have gotten here if a textbox + // is being removed. Setting the spellchecker to nullptr could free the + // object that is still in use! It will be freed when the editor is + // destroyed. + if (mInlineSpellChecker) + mInlineSpellChecker->Cleanup(aDestroyingFrames); + + // tell our listeners that the doc is going away + NotifyDocumentListeners(eDocumentToBeDestroyed); + + // Unregister event listeners + RemoveEventListeners(); + // If this editor is still hiding the caret, we need to restore it. + HideCaret(false); + mActionListeners.Clear(); + mEditorObservers.Clear(); + mDocStateListeners.Clear(); + mInlineSpellChecker = nullptr; + mSpellcheckCheckboxState = eTriUnset; + mRootElement = nullptr; + + mDidPreDestroy = true; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetFlags(uint32_t* aFlags) +{ + *aFlags = mFlags; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetFlags(uint32_t aFlags) +{ + if (mFlags == aFlags) { + return NS_OK; + } + + bool spellcheckerWasEnabled = CanEnableSpellCheck(); + mFlags = aFlags; + + if (!mDocWeak) { + // If we're initializing, we shouldn't do anything now. + // SetFlags() will be called by PostCreate(), + // we should synchronize some stuff for the flags at that time. + return NS_OK; + } + + // The flag change may cause the spellchecker state change + if (CanEnableSpellCheck() != spellcheckerWasEnabled) { + nsresult rv = SyncRealTimeSpell(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // If this is called from PostCreate(), it will update the IME state if it's + // necessary. + if (!mDidPostCreate) { + return NS_OK; + } + + // Might be changing editable state, so, we need to reset current IME state + // if we're focused and the flag change causes IME state change. + nsCOMPtr<nsIContent> focusedContent = GetFocusedContent(); + if (focusedContent) { + IMEState newState; + nsresult rv = GetPreferredIMEState(&newState); + if (NS_SUCCEEDED(rv)) { + // NOTE: When the enabled state isn't going to be modified, this method + // is going to do nothing. + nsCOMPtr<nsIContent> content = GetFocusedContentForIME(); + IMEStateManager::UpdateIMEState(newState, content, *this); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetIsSelectionEditable(bool* aIsSelectionEditable) +{ + NS_ENSURE_ARG_POINTER(aIsSelectionEditable); + + // get current selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // XXX we just check that the anchor node is editable at the moment + // we should check that all nodes in the selection are editable + nsCOMPtr<nsINode> anchorNode = selection->GetAnchorNode(); + *aIsSelectionEditable = anchorNode && IsEditable(anchorNode); + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetIsDocumentEditable(bool* aIsDocumentEditable) +{ + NS_ENSURE_ARG_POINTER(aIsDocumentEditable); + nsCOMPtr<nsIDocument> doc = GetDocument(); + *aIsDocumentEditable = !!doc; + + return NS_OK; +} + +already_AddRefed<nsIDocument> +EditorBase::GetDocument() +{ + NS_PRECONDITION(mDocWeak, "bad state, mDocWeak weak pointer not initialized"); + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + return doc.forget(); +} + +already_AddRefed<nsIDOMDocument> +EditorBase::GetDOMDocument() +{ + NS_PRECONDITION(mDocWeak, "bad state, mDocWeak weak pointer not initialized"); + nsCOMPtr<nsIDOMDocument> doc = do_QueryReferent(mDocWeak); + return doc.forget(); +} + +NS_IMETHODIMP +EditorBase::GetDocument(nsIDOMDocument** aDoc) +{ + *aDoc = GetDOMDocument().take(); + return *aDoc ? NS_OK : NS_ERROR_NOT_INITIALIZED; +} + +already_AddRefed<nsIPresShell> +EditorBase::GetPresShell() +{ + NS_PRECONDITION(mDocWeak, "bad state, null mDocWeak"); + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, nullptr); + nsCOMPtr<nsIPresShell> ps = doc->GetShell(); + return ps.forget(); +} + +already_AddRefed<nsIWidget> +EditorBase::GetWidget() +{ + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, nullptr); + nsPresContext* pc = ps->GetPresContext(); + NS_ENSURE_TRUE(pc, nullptr); + nsCOMPtr<nsIWidget> widget = pc->GetRootWidget(); + NS_ENSURE_TRUE(widget.get(), nullptr); + return widget.forget(); +} + +NS_IMETHODIMP +EditorBase::GetContentsMIMEType(char** aContentsMIMEType) +{ + NS_ENSURE_ARG_POINTER(aContentsMIMEType); + *aContentsMIMEType = ToNewCString(mContentMIMEType); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetContentsMIMEType(const char* aContentsMIMEType) +{ + mContentMIMEType.Assign(aContentsMIMEType ? aContentsMIMEType : ""); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetSelectionController(nsISelectionController** aSel) +{ + NS_ENSURE_TRUE(aSel, NS_ERROR_NULL_POINTER); + *aSel = nullptr; // init out param + nsCOMPtr<nsISelectionController> selCon; + if (mSelConWeak) { + selCon = do_QueryReferent(mSelConWeak); + } else { + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + selCon = do_QueryInterface(presShell); + } + if (!selCon) { + return NS_ERROR_NOT_INITIALIZED; + } + NS_ADDREF(*aSel = selCon); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::DeleteSelection(EDirection aAction, + EStripWrappers aStripWrappers) +{ + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + return DeleteSelectionImpl(aAction, aStripWrappers); +} + +NS_IMETHODIMP +EditorBase::GetSelection(nsISelection** aSelection) +{ + return GetSelection(SelectionType::eNormal, aSelection); +} + +nsresult +EditorBase::GetSelection(SelectionType aSelectionType, + nsISelection** aSelection) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + *aSelection = nullptr; + nsCOMPtr<nsISelectionController> selcon; + GetSelectionController(getter_AddRefs(selcon)); + if (!selcon) { + return NS_ERROR_NOT_INITIALIZED; + } + return selcon->GetSelection(ToRawSelectionType(aSelectionType), aSelection); +} + +Selection* +EditorBase::GetSelection(SelectionType aSelectionType) +{ + nsCOMPtr<nsISelection> sel; + nsresult rv = GetSelection(aSelectionType, getter_AddRefs(sel)); + if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(!sel)) { + return nullptr; + } + + return sel->AsSelection(); +} + +NS_IMETHODIMP +EditorBase::DoTransaction(nsITransaction* aTxn) +{ + if (mPlaceHolderBatch && !mPlaceHolderTxn) { + nsCOMPtr<nsIAbsorbingTransaction> placeholderTransaction = + new PlaceholderTransaction(); + + // Save off weak reference to placeholder transaction + mPlaceHolderTxn = do_GetWeakReference(placeholderTransaction); + placeholderTransaction->Init(mPlaceHolderName, mSelState, this); + // placeholder txn took ownership of this pointer + mSelState = nullptr; + + // QI to an nsITransaction since that's what DoTransaction() expects + nsCOMPtr<nsITransaction> transaction = + do_QueryInterface(placeholderTransaction); + // We will recurse, but will not hit this case in the nested call + DoTransaction(transaction); + + if (mTxnMgr) { + nsCOMPtr<nsITransaction> topTxn = mTxnMgr->PeekUndoStack(); + if (topTxn) { + placeholderTransaction = do_QueryInterface(topTxn); + if (placeholderTransaction) { + // there is a placeholder transaction on top of the undo stack. It + // is either the one we just created, or an earlier one that we are + // now merging into. From here on out remember this placeholder + // instead of the one we just created. + mPlaceHolderTxn = do_GetWeakReference(placeholderTransaction); + } + } + } + } + + if (aTxn) { + // XXX: Why are we doing selection specific batching stuff here? + // XXX: Most entry points into the editor have auto variables that + // XXX: should trigger Begin/EndUpdateViewBatch() calls that will make + // XXX: these selection batch calls no-ops. + // XXX: + // XXX: I suspect that this was placed here to avoid multiple + // XXX: selection changed notifications from happening until after + // XXX: the transaction was done. I suppose that can still happen + // XXX: if an embedding application called DoTransaction() directly + // XXX: to pump its own transactions through the system, but in that + // XXX: case, wouldn't we want to use Begin/EndUpdateViewBatch() or + // XXX: its auto equivalent AutoUpdateViewBatch to ensure that + // XXX: selection listeners have access to accurate frame data? + // XXX: + // XXX: Note that if we did add Begin/EndUpdateViewBatch() calls + // XXX: we will need to make sure that they are disabled during + // XXX: the init of the editor for text widgets to avoid layout + // XXX: re-entry during initial reflow. - kin + + // get the selection and start a batch change + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + selection->StartBatchChanges(); + + nsresult rv; + if (mTxnMgr) { + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + rv = txnMgr->DoTransaction(aTxn); + } else { + rv = aTxn->DoTransaction(); + } + if (NS_SUCCEEDED(rv)) { + DoAfterDoTransaction(aTxn); + } + + // no need to check rv here, don't lose result of operation + selection->EndBatchChanges(); + + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::EnableUndo(bool aEnable) +{ + if (aEnable) { + if (!mTxnMgr) { + mTxnMgr = new nsTransactionManager(); + } + mTxnMgr->SetMaxTransactionCount(-1); + } else if (mTxnMgr) { + // disable the transaction manager if it is enabled + mTxnMgr->Clear(); + mTxnMgr->SetMaxTransactionCount(0); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetNumberOfUndoItems(int32_t* aNumItems) +{ + *aNumItems = 0; + return mTxnMgr ? mTxnMgr->GetNumberOfUndoItems(aNumItems) : NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetNumberOfRedoItems(int32_t* aNumItems) +{ + *aNumItems = 0; + return mTxnMgr ? mTxnMgr->GetNumberOfRedoItems(aNumItems) : NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetTransactionManager(nsITransactionManager** aTxnManager) +{ + NS_ENSURE_ARG_POINTER(aTxnManager); + + *aTxnManager = nullptr; + NS_ENSURE_TRUE(mTxnMgr, NS_ERROR_FAILURE); + + NS_ADDREF(*aTxnManager = mTxnMgr); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetTransactionManager(nsITransactionManager* aTxnManager) +{ + NS_ENSURE_TRUE(aTxnManager, NS_ERROR_FAILURE); + + // nsITransactionManager is builtinclass, so this is safe + mTxnMgr = static_cast<nsTransactionManager*>(aTxnManager); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::Undo(uint32_t aCount) +{ + ForceCompositionEnd(); + + bool hasTxnMgr, hasTransaction = false; + CanUndo(&hasTxnMgr, &hasTransaction); + NS_ENSURE_TRUE(hasTransaction, NS_OK); + + AutoRules beginRulesSniffing(this, EditAction::undo, nsIEditor::eNone); + + if (!mTxnMgr) { + return NS_OK; + } + + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + for (uint32_t i = 0; i < aCount; ++i) { + nsresult rv = txnMgr->UndoTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + + DoAfterUndoTransaction(); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::CanUndo(bool* aIsEnabled, + bool* aCanUndo) +{ + NS_ENSURE_TRUE(aIsEnabled && aCanUndo, NS_ERROR_NULL_POINTER); + *aIsEnabled = !!mTxnMgr; + if (*aIsEnabled) { + int32_t numTxns = 0; + mTxnMgr->GetNumberOfUndoItems(&numTxns); + *aCanUndo = !!numTxns; + } else { + *aCanUndo = false; + } + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::Redo(uint32_t aCount) +{ + bool hasTxnMgr, hasTransaction = false; + CanRedo(&hasTxnMgr, &hasTransaction); + NS_ENSURE_TRUE(hasTransaction, NS_OK); + + AutoRules beginRulesSniffing(this, EditAction::redo, nsIEditor::eNone); + + if (!mTxnMgr) { + return NS_OK; + } + + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + for (uint32_t i = 0; i < aCount; ++i) { + nsresult rv = txnMgr->RedoTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + + DoAfterRedoTransaction(); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::CanRedo(bool* aIsEnabled, bool* aCanRedo) +{ + NS_ENSURE_TRUE(aIsEnabled && aCanRedo, NS_ERROR_NULL_POINTER); + + *aIsEnabled = !!mTxnMgr; + if (*aIsEnabled) { + int32_t numTxns = 0; + mTxnMgr->GetNumberOfRedoItems(&numTxns); + *aCanRedo = !!numTxns; + } else { + *aCanRedo = false; + } + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::BeginTransaction() +{ + BeginUpdateViewBatch(); + + if (mTxnMgr) { + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + txnMgr->BeginBatch(nullptr); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::EndTransaction() +{ + if (mTxnMgr) { + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + txnMgr->EndBatch(false); + } + + EndUpdateViewBatch(); + + return NS_OK; +} + + +// These two routines are similar to the above, but do not use +// the transaction managers batching feature. Instead we use +// a placeholder transaction to wrap up any further transaction +// while the batch is open. The advantage of this is that +// placeholder transactions can later merge, if needed. Merging +// is unavailable between transaction manager batches. + +NS_IMETHODIMP +EditorBase::BeginPlaceHolderTransaction(nsIAtom* aName) +{ + NS_PRECONDITION(mPlaceHolderBatch >= 0, "negative placeholder batch count!"); + if (!mPlaceHolderBatch) { + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + // time to turn on the batch + BeginUpdateViewBatch(); + mPlaceHolderTxn = nullptr; + mPlaceHolderName = aName; + RefPtr<Selection> selection = GetSelection(); + if (selection) { + mSelState = new SelectionState(); + mSelState->SaveSelection(selection); + // Composition transaction can modify multiple nodes and it merges text + // node for ime into single text node. + // So if current selection is into IME text node, it might be failed + // to restore selection by UndoTransaction. + // So we need update selection by range updater. + if (mPlaceHolderName == nsGkAtoms::IMETxnName) { + mRangeUpdater.RegisterSelectionState(*mSelState); + } + } + } + mPlaceHolderBatch++; + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::EndPlaceHolderTransaction() +{ + NS_PRECONDITION(mPlaceHolderBatch > 0, "zero or negative placeholder batch count when ending batch!"); + if (mPlaceHolderBatch == 1) { + RefPtr<Selection> selection = GetSelection(); + + // By making the assumption that no reflow happens during the calls + // to EndUpdateViewBatch and ScrollSelectionIntoView, we are able to + // allow the selection to cache a frame offset which is used by the + // caret drawing code. We only enable this cache here; at other times, + // we have no way to know whether reflow invalidates it + // See bugs 35296 and 199412. + if (selection) { + selection->SetCanCacheFrameOffset(true); + } + + { + // Hide the caret here to avoid hiding it twice, once in EndUpdateViewBatch + // and once in ScrollSelectionIntoView. + RefPtr<nsCaret> caret; + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + + if (presShell) { + caret = presShell->GetCaret(); + } + + // time to turn off the batch + EndUpdateViewBatch(); + // make sure selection is in view + + // After ScrollSelectionIntoView(), the pending notifications might be + // flushed and PresShell/PresContext/Frames may be dead. See bug 418470. + ScrollSelectionIntoView(false); + } + + // cached for frame offset are Not available now + if (selection) { + selection->SetCanCacheFrameOffset(false); + } + + if (mSelState) { + // we saved the selection state, but never got to hand it to placeholder + // (else we ould have nulled out this pointer), so destroy it to prevent leaks. + if (mPlaceHolderName == nsGkAtoms::IMETxnName) { + mRangeUpdater.DropSelectionState(*mSelState); + } + delete mSelState; + mSelState = nullptr; + } + // We might have never made a placeholder if no action took place. + if (mPlaceHolderTxn) { + nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryReferent(mPlaceHolderTxn); + if (plcTxn) { + plcTxn->EndPlaceHolderBatch(); + } else { + // in the future we will check to make sure undo is off here, + // since that is the only known case where the placeholdertxn would disappear on us. + // For now just removing the assert. + } + // notify editor observers of action but if composing, it's done by + // compositionchange event handler. + if (!mComposition) { + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + } + } else { + NotifyEditorObservers(eNotifyEditorObserversOfCancel); + } + } + mPlaceHolderBatch--; + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::ShouldTxnSetSelection(bool* aResult) +{ + NS_ENSURE_TRUE(aResult, NS_ERROR_NULL_POINTER); + *aResult = mShouldTxnSetSelection; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetShouldTxnSetSelection(bool aShould) +{ + mShouldTxnSetSelection = aShould; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetDocumentIsEmpty(bool* aDocumentIsEmpty) +{ + *aDocumentIsEmpty = true; + + dom::Element* root = GetRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER); + + *aDocumentIsEmpty = !root->HasChildren(); + return NS_OK; +} + +// XXX: The rule system should tell us which node to select all on (ie, the +// root, or the body) +NS_IMETHODIMP +EditorBase::SelectAll() +{ + if (!mDocWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + ForceCompositionEnd(); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + return SelectEntireDocument(selection); +} + +NS_IMETHODIMP +EditorBase::BeginningOfDocument() +{ + if (!mDocWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + + // get the selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + + // get the root element + dom::Element* rootElement = GetRoot(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER); + + // find first editable thingy + nsCOMPtr<nsINode> firstNode = GetFirstEditableNode(rootElement); + if (!firstNode) { + // just the root node, set selection to inside the root + return selection->CollapseNative(rootElement, 0); + } + + if (firstNode->NodeType() == nsIDOMNode::TEXT_NODE) { + // If firstNode is text, set selection to beginning of the text node. + return selection->CollapseNative(firstNode, 0); + } + + // Otherwise, it's a leaf node and we set the selection just in front of it. + nsCOMPtr<nsIContent> parent = firstNode->GetParent(); + if (!parent) { + return NS_ERROR_NULL_POINTER; + } + + int32_t offsetInParent = parent->IndexOf(firstNode); + return selection->CollapseNative(parent, offsetInParent); +} + +NS_IMETHODIMP +EditorBase::EndOfDocument() +{ + NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED); + + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // get the root element + nsINode* node = GetRoot(); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + nsINode* child = node->GetLastChild(); + + while (child && IsContainer(child->AsDOMNode())) { + node = child; + child = node->GetLastChild(); + } + + uint32_t length = node->Length(); + return selection->CollapseNative(node, int32_t(length)); +} + +NS_IMETHODIMP +EditorBase::GetDocumentModified(bool* outDocModified) +{ + NS_ENSURE_TRUE(outDocModified, NS_ERROR_NULL_POINTER); + + int32_t modCount = 0; + GetModificationCount(&modCount); + + *outDocModified = (modCount != 0); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetDocumentCharacterSet(nsACString& characterSet) +{ + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED); + + characterSet = doc->GetDocumentCharacterSet(); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetDocumentCharacterSet(const nsACString& characterSet) +{ + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED); + + doc->SetDocumentCharacterSet(characterSet); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::Cut() +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanCut(bool* aCanCut) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::Copy() +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanCopy(bool* aCanCut) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanDelete(bool* aCanDelete) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::Paste(int32_t aSelectionType) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::PasteTransferable(nsITransferable* aTransferable) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanPaste(int32_t aSelectionType, bool* aCanPaste) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanPasteTransferable(nsITransferable* aTransferable, + bool* aCanPaste) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::SetAttribute(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIAtom> attribute = NS_Atomize(aAttribute); + + RefPtr<ChangeAttributeTransaction> transaction = + CreateTxnForSetAttribute(*element, *attribute, aValue); + return DoTransaction(transaction); +} + +NS_IMETHODIMP +EditorBase::GetAttributeValue(nsIDOMElement* aElement, + const nsAString& aAttribute, + nsAString& aResultValue, + bool* aResultIsSet) +{ + NS_ENSURE_TRUE(aResultIsSet, NS_ERROR_NULL_POINTER); + *aResultIsSet = false; + if (!aElement) { + return NS_OK; + } + nsAutoString value; + nsresult rv = aElement->GetAttribute(aAttribute, value); + NS_ENSURE_SUCCESS(rv, rv); + if (!DOMStringIsNull(value)) { + *aResultIsSet = true; + aResultValue = value; + } + return rv; +} + +NS_IMETHODIMP +EditorBase::RemoveAttribute(nsIDOMElement* aElement, + const nsAString& aAttribute) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIAtom> attribute = NS_Atomize(aAttribute); + + RefPtr<ChangeAttributeTransaction> transaction = + CreateTxnForRemoveAttribute(*element, *attribute); + return DoTransaction(transaction); +} + +bool +EditorBase::OutputsMozDirty() +{ + // Return true for Composer (!eEditorAllowInteraction) or mail + // (eEditorMailMask), but false for webpages. + return !(mFlags & nsIPlaintextEditor::eEditorAllowInteraction) || + (mFlags & nsIPlaintextEditor::eEditorMailMask); +} + +NS_IMETHODIMP +EditorBase::MarkNodeDirty(nsIDOMNode* aNode) +{ + // Mark the node dirty, but not for webpages (bug 599983) + if (!OutputsMozDirty()) { + return NS_OK; + } + nsCOMPtr<dom::Element> element = do_QueryInterface(aNode); + if (element) { + element->SetAttr(kNameSpaceID_None, nsGkAtoms::mozdirty, + EmptyString(), false); + } + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetInlineSpellChecker(bool autoCreate, + nsIInlineSpellChecker** aInlineSpellChecker) +{ + NS_ENSURE_ARG_POINTER(aInlineSpellChecker); + + if (mDidPreDestroy) { + // Don't allow people to get or create the spell checker once the editor + // is going away. + *aInlineSpellChecker = nullptr; + return autoCreate ? NS_ERROR_NOT_AVAILABLE : NS_OK; + } + + // We don't want to show the spell checking UI if there are no spell check dictionaries available. + bool canSpell = mozInlineSpellChecker::CanEnableInlineSpellChecking(); + if (!canSpell) { + *aInlineSpellChecker = nullptr; + return NS_ERROR_FAILURE; + } + + nsresult rv; + if (!mInlineSpellChecker && autoCreate) { + mInlineSpellChecker = do_CreateInstance(MOZ_INLINESPELLCHECKER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mInlineSpellChecker) { + rv = mInlineSpellChecker->Init(this); + if (NS_FAILED(rv)) { + mInlineSpellChecker = nullptr; + } + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_IF_ADDREF(*aInlineSpellChecker = mInlineSpellChecker); + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SyncRealTimeSpell() +{ + bool enable = GetDesiredSpellCheckState(); + + // Initializes mInlineSpellChecker + nsCOMPtr<nsIInlineSpellChecker> spellChecker; + GetInlineSpellChecker(enable, getter_AddRefs(spellChecker)); + + if (mInlineSpellChecker) { + // We might have a mInlineSpellChecker even if there are no dictionaries + // available since we don't destroy the mInlineSpellChecker when the last + // dictionariy is removed, but in that case spellChecker is null + mInlineSpellChecker->SetEnableRealTimeSpell(enable && spellChecker); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetSpellcheckUserOverride(bool enable) +{ + mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse; + + return SyncRealTimeSpell(); +} + +NS_IMETHODIMP +EditorBase::CreateNode(const nsAString& aTag, + nsIDOMNode* aParent, + int32_t aPosition, + nsIDOMNode** aNewNode) +{ + nsCOMPtr<nsIAtom> tag = NS_Atomize(aTag); + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + NS_ENSURE_STATE(parent); + *aNewNode = GetAsDOMNode(CreateNode(tag, parent, aPosition).take()); + NS_ENSURE_STATE(*aNewNode); + return NS_OK; +} + +already_AddRefed<Element> +EditorBase::CreateNode(nsIAtom* aTag, + nsINode* aParent, + int32_t aPosition) +{ + MOZ_ASSERT(aTag && aParent); + + AutoRules beginRulesSniffing(this, EditAction::createNode, nsIEditor::eNext); + + for (auto& listener : mActionListeners) { + listener->WillCreateNode(nsDependentAtomString(aTag), + GetAsDOMNode(aParent), aPosition); + } + + nsCOMPtr<Element> ret; + + RefPtr<CreateElementTransaction> transaction = + CreateTxnForCreateElement(*aTag, *aParent, aPosition); + nsresult rv = DoTransaction(transaction); + if (NS_SUCCEEDED(rv)) { + ret = transaction->GetNewNode(); + MOZ_ASSERT(ret); + } + + mRangeUpdater.SelAdjCreateNode(aParent, aPosition); + + for (auto& listener : mActionListeners) { + listener->DidCreateNode(nsDependentAtomString(aTag), GetAsDOMNode(ret), + GetAsDOMNode(aParent), aPosition, rv); + } + + return ret.forget(); +} + +NS_IMETHODIMP +EditorBase::InsertNode(nsIDOMNode* aNode, + nsIDOMNode* aParent, + int32_t aPosition) +{ + nsCOMPtr<nsIContent> node = do_QueryInterface(aNode); + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + NS_ENSURE_TRUE(node && parent, NS_ERROR_NULL_POINTER); + + return InsertNode(*node, *parent, aPosition); +} + +nsresult +EditorBase::InsertNode(nsIContent& aNode, + nsINode& aParent, + int32_t aPosition) +{ + AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext); + + for (auto& listener : mActionListeners) { + listener->WillInsertNode(aNode.AsDOMNode(), aParent.AsDOMNode(), + aPosition); + } + + RefPtr<InsertNodeTransaction> transaction = + CreateTxnForInsertNode(aNode, aParent, aPosition); + nsresult rv = DoTransaction(transaction); + + mRangeUpdater.SelAdjInsertNode(aParent.AsDOMNode(), aPosition); + + for (auto& listener : mActionListeners) { + listener->DidInsertNode(aNode.AsDOMNode(), aParent.AsDOMNode(), aPosition, + rv); + } + + return rv; +} + +NS_IMETHODIMP +EditorBase::SplitNode(nsIDOMNode* aNode, + int32_t aOffset, + nsIDOMNode** aNewLeftNode) +{ + nsCOMPtr<nsIContent> node = do_QueryInterface(aNode); + NS_ENSURE_STATE(node); + ErrorResult rv; + nsCOMPtr<nsIContent> newNode = SplitNode(*node, aOffset, rv); + *aNewLeftNode = GetAsDOMNode(newNode.forget().take()); + return rv.StealNSResult(); +} + +nsIContent* +EditorBase::SplitNode(nsIContent& aNode, + int32_t aOffset, + ErrorResult& aResult) +{ + AutoRules beginRulesSniffing(this, EditAction::splitNode, nsIEditor::eNext); + + for (auto& listener : mActionListeners) { + listener->WillSplitNode(aNode.AsDOMNode(), aOffset); + } + + RefPtr<SplitNodeTransaction> transaction = + CreateTxnForSplitNode(aNode, aOffset); + aResult = DoTransaction(transaction); + + nsCOMPtr<nsIContent> newNode = aResult.Failed() ? nullptr + : transaction->GetNewNode(); + + mRangeUpdater.SelAdjSplitNode(aNode, aOffset, newNode); + + nsresult rv = aResult.StealNSResult(); + for (auto& listener : mActionListeners) { + listener->DidSplitNode(aNode.AsDOMNode(), aOffset, GetAsDOMNode(newNode), + rv); + } + // Note: result might be a success code, so we can't use Throw() to + // set it on aResult. + aResult = rv; + + return newNode; +} + +NS_IMETHODIMP +EditorBase::JoinNodes(nsIDOMNode* aLeftNode, + nsIDOMNode* aRightNode, + nsIDOMNode*) +{ + nsCOMPtr<nsINode> leftNode = do_QueryInterface(aLeftNode); + nsCOMPtr<nsINode> rightNode = do_QueryInterface(aRightNode); + NS_ENSURE_STATE(leftNode && rightNode && leftNode->GetParentNode()); + return JoinNodes(*leftNode, *rightNode); +} + +nsresult +EditorBase::JoinNodes(nsINode& aLeftNode, + nsINode& aRightNode) +{ + nsCOMPtr<nsINode> parent = aLeftNode.GetParentNode(); + MOZ_ASSERT(parent); + + AutoRules beginRulesSniffing(this, EditAction::joinNode, + nsIEditor::ePrevious); + + // Remember some values; later used for saved selection updating. + // Find the offset between the nodes to be joined. + int32_t offset = parent->IndexOf(&aRightNode); + // Find the number of children of the lefthand node + uint32_t oldLeftNodeLen = aLeftNode.Length(); + + for (auto& listener : mActionListeners) { + listener->WillJoinNodes(aLeftNode.AsDOMNode(), aRightNode.AsDOMNode(), + parent->AsDOMNode()); + } + + nsresult rv = NS_OK; + RefPtr<JoinNodeTransaction> transaction = + CreateTxnForJoinNode(aLeftNode, aRightNode); + if (transaction) { + rv = DoTransaction(transaction); + } + + mRangeUpdater.SelAdjJoinNodes(aLeftNode, aRightNode, *parent, offset, + (int32_t)oldLeftNodeLen); + + for (auto& listener : mActionListeners) { + listener->DidJoinNodes(aLeftNode.AsDOMNode(), aRightNode.AsDOMNode(), + parent->AsDOMNode(), rv); + } + + return rv; +} + +NS_IMETHODIMP +EditorBase::DeleteNode(nsIDOMNode* aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_STATE(node); + return DeleteNode(node); +} + +nsresult +EditorBase::DeleteNode(nsINode* aNode) +{ + AutoRules beginRulesSniffing(this, EditAction::createNode, + nsIEditor::ePrevious); + + // save node location for selection updating code. + for (auto& listener : mActionListeners) { + listener->WillDeleteNode(aNode->AsDOMNode()); + } + + RefPtr<DeleteNodeTransaction> transaction; + nsresult rv = CreateTxnForDeleteNode(aNode, getter_AddRefs(transaction)); + if (NS_SUCCEEDED(rv)) { + rv = DoTransaction(transaction); + } + + for (auto& listener : mActionListeners) { + listener->DidDeleteNode(aNode->AsDOMNode(), rv); + } + + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +/** + * ReplaceContainer() replaces inNode with a new node (outNode) which is + * constructed to be of type aNodeType. Put inNodes children into outNode. + * Callers responsibility to make sure inNode's children can go in outNode. + */ +already_AddRefed<Element> +EditorBase::ReplaceContainer(Element* aOldContainer, + nsIAtom* aNodeType, + nsIAtom* aAttribute, + const nsAString* aValue, + ECloneAttributes aCloneAttributes) +{ + MOZ_ASSERT(aOldContainer && aNodeType); + + nsCOMPtr<nsIContent> parent = aOldContainer->GetParent(); + NS_ENSURE_TRUE(parent, nullptr); + + int32_t offset = parent->IndexOf(aOldContainer); + + // create new container + nsCOMPtr<Element> ret = CreateHTMLContent(aNodeType); + NS_ENSURE_TRUE(ret, nullptr); + + // set attribute if needed + if (aAttribute && aValue && aAttribute != nsGkAtoms::_empty) { + nsresult rv = ret->SetAttr(kNameSpaceID_None, aAttribute, *aValue, true); + NS_ENSURE_SUCCESS(rv, nullptr); + } + if (aCloneAttributes == eCloneAttributes) { + CloneAttributes(ret, aOldContainer); + } + + // notify our internal selection state listener + // (Note: An AutoSelectionRestorer object must be created + // before calling this to initialize mRangeUpdater) + AutoReplaceContainerSelNotify selStateNotify(mRangeUpdater, aOldContainer, + ret); + { + AutoTransactionsConserveSelection conserveSelection(this); + while (aOldContainer->HasChildren()) { + nsCOMPtr<nsIContent> child = aOldContainer->GetFirstChild(); + + nsresult rv = DeleteNode(child); + NS_ENSURE_SUCCESS(rv, nullptr); + + rv = InsertNode(*child, *ret, -1); + NS_ENSURE_SUCCESS(rv, nullptr); + } + } + + // insert new container into tree + nsresult rv = InsertNode(*ret, *parent, offset); + NS_ENSURE_SUCCESS(rv, nullptr); + + // delete old container + rv = DeleteNode(aOldContainer); + NS_ENSURE_SUCCESS(rv, nullptr); + + return ret.forget(); +} + +/** + * RemoveContainer() removes inNode, reparenting its children (if any) into the + * parent of inNode. + */ +nsresult +EditorBase::RemoveContainer(nsIContent* aNode) +{ + MOZ_ASSERT(aNode); + + nsCOMPtr<nsINode> parent = aNode->GetParentNode(); + NS_ENSURE_STATE(parent); + + int32_t offset = parent->IndexOf(aNode); + + // Loop through the children of inNode and promote them into inNode's parent + uint32_t nodeOrigLen = aNode->GetChildCount(); + + // notify our internal selection state listener + AutoRemoveContainerSelNotify selNotify(mRangeUpdater, aNode, parent, + offset, nodeOrigLen); + + while (aNode->HasChildren()) { + nsCOMPtr<nsIContent> child = aNode->GetLastChild(); + nsresult rv = DeleteNode(child); + NS_ENSURE_SUCCESS(rv, rv); + + rv = InsertNode(*child, *parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + } + + return DeleteNode(aNode); +} + +/** + * InsertContainerAbove() inserts a new parent for inNode, which is contructed + * to be of type aNodeType. outNode becomes a child of inNode's earlier + * parent. Caller's responsibility to make sure inNode's can be child of + * outNode, and outNode can be child of old parent. + */ +already_AddRefed<Element> +EditorBase::InsertContainerAbove(nsIContent* aNode, + nsIAtom* aNodeType, + nsIAtom* aAttribute, + const nsAString* aValue) +{ + MOZ_ASSERT(aNode && aNodeType); + + nsCOMPtr<nsIContent> parent = aNode->GetParent(); + NS_ENSURE_TRUE(parent, nullptr); + int32_t offset = parent->IndexOf(aNode); + + // Create new container + nsCOMPtr<Element> newContent = CreateHTMLContent(aNodeType); + NS_ENSURE_TRUE(newContent, nullptr); + + // Set attribute if needed + if (aAttribute && aValue && aAttribute != nsGkAtoms::_empty) { + nsresult rv = + newContent->SetAttr(kNameSpaceID_None, aAttribute, *aValue, true); + NS_ENSURE_SUCCESS(rv, nullptr); + } + + // Notify our internal selection state listener + AutoInsertContainerSelNotify selNotify(mRangeUpdater); + + // Put inNode in new parent, outNode + nsresult rv = DeleteNode(aNode); + NS_ENSURE_SUCCESS(rv, nullptr); + + { + AutoTransactionsConserveSelection conserveSelection(this); + rv = InsertNode(*aNode, *newContent, 0); + NS_ENSURE_SUCCESS(rv, nullptr); + } + + // Put new parent in doc + rv = InsertNode(*newContent, *parent, offset); + NS_ENSURE_SUCCESS(rv, nullptr); + + return newContent.forget(); +} + +/** + * MoveNode() moves aNode to {aParent,aOffset}. + */ +nsresult +EditorBase::MoveNode(nsIContent* aNode, + nsINode* aParent, + int32_t aOffset) +{ + MOZ_ASSERT(aNode); + MOZ_ASSERT(aParent); + MOZ_ASSERT(aOffset == -1 || + (0 <= aOffset && + AssertedCast<uint32_t>(aOffset) <= aParent->Length())); + + nsCOMPtr<nsINode> oldParent = aNode->GetParentNode(); + int32_t oldOffset = oldParent ? oldParent->IndexOf(aNode) : -1; + + if (aOffset == -1) { + // Magic value meaning "move to end of aParent" + aOffset = AssertedCast<int32_t>(aParent->Length()); + } + + // Don't do anything if it's already in right place + if (aParent == oldParent && aOffset == oldOffset) { + return NS_OK; + } + + // Notify our internal selection state listener + AutoMoveNodeSelNotify selNotify(mRangeUpdater, oldParent, oldOffset, + aParent, aOffset); + + // Need to adjust aOffset if we're moving aNode later in its current parent + if (aParent == oldParent && oldOffset < aOffset) { + // When we delete aNode, it will make the offsets after it off by one + aOffset--; + } + + // Hold a reference so aNode doesn't go away when we remove it (bug 772282) + nsCOMPtr<nsINode> kungFuDeathGrip = aNode; + + nsresult rv = DeleteNode(aNode); + NS_ENSURE_SUCCESS(rv, rv); + + return InsertNode(*aNode, *aParent, aOffset); +} + +NS_IMETHODIMP +EditorBase::AddEditorObserver(nsIEditorObserver* aObserver) +{ + // we don't keep ownership of the observers. They must + // remove themselves as observers before they are destroyed. + + NS_ENSURE_TRUE(aObserver, NS_ERROR_NULL_POINTER); + + // Make sure the listener isn't already on the list + if (!mEditorObservers.Contains(aObserver)) { + mEditorObservers.AppendElement(*aObserver); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::RemoveEditorObserver(nsIEditorObserver* aObserver) +{ + NS_ENSURE_TRUE(aObserver, NS_ERROR_FAILURE); + + mEditorObservers.RemoveElement(aObserver); + + return NS_OK; +} + +class EditorInputEventDispatcher final : public Runnable +{ +public: + EditorInputEventDispatcher(EditorBase* aEditorBase, + nsIContent* aTarget, + bool aIsComposing) + : mEditorBase(aEditorBase) + , mTarget(aTarget) + , mIsComposing(aIsComposing) + { + } + + NS_IMETHOD Run() override + { + // Note that we don't need to check mDispatchInputEvent here. We need + // to check it only when the editor requests to dispatch the input event. + + if (!mTarget->IsInComposedDoc()) { + return NS_OK; + } + + nsCOMPtr<nsIPresShell> ps = mEditorBase->GetPresShell(); + if (!ps) { + return NS_OK; + } + + nsCOMPtr<nsIWidget> widget = mEditorBase->GetWidget(); + if (!widget) { + return NS_OK; + } + + // Even if the change is caused by untrusted event, we need to dispatch + // trusted input event since it's a fact. + InternalEditorInputEvent inputEvent(true, eEditorInput, widget); + inputEvent.mTime = static_cast<uint64_t>(PR_Now() / 1000); + inputEvent.mIsComposing = mIsComposing; + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = + ps->HandleEventWithTarget(&inputEvent, nullptr, mTarget, &status); + NS_ENSURE_SUCCESS(rv, NS_OK); // print the warning if error + return NS_OK; + } + +private: + RefPtr<EditorBase> mEditorBase; + nsCOMPtr<nsIContent> mTarget; + bool mIsComposing; +}; + +void +EditorBase::NotifyEditorObservers(NotificationForEditorObservers aNotification) +{ + // Copy the observers since EditAction()s can modify mEditorObservers. + nsTArray<mozilla::OwningNonNull<nsIEditorObserver>> observers(mEditorObservers); + switch (aNotification) { + case eNotifyEditorObserversOfEnd: + mIsInEditAction = false; + for (auto& observer : observers) { + observer->EditAction(); + } + + if (!mDispatchInputEvent) { + return; + } + + FireInputEvent(); + break; + case eNotifyEditorObserversOfBefore: + if (NS_WARN_IF(mIsInEditAction)) { + break; + } + mIsInEditAction = true; + for (auto& observer : observers) { + observer->BeforeEditAction(); + } + break; + case eNotifyEditorObserversOfCancel: + mIsInEditAction = false; + for (auto& observer : observers) { + observer->CancelEditAction(); + } + break; + default: + MOZ_CRASH("Handle all notifications here"); + break; + } +} + +void +EditorBase::FireInputEvent() +{ + // We don't need to dispatch multiple input events if there is a pending + // input event. However, it may have different event target. If we resolved + // this issue, we need to manage the pending events in an array. But it's + // overwork. We don't need to do it for the very rare case. + + nsCOMPtr<nsIContent> target = GetInputEventTargetContent(); + NS_ENSURE_TRUE_VOID(target); + + // NOTE: Don't refer IsIMEComposing() because it returns false even before + // compositionend. However, DOM Level 3 Events defines it should be + // true after compositionstart and before compositionend. + nsContentUtils::AddScriptRunner( + new EditorInputEventDispatcher(this, target, !!GetComposition())); +} + +NS_IMETHODIMP +EditorBase::AddEditActionListener(nsIEditActionListener* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER); + + // Make sure the listener isn't already on the list + if (!mActionListeners.Contains(aListener)) { + mActionListeners.AppendElement(*aListener); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::RemoveEditActionListener(nsIEditActionListener* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_FAILURE); + + mActionListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::AddDocumentStateListener(nsIDocumentStateListener* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER); + + if (!mDocStateListeners.Contains(aListener)) { + mDocStateListeners.AppendElement(*aListener); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::RemoveDocumentStateListener(nsIDocumentStateListener* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER); + + mDocStateListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::OutputToString(const nsAString& aFormatType, + uint32_t aFlags, + nsAString& aOutputString) +{ + // these should be implemented by derived classes. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::OutputToStream(nsIOutputStream* aOutputStream, + const nsAString& aFormatType, + const nsACString& aCharsetOverride, + uint32_t aFlags) +{ + // these should be implemented by derived classes. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::DumpContentTree() +{ +#ifdef DEBUG + if (mRootElement) { + mRootElement->List(stdout); + } +#endif + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::DebugDumpContent() +{ +#ifdef DEBUG + nsCOMPtr<nsIDOMHTMLDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsIDOMHTMLElement>bodyElem; + doc->GetBody(getter_AddRefs(bodyElem)); + nsCOMPtr<nsIContent> content = do_QueryInterface(bodyElem); + if (content) { + content->List(); + } +#endif + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::DebugUnitTests(int32_t* outNumTests, + int32_t* outNumTestsFailed) +{ +#ifdef DEBUG + NS_NOTREACHED("This should never get called. Overridden by subclasses"); +#endif + return NS_OK; +} + +bool +EditorBase::ArePreservingSelection() +{ + return !(mSavedSel.IsEmpty()); +} + +void +EditorBase::PreserveSelectionAcrossActions(Selection* aSel) +{ + mSavedSel.SaveSelection(aSel); + mRangeUpdater.RegisterSelectionState(mSavedSel); +} + +nsresult +EditorBase::RestorePreservedSelection(Selection* aSel) +{ + if (mSavedSel.IsEmpty()) { + return NS_ERROR_FAILURE; + } + mSavedSel.RestoreSelection(aSel); + StopPreservingSelection(); + return NS_OK; +} + +void +EditorBase::StopPreservingSelection() +{ + mRangeUpdater.DropSelectionState(mSavedSel); + mSavedSel.MakeEmpty(); +} + +bool +EditorBase::EnsureComposition(WidgetCompositionEvent* aCompositionEvent) +{ + if (mComposition) { + return true; + } + // The compositionstart event must cause creating new TextComposition + // instance at being dispatched by IMEStateManager. + mComposition = IMEStateManager::GetTextCompositionFor(aCompositionEvent); + if (!mComposition) { + // However, TextComposition may be committed before the composition + // event comes here. + return false; + } + mComposition->StartHandlingComposition(this); + return true; +} + +nsresult +EditorBase::BeginIMEComposition(WidgetCompositionEvent* aCompositionEvent) +{ + MOZ_ASSERT(!mComposition, "There is composition already"); + if (!EnsureComposition(aCompositionEvent)) { + return NS_OK; + } + if (mPhonetic) { + mPhonetic->Truncate(0); + } + return NS_OK; +} + +void +EditorBase::EndIMEComposition() +{ + NS_ENSURE_TRUE_VOID(mComposition); // nothing to do + + // commit the IME transaction..we can get at it via the transaction mgr. + // Note that this means IME won't work without an undo stack! + if (mTxnMgr) { + nsCOMPtr<nsITransaction> txn = mTxnMgr->PeekUndoStack(); + nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryInterface(txn); + if (plcTxn) { + DebugOnly<nsresult> rv = plcTxn->Commit(); + NS_ASSERTION(NS_SUCCEEDED(rv), + "nsIAbsorbingTransaction::Commit() failed"); + } + } + + // Composition string may have hidden the caret. Therefore, we need to + // cancel it here. + HideCaret(false); + + /* reset the data we need to construct a transaction */ + mIMETextNode = nullptr; + mIMETextOffset = 0; + mIMETextLength = 0; + mComposition->EndHandlingComposition(this); + mComposition = nullptr; + + // notify editor observers of action + NotifyEditorObservers(eNotifyEditorObserversOfEnd); +} + +NS_IMETHODIMP +EditorBase::GetPhonetic(nsAString& aPhonetic) +{ + if (mPhonetic) { + aPhonetic = *mPhonetic; + } else { + aPhonetic.Truncate(0); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::ForceCompositionEnd() +{ + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + if (!ps) { + return NS_ERROR_NOT_AVAILABLE; + } + nsPresContext* pc = ps->GetPresContext(); + if (!pc) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mComposition ? + IMEStateManager::NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, pc) : NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetPreferredIMEState(IMEState* aState) +{ + NS_ENSURE_ARG_POINTER(aState); + aState->mEnabled = IMEState::ENABLED; + aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE; + + if (IsReadonly() || IsDisabled()) { + aState->mEnabled = IMEState::DISABLED; + return NS_OK; + } + + nsCOMPtr<nsIContent> content = GetRoot(); + NS_ENSURE_TRUE(content, NS_ERROR_FAILURE); + + nsIFrame* frame = content->GetPrimaryFrame(); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + + switch (frame->StyleUIReset()->mIMEMode) { + case NS_STYLE_IME_MODE_AUTO: + if (IsPasswordEditor()) + aState->mEnabled = IMEState::PASSWORD; + break; + case NS_STYLE_IME_MODE_DISABLED: + // we should use password state for |ime-mode: disabled;|. + aState->mEnabled = IMEState::PASSWORD; + break; + case NS_STYLE_IME_MODE_ACTIVE: + aState->mOpen = IMEState::OPEN; + break; + case NS_STYLE_IME_MODE_INACTIVE: + aState->mOpen = IMEState::CLOSED; + break; + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetComposing(bool* aResult) +{ + NS_ENSURE_ARG_POINTER(aResult); + *aResult = IsIMEComposing(); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetRootElement(nsIDOMElement** aRootElement) +{ + NS_ENSURE_ARG_POINTER(aRootElement); + NS_ENSURE_TRUE(mRootElement, NS_ERROR_NOT_AVAILABLE); + nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(mRootElement); + rootElement.forget(aRootElement); + return NS_OK; +} + +/** + * All editor operations which alter the doc should be prefaced + * with a call to StartOperation, naming the action and direction. + */ +NS_IMETHODIMP +EditorBase::StartOperation(EditAction opID, + nsIEditor::EDirection aDirection) +{ + mAction = opID; + mDirection = aDirection; + return NS_OK; +} + +/** + * All editor operations which alter the doc should be followed + * with a call to EndOperation. + */ +NS_IMETHODIMP +EditorBase::EndOperation() +{ + mAction = EditAction::none; + mDirection = eNone; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::CloneAttribute(const nsAString& aAttribute, + nsIDOMNode* aDestNode, + nsIDOMNode* aSourceNode) +{ + NS_ENSURE_TRUE(aDestNode && aSourceNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMElement> destElement = do_QueryInterface(aDestNode); + nsCOMPtr<nsIDOMElement> sourceElement = do_QueryInterface(aSourceNode); + NS_ENSURE_TRUE(destElement && sourceElement, NS_ERROR_NO_INTERFACE); + + nsAutoString attrValue; + bool isAttrSet; + nsresult rv = GetAttributeValue(sourceElement, + aAttribute, + attrValue, + &isAttrSet); + NS_ENSURE_SUCCESS(rv, rv); + if (isAttrSet) { + rv = SetAttribute(destElement, aAttribute, attrValue); + } else { + rv = RemoveAttribute(destElement, aAttribute); + } + + return rv; +} + +/** + * @param aDest Must be a DOM element. + * @param aSource Must be a DOM element. + */ +NS_IMETHODIMP +EditorBase::CloneAttributes(nsIDOMNode* aDest, + nsIDOMNode* aSource) +{ + NS_ENSURE_TRUE(aDest && aSource, NS_ERROR_NULL_POINTER); + + nsCOMPtr<Element> dest = do_QueryInterface(aDest); + nsCOMPtr<Element> source = do_QueryInterface(aSource); + NS_ENSURE_TRUE(dest && source, NS_ERROR_NO_INTERFACE); + + CloneAttributes(dest, source); + + return NS_OK; +} + +void +EditorBase::CloneAttributes(Element* aDest, + Element* aSource) +{ + MOZ_ASSERT(aDest && aSource); + + AutoEditBatch beginBatching(this); + + // Use transaction system for undo only if destination is already in the + // document + NS_ENSURE_TRUE(GetRoot(), ); + bool destInBody = GetRoot()->Contains(aDest); + + // Clear existing attributes + RefPtr<nsDOMAttributeMap> destAttributes = aDest->Attributes(); + while (RefPtr<Attr> attr = destAttributes->Item(0)) { + if (destInBody) { + RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)), + attr->NodeName()); + } else { + ErrorResult ignored; + aDest->RemoveAttribute(attr->NodeName(), ignored); + } + } + + // Set just the attributes that the source element has + RefPtr<nsDOMAttributeMap> sourceAttributes = aSource->Attributes(); + uint32_t sourceCount = sourceAttributes->Length(); + for (uint32_t i = 0; i < sourceCount; i++) { + RefPtr<Attr> attr = sourceAttributes->Item(i); + nsAutoString value; + attr->GetValue(value); + if (destInBody) { + SetAttributeOrEquivalent(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)), + attr->NodeName(), value, false); + } else { + // The element is not inserted in the document yet, we don't want to put + // a transaction on the UndoStack + SetAttributeOrEquivalent(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)), + attr->NodeName(), value, true); + } + } +} + +NS_IMETHODIMP +EditorBase::ScrollSelectionIntoView(bool aScrollToAnchor) +{ + nsCOMPtr<nsISelectionController> selCon; + if (NS_SUCCEEDED(GetSelectionController(getter_AddRefs(selCon))) && selCon) { + int16_t region = nsISelectionController::SELECTION_FOCUS_REGION; + + if (aScrollToAnchor) { + region = nsISelectionController::SELECTION_ANCHOR_REGION; + } + + selCon->ScrollSelectionIntoView(nsISelectionController::SELECTION_NORMAL, + region, nsISelectionController::SCROLL_OVERFLOW_HIDDEN); + } + + return NS_OK; +} + +void +EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsIDOMNode>& aNode, + int32_t& aOffset) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + FindBetterInsertionPoint(node, aOffset); + aNode = do_QueryInterface(node); +} + +void +EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode, + int32_t& aOffset) +{ + if (aNode->IsNodeOfType(nsINode::eTEXT)) { + // There is no "better" insertion point. + return; + } + + if (!IsPlaintextEditor()) { + // We cannot find "better" insertion point in HTML editor. + // WARNING: When you add some code to find better node in HTML editor, + // you need to call this before calling InsertTextImpl() in + // HTMLEditRules. + return; + } + + nsCOMPtr<nsINode> node = aNode; + int32_t offset = aOffset; + + nsCOMPtr<nsINode> root = GetRoot(); + if (aNode == root) { + // In some cases, aNode is the anonymous DIV, and offset is 0. To avoid + // injecting unneeded text nodes, we first look to see if we have one + // available. In that case, we'll just adjust node and offset accordingly. + if (!offset && node->HasChildren() && + node->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) { + aNode = node->GetFirstChild(); + aOffset = 0; + return; + } + + // In some other cases, aNode is the anonymous DIV, and offset points to the + // terminating mozBR. In that case, we'll adjust aInOutNode and + // aInOutOffset to the preceding text node, if any. + if (offset > 0 && node->GetChildAt(offset - 1) && + node->GetChildAt(offset - 1)->IsNodeOfType(nsINode::eTEXT)) { + NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX); + aNode = node->GetChildAt(offset - 1); + aOffset = static_cast<int32_t>(aNode->Length()); + return; + } + } + + // Sometimes, aNode is the mozBR element itself. In that case, we'll adjust + // the insertion point to the previous text node, if one exists, or to the + // parent anonymous DIV. + if (TextEditUtils::IsMozBR(node) && !offset) { + if (node->GetPreviousSibling() && + node->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) { + NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX); + aNode = node->GetPreviousSibling(); + aOffset = static_cast<int32_t>(aNode->Length()); + return; + } + + if (node->GetParentNode() && node->GetParentNode() == root) { + aNode = node->GetParentNode(); + aOffset = 0; + return; + } + } +} + +nsresult +EditorBase::InsertTextImpl(const nsAString& aStringToInsert, + nsCOMPtr<nsINode>* aInOutNode, + int32_t* aInOutOffset, + nsIDocument* aDoc) +{ + // NOTE: caller *must* have already used AutoTransactionsConserveSelection + // stack-based class to turn off txn selection updating. Caller also turned + // on rules sniffing if desired. + + NS_ENSURE_TRUE(aInOutNode && *aInOutNode && aInOutOffset && aDoc, + NS_ERROR_NULL_POINTER); + + if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) { + return NS_OK; + } + + // This method doesn't support over INT32_MAX length text since aInOutOffset + // is int32_t*. + CheckedInt<int32_t> lengthToInsert(aStringToInsert.Length()); + NS_ENSURE_TRUE(lengthToInsert.isValid(), NS_ERROR_INVALID_ARG); + + nsCOMPtr<nsINode> node = *aInOutNode; + int32_t offset = *aInOutOffset; + + // In some cases, the node may be the anonymous div elemnt or a mozBR + // element. Let's try to look for better insertion point in the nearest + // text node if there is. + FindBetterInsertionPoint(node, offset); + + if (ShouldHandleIMEComposition()) { + CheckedInt<int32_t> newOffset; + if (!node->IsNodeOfType(nsINode::eTEXT)) { + // create a text node + RefPtr<nsTextNode> newNode = aDoc->CreateTextNode(EmptyString()); + // then we insert it into the dom tree + nsresult rv = InsertNode(*newNode, *node, offset); + NS_ENSURE_SUCCESS(rv, rv); + node = newNode; + offset = 0; + newOffset = lengthToInsert; + } else { + newOffset = lengthToInsert + offset; + NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE); + } + nsresult rv = + InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset); + NS_ENSURE_SUCCESS(rv, rv); + offset = newOffset.value(); + } else { + if (node->IsNodeOfType(nsINode::eTEXT)) { + CheckedInt<int32_t> newOffset = lengthToInsert + offset; + NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE); + // we are inserting text into an existing text node. + nsresult rv = + InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset); + NS_ENSURE_SUCCESS(rv, rv); + offset = newOffset.value(); + } else { + // we are inserting text into a non-text node. first we have to create a + // textnode (this also populates it with the text) + RefPtr<nsTextNode> newNode = aDoc->CreateTextNode(aStringToInsert); + // then we insert it into the dom tree + nsresult rv = InsertNode(*newNode, *node, offset); + NS_ENSURE_SUCCESS(rv, rv); + node = newNode; + offset = lengthToInsert.value(); + } + } + + *aInOutNode = node; + *aInOutOffset = offset; + return NS_OK; +} + +nsresult +EditorBase::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert, + Text& aTextNode, + int32_t aOffset, + bool aSuppressIME) +{ + RefPtr<EditTransactionBase> transaction; + bool isIMETransaction = false; + RefPtr<Text> insertedTextNode = &aTextNode; + int32_t insertedOffset = aOffset; + // aSuppressIME is used when editor must insert text, yet this text is not + // part of the current IME operation. Example: adjusting whitespace around an + // IME insertion. + if (ShouldHandleIMEComposition() && !aSuppressIME) { + if (!mIMETextNode) { + mIMETextNode = &aTextNode; + mIMETextOffset = aOffset; + } + // Modify mPhonetic with raw text input clauses. + const TextRangeArray* ranges = mComposition->GetRanges(); + for (uint32_t i = 0; i < (ranges ? ranges->Length() : 0); ++i) { + const TextRange& textRange = ranges->ElementAt(i); + if (!textRange.Length() || + textRange.mRangeType != TextRangeType::eRawClause) { + continue; + } + if (!mPhonetic) { + mPhonetic = new nsString(); + } + nsAutoString stringToInsert(aStringToInsert); + stringToInsert.Mid(*mPhonetic, + textRange.mStartOffset, textRange.Length()); + } + + transaction = CreateTxnForComposition(aStringToInsert); + isIMETransaction = true; + // All characters of the composition string will be replaced with + // aStringToInsert. So, we need to emulate to remove the composition + // string. + insertedTextNode = mIMETextNode; + insertedOffset = mIMETextOffset; + mIMETextLength = aStringToInsert.Length(); + } else { + transaction = CreateTxnForInsertText(aStringToInsert, aTextNode, aOffset); + } + + // Let listeners know what's up + for (auto& listener : mActionListeners) { + listener->WillInsertText( + static_cast<nsIDOMCharacterData*>(insertedTextNode->AsDOMNode()), + insertedOffset, aStringToInsert); + } + + // XXX We may not need these view batches anymore. This is handled at a + // higher level now I believe. + BeginUpdateViewBatch(); + nsresult rv = DoTransaction(transaction); + EndUpdateViewBatch(); + + // let listeners know what happened + for (auto& listener : mActionListeners) { + listener->DidInsertText( + static_cast<nsIDOMCharacterData*>(insertedTextNode->AsDOMNode()), + insertedOffset, aStringToInsert, rv); + } + + // Added some cruft here for bug 43366. Layout was crashing because we left + // an empty text node lying around in the document. So I delete empty text + // nodes caused by IME. I have to mark the IME transaction as "fixed", which + // means that furure IME txns won't merge with it. This is because we don't + // want future IME txns trying to put their text into a node that is no + // longer in the document. This does not break undo/redo, because all these + // txns are wrapped in a parent PlaceHolder txn, and placeholder txns are + // already savvy to having multiple ime txns inside them. + + // Delete empty IME text node if there is one + if (isIMETransaction && mIMETextNode) { + uint32_t len = mIMETextNode->Length(); + if (!len) { + DeleteNode(mIMETextNode); + mIMETextNode = nullptr; + static_cast<CompositionTransaction*>(transaction.get())->MarkFixed(); + } + } + + return rv; +} + +nsresult +EditorBase::SelectEntireDocument(Selection* aSelection) +{ + if (!aSelection) { + return NS_ERROR_NULL_POINTER; + } + + nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(GetRoot()); + if (!rootElement) { + return NS_ERROR_NOT_INITIALIZED; + } + + return aSelection->SelectAllChildren(rootElement); +} + +nsINode* +EditorBase::GetFirstEditableNode(nsINode* aRoot) +{ + MOZ_ASSERT(aRoot); + + nsIContent* node = GetLeftmostChild(aRoot); + if (node && !IsEditable(node)) { + node = GetNextNode(node, /* aEditableNode = */ true); + } + + return (node != aRoot) ? node : nullptr; +} + +NS_IMETHODIMP +EditorBase::NotifyDocumentListeners( + TDocumentListenerNotification aNotificationType) +{ + if (!mDocStateListeners.Length()) { + // Maybe there just aren't any. + return NS_OK; + } + + nsTArray<OwningNonNull<nsIDocumentStateListener>> + listeners(mDocStateListeners); + nsresult rv = NS_OK; + + switch (aNotificationType) { + case eDocumentCreated: + for (auto& listener : listeners) { + rv = listener->NotifyDocumentCreated(); + if (NS_FAILED(rv)) { + break; + } + } + break; + + case eDocumentToBeDestroyed: + for (auto& listener : listeners) { + rv = listener->NotifyDocumentWillBeDestroyed(); + if (NS_FAILED(rv)) { + break; + } + } + break; + + case eDocumentStateChanged: { + bool docIsDirty; + rv = GetDocumentModified(&docIsDirty); + NS_ENSURE_SUCCESS(rv, rv); + + if (static_cast<int8_t>(docIsDirty) == mDocDirtyState) { + return NS_OK; + } + + mDocDirtyState = docIsDirty; + + for (auto& listener : listeners) { + rv = listener->NotifyDocumentStateChanged(mDocDirtyState); + if (NS_FAILED(rv)) { + break; + } + } + break; + } + default: + NS_NOTREACHED("Unknown notification"); + } + + return rv; +} + +already_AddRefed<InsertTextTransaction> +EditorBase::CreateTxnForInsertText(const nsAString& aStringToInsert, + Text& aTextNode, + int32_t aOffset) +{ + RefPtr<InsertTextTransaction> transaction = + new InsertTextTransaction(aTextNode, aOffset, aStringToInsert, *this, + &mRangeUpdater); + return transaction.forget(); +} + +nsresult +EditorBase::DeleteText(nsGenericDOMDataNode& aCharData, + uint32_t aOffset, + uint32_t aLength) +{ + RefPtr<DeleteTextTransaction> transaction = + CreateTxnForDeleteText(aCharData, aOffset, aLength); + NS_ENSURE_STATE(transaction); + + AutoRules beginRulesSniffing(this, EditAction::deleteText, + nsIEditor::ePrevious); + + // Let listeners know what's up + for (auto& listener : mActionListeners) { + listener->WillDeleteText( + static_cast<nsIDOMCharacterData*>(GetAsDOMNode(&aCharData)), aOffset, + aLength); + } + + nsresult rv = DoTransaction(transaction); + + // Let listeners know what happened + for (auto& listener : mActionListeners) { + listener->DidDeleteText( + static_cast<nsIDOMCharacterData*>(GetAsDOMNode(&aCharData)), aOffset, + aLength, rv); + } + + return rv; +} + +already_AddRefed<DeleteTextTransaction> +EditorBase::CreateTxnForDeleteText(nsGenericDOMDataNode& aCharData, + uint32_t aOffset, + uint32_t aLength) +{ + RefPtr<DeleteTextTransaction> transaction = + new DeleteTextTransaction(*this, aCharData, aOffset, aLength, + &mRangeUpdater); + nsresult rv = transaction->Init(); + NS_ENSURE_SUCCESS(rv, nullptr); + + return transaction.forget(); +} + +already_AddRefed<SplitNodeTransaction> +EditorBase::CreateTxnForSplitNode(nsIContent& aNode, + uint32_t aOffset) +{ + RefPtr<SplitNodeTransaction> transaction = + new SplitNodeTransaction(*this, aNode, aOffset); + return transaction.forget(); +} + +already_AddRefed<JoinNodeTransaction> +EditorBase::CreateTxnForJoinNode(nsINode& aLeftNode, + nsINode& aRightNode) +{ + RefPtr<JoinNodeTransaction> transaction = + new JoinNodeTransaction(*this, aLeftNode, aRightNode); + + NS_ENSURE_SUCCESS(transaction->CheckValidity(), nullptr); + + return transaction.forget(); +} + +struct SavedRange final +{ + RefPtr<Selection> mSelection; + nsCOMPtr<nsINode> mStartNode; + nsCOMPtr<nsINode> mEndNode; + int32_t mStartOffset; + int32_t mEndOffset; +}; + +nsresult +EditorBase::SplitNodeImpl(nsIContent& aExistingRightNode, + int32_t aOffset, + nsIContent& aNewLeftNode) +{ + // Remember all selection points. + AutoTArray<SavedRange, 10> savedRanges; + for (size_t i = 0; i < kPresentSelectionTypeCount; ++i) { + SelectionType selectionType(ToSelectionType(1 << i)); + SavedRange range; + range.mSelection = GetSelection(selectionType); + if (selectionType == SelectionType::eNormal) { + NS_ENSURE_TRUE(range.mSelection, NS_ERROR_NULL_POINTER); + } else if (!range.mSelection) { + // For non-normal selections, skip over the non-existing ones. + continue; + } + + for (uint32_t j = 0; j < range.mSelection->RangeCount(); ++j) { + RefPtr<nsRange> r = range.mSelection->GetRangeAt(j); + MOZ_ASSERT(r->IsPositioned()); + range.mStartNode = r->GetStartParent(); + range.mStartOffset = r->StartOffset(); + range.mEndNode = r->GetEndParent(); + range.mEndOffset = r->EndOffset(); + + savedRanges.AppendElement(range); + } + } + + nsCOMPtr<nsINode> parent = aExistingRightNode.GetParentNode(); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + + ErrorResult rv; + nsCOMPtr<nsINode> refNode = &aExistingRightNode; + parent->InsertBefore(aNewLeftNode, refNode, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + + // Split the children between the two nodes. At this point, + // aExistingRightNode has all the children. Move all the children whose + // index is < aOffset to aNewLeftNode. + if (aOffset < 0) { + // This means move no children + return NS_OK; + } + + // If it's a text node, just shuffle around some text + if (aExistingRightNode.GetAsText() && aNewLeftNode.GetAsText()) { + // Fix right node + nsAutoString leftText; + aExistingRightNode.GetAsText()->SubstringData(0, aOffset, leftText); + aExistingRightNode.GetAsText()->DeleteData(0, aOffset); + // Fix left node + aNewLeftNode.GetAsText()->SetData(leftText); + } else { + // Otherwise it's an interior node, so shuffle around the children. Go + // through list backwards so deletes don't interfere with the iteration. + nsCOMPtr<nsINodeList> childNodes = aExistingRightNode.ChildNodes(); + for (int32_t i = aOffset - 1; i >= 0; i--) { + nsCOMPtr<nsIContent> childNode = childNodes->Item(i); + if (childNode) { + aExistingRightNode.RemoveChild(*childNode, rv); + if (!rv.Failed()) { + nsCOMPtr<nsIContent> firstChild = aNewLeftNode.GetFirstChild(); + aNewLeftNode.InsertBefore(*childNode, firstChild, rv); + } + } + if (rv.Failed()) { + break; + } + } + } + + // Handle selection + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + if (ps) { + ps->FlushPendingNotifications(Flush_Frames); + } + + bool shouldSetSelection = GetShouldTxnSetSelection(); + + RefPtr<Selection> previousSelection; + for (size_t i = 0; i < savedRanges.Length(); ++i) { + // Adjust the selection if needed. + SavedRange& range = savedRanges[i]; + + // If we have not seen the selection yet, clear all of its ranges. + if (range.mSelection != previousSelection) { + nsresult rv = range.mSelection->RemoveAllRanges(); + NS_ENSURE_SUCCESS(rv, rv); + previousSelection = range.mSelection; + } + + if (shouldSetSelection && + range.mSelection->Type() == SelectionType::eNormal) { + // If the editor should adjust the selection, don't bother restoring + // the ranges for the normal selection here. + continue; + } + + // Split the selection into existing node and new node. + if (range.mStartNode == &aExistingRightNode) { + if (range.mStartOffset < aOffset) { + range.mStartNode = &aNewLeftNode; + } else { + range.mStartOffset -= aOffset; + } + } + + if (range.mEndNode == &aExistingRightNode) { + if (range.mEndOffset < aOffset) { + range.mEndNode = &aNewLeftNode; + } else { + range.mEndOffset -= aOffset; + } + } + + RefPtr<nsRange> newRange; + nsresult rv = nsRange::CreateRange(range.mStartNode, range.mStartOffset, + range.mEndNode, range.mEndOffset, + getter_AddRefs(newRange)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range.mSelection->AddRange(newRange); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (shouldSetSelection) { + // Editor wants us to set selection at split point. + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + selection->Collapse(&aNewLeftNode, aOffset); + } + + return NS_OK; +} + +nsresult +EditorBase::JoinNodesImpl(nsINode* aNodeToKeep, + nsINode* aNodeToJoin, + nsINode* aParent) +{ + MOZ_ASSERT(aNodeToKeep); + MOZ_ASSERT(aNodeToJoin); + MOZ_ASSERT(aParent); + + uint32_t firstNodeLength = aNodeToJoin->Length(); + + int32_t joinOffset; + GetNodeLocation(aNodeToJoin, &joinOffset); + int32_t keepOffset; + nsINode* parent = GetNodeLocation(aNodeToKeep, &keepOffset); + + // Remember all selection points. + AutoTArray<SavedRange, 10> savedRanges; + for (size_t i = 0; i < kPresentSelectionTypeCount; ++i) { + SelectionType selectionType(ToSelectionType(1 << i)); + SavedRange range; + range.mSelection = GetSelection(selectionType); + if (selectionType == SelectionType::eNormal) { + NS_ENSURE_TRUE(range.mSelection, NS_ERROR_NULL_POINTER); + } else if (!range.mSelection) { + // For non-normal selections, skip over the non-existing ones. + continue; + } + + for (uint32_t j = 0; j < range.mSelection->RangeCount(); ++j) { + RefPtr<nsRange> r = range.mSelection->GetRangeAt(j); + MOZ_ASSERT(r->IsPositioned()); + range.mStartNode = r->GetStartParent(); + range.mStartOffset = r->StartOffset(); + range.mEndNode = r->GetEndParent(); + range.mEndOffset = r->EndOffset(); + + // If selection endpoint is between the nodes, remember it as being + // in the one that is going away instead. This simplifies later selection + // adjustment logic at end of this method. + if (range.mStartNode) { + if (range.mStartNode == parent && + joinOffset < range.mStartOffset && + range.mStartOffset <= keepOffset) { + range.mStartNode = aNodeToJoin; + range.mStartOffset = firstNodeLength; + } + if (range.mEndNode == parent && + joinOffset < range.mEndOffset && + range.mEndOffset <= keepOffset) { + range.mEndNode = aNodeToJoin; + range.mEndOffset = firstNodeLength; + } + } + + savedRanges.AppendElement(range); + } + } + + // OK, ready to do join now. + // If it's a text node, just shuffle around some text. + nsCOMPtr<nsIDOMCharacterData> keepNodeAsText( do_QueryInterface(aNodeToKeep) ); + nsCOMPtr<nsIDOMCharacterData> joinNodeAsText( do_QueryInterface(aNodeToJoin) ); + if (keepNodeAsText && joinNodeAsText) { + nsAutoString rightText; + nsAutoString leftText; + keepNodeAsText->GetData(rightText); + joinNodeAsText->GetData(leftText); + leftText += rightText; + keepNodeAsText->SetData(leftText); + } else { + // Otherwise it's an interior node, so shuffle around the children. + nsCOMPtr<nsINodeList> childNodes = aNodeToJoin->ChildNodes(); + MOZ_ASSERT(childNodes); + + // Remember the first child in aNodeToKeep, we'll insert all the children of aNodeToJoin in front of it + // GetFirstChild returns nullptr firstNode if aNodeToKeep has no children, that's OK. + nsCOMPtr<nsIContent> firstNode = aNodeToKeep->GetFirstChild(); + + // Have to go through the list backwards to keep deletes from interfering with iteration. + for (uint32_t i = childNodes->Length(); i; --i) { + nsCOMPtr<nsIContent> childNode = childNodes->Item(i - 1); + if (childNode) { + // prepend children of aNodeToJoin + ErrorResult err; + aNodeToKeep->InsertBefore(*childNode, firstNode, err); + NS_ENSURE_TRUE(!err.Failed(), err.StealNSResult()); + firstNode = childNode.forget(); + } + } + } + + // Delete the extra node. + ErrorResult err; + aParent->RemoveChild(*aNodeToJoin, err); + + bool shouldSetSelection = GetShouldTxnSetSelection(); + + RefPtr<Selection> previousSelection; + for (size_t i = 0; i < savedRanges.Length(); ++i) { + // And adjust the selection if needed. + SavedRange& range = savedRanges[i]; + + // If we have not seen the selection yet, clear all of its ranges. + if (range.mSelection != previousSelection) { + nsresult rv = range.mSelection->RemoveAllRanges(); + NS_ENSURE_SUCCESS(rv, rv); + previousSelection = range.mSelection; + } + + if (shouldSetSelection && + range.mSelection->Type() == SelectionType::eNormal) { + // If the editor should adjust the selection, don't bother restoring + // the ranges for the normal selection here. + continue; + } + + // Check to see if we joined nodes where selection starts. + if (range.mStartNode == aNodeToJoin) { + range.mStartNode = aNodeToKeep; + } else if (range.mStartNode == aNodeToKeep) { + range.mStartOffset += firstNodeLength; + } + + // Check to see if we joined nodes where selection ends. + if (range.mEndNode == aNodeToJoin) { + range.mEndNode = aNodeToKeep; + } else if (range.mEndNode == aNodeToKeep) { + range.mEndOffset += firstNodeLength; + } + + RefPtr<nsRange> newRange; + nsresult rv = nsRange::CreateRange(range.mStartNode, range.mStartOffset, + range.mEndNode, range.mEndOffset, + getter_AddRefs(newRange)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range.mSelection->AddRange(newRange); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (shouldSetSelection) { + // Editor wants us to set selection at join point. + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + selection->Collapse(aNodeToKeep, AssertedCast<int32_t>(firstNodeLength)); + } + + return err.StealNSResult(); +} + +int32_t +EditorBase::GetChildOffset(nsIDOMNode* aChild, + nsIDOMNode* aParent) +{ + MOZ_ASSERT(aChild && aParent); + + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + nsCOMPtr<nsINode> child = do_QueryInterface(aChild); + MOZ_ASSERT(parent && child); + + int32_t idx = parent->IndexOf(child); + MOZ_ASSERT(idx != -1); + return idx; +} + +// static +already_AddRefed<nsIDOMNode> +EditorBase::GetNodeLocation(nsIDOMNode* aChild, + int32_t* outOffset) +{ + MOZ_ASSERT(aChild && outOffset); + NS_ENSURE_TRUE(aChild && outOffset, nullptr); + *outOffset = -1; + + nsCOMPtr<nsIDOMNode> parent; + + MOZ_ALWAYS_SUCCEEDS(aChild->GetParentNode(getter_AddRefs(parent))); + if (parent) { + *outOffset = GetChildOffset(aChild, parent); + } + + return parent.forget(); +} + +nsINode* +EditorBase::GetNodeLocation(nsINode* aChild, + int32_t* aOffset) +{ + MOZ_ASSERT(aChild); + MOZ_ASSERT(aOffset); + + nsINode* parent = aChild->GetParentNode(); + if (parent) { + *aOffset = parent->IndexOf(aChild); + MOZ_ASSERT(*aOffset != -1); + } else { + *aOffset = -1; + } + return parent; +} + +/** + * Returns the number of things inside aNode. If aNode is text, returns number + * of characters. If not, returns number of children nodes. + */ +nsresult +EditorBase::GetLengthOfDOMNode(nsIDOMNode* aNode, + uint32_t& aCount) +{ + aCount = 0; + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + aCount = node->Length(); + return NS_OK; +} + +nsIContent* +EditorBase::GetPriorNode(nsINode* aParentNode, + int32_t aOffset, + bool aEditableNode, + bool aNoBlockCrossing) +{ + MOZ_ASSERT(aParentNode); + + // If we are at the beginning of the node, or it is a text node, then just + // look before it. + if (!aOffset || aParentNode->NodeType() == nsIDOMNode::TEXT_NODE) { + if (aNoBlockCrossing && IsBlockNode(aParentNode)) { + // If we aren't allowed to cross blocks, don't look before this block. + return nullptr; + } + return GetPriorNode(aParentNode, aEditableNode, aNoBlockCrossing); + } + + // else look before the child at 'aOffset' + if (nsIContent* child = aParentNode->GetChildAt(aOffset)) { + return GetPriorNode(child, aEditableNode, aNoBlockCrossing); + } + + // unless there isn't one, in which case we are at the end of the node + // and want the deep-right child. + nsIContent* resultNode = GetRightmostChild(aParentNode, aNoBlockCrossing); + if (!resultNode || !aEditableNode || IsEditable(resultNode)) { + return resultNode; + } + + // restart the search from the non-editable node we just found + return GetPriorNode(resultNode, aEditableNode, aNoBlockCrossing); +} + +nsIContent* +EditorBase::GetNextNode(nsINode* aParentNode, + int32_t aOffset, + bool aEditableNode, + bool aNoBlockCrossing) +{ + MOZ_ASSERT(aParentNode); + + // if aParentNode is a text node, use its location instead + if (aParentNode->NodeType() == nsIDOMNode::TEXT_NODE) { + nsINode* parent = aParentNode->GetParentNode(); + NS_ENSURE_TRUE(parent, nullptr); + aOffset = parent->IndexOf(aParentNode) + 1; // _after_ the text node + aParentNode = parent; + } + + // look at the child at 'aOffset' + nsIContent* child = aParentNode->GetChildAt(aOffset); + if (child) { + if (aNoBlockCrossing && IsBlockNode(child)) { + return child; + } + + nsIContent* resultNode = GetLeftmostChild(child, aNoBlockCrossing); + if (!resultNode) { + return child; + } + + if (!IsDescendantOfEditorRoot(resultNode)) { + return nullptr; + } + + if (!aEditableNode || IsEditable(resultNode)) { + return resultNode; + } + + // restart the search from the non-editable node we just found + return GetNextNode(resultNode, aEditableNode, aNoBlockCrossing); + } + + // unless there isn't one, in which case we are at the end of the node + // and want the next one. + if (aNoBlockCrossing && IsBlockNode(aParentNode)) { + // don't cross out of parent block + return nullptr; + } + + return GetNextNode(aParentNode, aEditableNode, aNoBlockCrossing); +} + +nsIContent* +EditorBase::GetPriorNode(nsINode* aCurrentNode, + bool aEditableNode, + bool aNoBlockCrossing /* = false */) +{ + MOZ_ASSERT(aCurrentNode); + + if (!IsDescendantOfEditorRoot(aCurrentNode)) { + return nullptr; + } + + return FindNode(aCurrentNode, false, aEditableNode, aNoBlockCrossing); +} + +nsIContent* +EditorBase::FindNextLeafNode(nsINode* aCurrentNode, + bool aGoForward, + bool bNoBlockCrossing) +{ + // called only by GetPriorNode so we don't need to check params. + NS_PRECONDITION(IsDescendantOfEditorRoot(aCurrentNode) && + !IsEditorRoot(aCurrentNode), + "Bogus arguments"); + + nsINode* cur = aCurrentNode; + for (;;) { + // if aCurrentNode has a sibling in the right direction, return + // that sibling's closest child (or itself if it has no children) + nsIContent* sibling = + aGoForward ? cur->GetNextSibling() : cur->GetPreviousSibling(); + if (sibling) { + if (bNoBlockCrossing && IsBlockNode(sibling)) { + // don't look inside prevsib, since it is a block + return sibling; + } + nsIContent *leaf = + aGoForward ? GetLeftmostChild(sibling, bNoBlockCrossing) : + GetRightmostChild(sibling, bNoBlockCrossing); + if (!leaf) { + return sibling; + } + + return leaf; + } + + nsINode *parent = cur->GetParentNode(); + if (!parent) { + return nullptr; + } + + NS_ASSERTION(IsDescendantOfEditorRoot(parent), + "We started with a proper descendant of root, and should stop " + "if we ever hit the root, so we better have a descendant of " + "root now!"); + if (IsEditorRoot(parent) || + (bNoBlockCrossing && IsBlockNode(parent))) { + return nullptr; + } + + cur = parent; + } + + NS_NOTREACHED("What part of for(;;) do you not understand?"); + return nullptr; +} + +nsIContent* +EditorBase::GetNextNode(nsINode* aCurrentNode, + bool aEditableNode, + bool bNoBlockCrossing) +{ + MOZ_ASSERT(aCurrentNode); + + if (!IsDescendantOfEditorRoot(aCurrentNode)) { + return nullptr; + } + + return FindNode(aCurrentNode, true, aEditableNode, bNoBlockCrossing); +} + +nsIContent* +EditorBase::FindNode(nsINode* aCurrentNode, + bool aGoForward, + bool aEditableNode, + bool bNoBlockCrossing) +{ + if (IsEditorRoot(aCurrentNode)) { + // Don't allow traversal above the root node! This helps + // prevent us from accidentally editing browser content + // when the editor is in a text widget. + + return nullptr; + } + + nsCOMPtr<nsIContent> candidate = + FindNextLeafNode(aCurrentNode, aGoForward, bNoBlockCrossing); + + if (!candidate) { + return nullptr; + } + + if (!aEditableNode || IsEditable(candidate)) { + return candidate; + } + + return FindNode(candidate, aGoForward, aEditableNode, bNoBlockCrossing); +} + +nsIContent* +EditorBase::GetRightmostChild(nsINode* aCurrentNode, + bool bNoBlockCrossing) +{ + NS_ENSURE_TRUE(aCurrentNode, nullptr); + nsIContent *cur = aCurrentNode->GetLastChild(); + if (!cur) { + return nullptr; + } + for (;;) { + if (bNoBlockCrossing && IsBlockNode(cur)) { + return cur; + } + nsIContent* next = cur->GetLastChild(); + if (!next) { + return cur; + } + cur = next; + } + + NS_NOTREACHED("What part of for(;;) do you not understand?"); + return nullptr; +} + +nsIContent* +EditorBase::GetLeftmostChild(nsINode* aCurrentNode, + bool bNoBlockCrossing) +{ + NS_ENSURE_TRUE(aCurrentNode, nullptr); + nsIContent *cur = aCurrentNode->GetFirstChild(); + if (!cur) { + return nullptr; + } + for (;;) { + if (bNoBlockCrossing && IsBlockNode(cur)) { + return cur; + } + nsIContent *next = cur->GetFirstChild(); + if (!next) { + return cur; + } + cur = next; + } + + NS_NOTREACHED("What part of for(;;) do you not understand?"); + return nullptr; +} + +bool +EditorBase::IsBlockNode(nsINode* aNode) +{ + // stub to be overridden in HTMLEditor. + // screwing around with the class hierarchy here in order + // to not duplicate the code in GetNextNode/GetPrevNode + // across both EditorBase/HTMLEditor. + return false; +} + +bool +EditorBase::CanContain(nsINode& aParent, + nsIContent& aChild) +{ + switch (aParent.NodeType()) { + case nsIDOMNode::ELEMENT_NODE: + case nsIDOMNode::DOCUMENT_FRAGMENT_NODE: + return TagCanContain(*aParent.NodeInfo()->NameAtom(), aChild); + } + return false; +} + +bool +EditorBase::CanContainTag(nsINode& aParent, + nsIAtom& aChildTag) +{ + switch (aParent.NodeType()) { + case nsIDOMNode::ELEMENT_NODE: + case nsIDOMNode::DOCUMENT_FRAGMENT_NODE: + return TagCanContainTag(*aParent.NodeInfo()->NameAtom(), aChildTag); + } + return false; +} + +bool +EditorBase::TagCanContain(nsIAtom& aParentTag, + nsIContent& aChild) +{ + switch (aChild.NodeType()) { + case nsIDOMNode::TEXT_NODE: + case nsIDOMNode::ELEMENT_NODE: + case nsIDOMNode::DOCUMENT_FRAGMENT_NODE: + return TagCanContainTag(aParentTag, *aChild.NodeInfo()->NameAtom()); + } + return false; +} + +bool +EditorBase::TagCanContainTag(nsIAtom& aParentTag, + nsIAtom& aChildTag) +{ + return true; +} + +bool +EditorBase::IsRoot(nsIDOMNode* inNode) +{ + NS_ENSURE_TRUE(inNode, false); + + nsCOMPtr<nsIDOMNode> rootNode = do_QueryInterface(GetRoot()); + + return inNode == rootNode; +} + +bool +EditorBase::IsRoot(nsINode* inNode) +{ + NS_ENSURE_TRUE(inNode, false); + + nsCOMPtr<nsINode> rootNode = GetRoot(); + + return inNode == rootNode; +} + +bool +EditorBase::IsEditorRoot(nsINode* aNode) +{ + NS_ENSURE_TRUE(aNode, false); + nsCOMPtr<nsINode> rootNode = GetEditorRoot(); + return aNode == rootNode; +} + +bool +EditorBase::IsDescendantOfRoot(nsIDOMNode* inNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(inNode); + return IsDescendantOfRoot(node); +} + +bool +EditorBase::IsDescendantOfRoot(nsINode* inNode) +{ + NS_ENSURE_TRUE(inNode, false); + nsCOMPtr<nsIContent> root = GetRoot(); + NS_ENSURE_TRUE(root, false); + + return nsContentUtils::ContentIsDescendantOf(inNode, root); +} + +bool +EditorBase::IsDescendantOfEditorRoot(nsINode* aNode) +{ + NS_ENSURE_TRUE(aNode, false); + nsCOMPtr<nsIContent> root = GetEditorRoot(); + NS_ENSURE_TRUE(root, false); + + return nsContentUtils::ContentIsDescendantOf(aNode, root); +} + +bool +EditorBase::IsContainer(nsINode* aNode) +{ + return aNode ? true : false; +} + +bool +EditorBase::IsContainer(nsIDOMNode* aNode) +{ + return aNode ? true : false; +} + +static inline bool +IsElementVisible(Element* aElement) +{ + if (aElement->GetPrimaryFrame()) { + // It's visible, for our purposes + return true; + } + + nsIContent *cur = aElement; + for (;;) { + // Walk up the tree looking for the nearest ancestor with a frame. + // The state of the child right below it will determine whether + // we might possibly have a frame or not. + bool haveLazyBitOnChild = cur->HasFlag(NODE_NEEDS_FRAME); + cur = cur->GetFlattenedTreeParent(); + if (!cur) { + if (!haveLazyBitOnChild) { + // None of our ancestors have lazy bits set, so we shouldn't + // have a frame + return false; + } + + // The root has a lazy frame construction bit. We need to check + // our style. + break; + } + + if (cur->GetPrimaryFrame()) { + if (!haveLazyBitOnChild) { + // Our ancestor directly under |cur| doesn't have lazy bits; + // that means we won't get a frame + return false; + } + + if (cur->GetPrimaryFrame()->IsLeaf()) { + // Nothing under here will ever get frames + return false; + } + + // Otherwise, we might end up with a frame when that lazy bit is + // processed. Figure out our actual style. + break; + } + } + + // Now it might be that we have no frame because we're in a + // display:none subtree, or it might be that we're just dealing with + // lazy frame construction and it hasn't happened yet. Check which + // one it is. + RefPtr<nsStyleContext> styleContext = + nsComputedDOMStyle::GetStyleContextForElementNoFlush(aElement, + nullptr, nullptr); + if (styleContext) { + return styleContext->StyleDisplay()->mDisplay != StyleDisplay::None; + } + return false; +} + +bool +EditorBase::IsEditable(nsIDOMNode* aNode) +{ + nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); + return IsEditable(content); +} + +bool +EditorBase::IsEditable(nsINode* aNode) +{ + NS_ENSURE_TRUE(aNode, false); + + if (!aNode->IsNodeOfType(nsINode::eCONTENT) || IsMozEditorBogusNode(aNode) || + !IsModifiableNode(aNode)) { + return false; + } + + // see if it has a frame. If so, we'll edit it. + // special case for textnodes: frame must have width. + if (aNode->IsElement() && !IsElementVisible(aNode->AsElement())) { + // If the element has no frame, it's not editable. Note that we + // need to check IsElement() here, because some of our tests + // rely on frameless textnodes being visible. + return false; + } + switch (aNode->NodeType()) { + case nsIDOMNode::ELEMENT_NODE: + case nsIDOMNode::TEXT_NODE: + return true; // element or text node; not invisible + default: + return false; + } +} + +bool +EditorBase::IsMozEditorBogusNode(nsINode* element) +{ + return element && element->IsElement() && + element->AsElement()->AttrValueIs(kNameSpaceID_None, + kMOZEditorBogusNodeAttrAtom, kMOZEditorBogusNodeValue, + eCaseMatters); +} + +uint32_t +EditorBase::CountEditableChildren(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + uint32_t count = 0; + for (nsIContent* child = aNode->GetFirstChild(); + child; + child = child->GetNextSibling()) { + if (IsEditable(child)) { + ++count; + } + } + return count; +} + +NS_IMETHODIMP +EditorBase::IncrementModificationCount(int32_t inNumMods) +{ + uint32_t oldModCount = mModCount; + + mModCount += inNumMods; + + if ((!oldModCount && mModCount) || + (oldModCount && !mModCount)) { + NotifyDocumentListeners(eDocumentStateChanged); + } + return NS_OK; +} + + +NS_IMETHODIMP +EditorBase::GetModificationCount(int32_t* outModCount) +{ + NS_ENSURE_ARG_POINTER(outModCount); + *outModCount = mModCount; + return NS_OK; +} + + +NS_IMETHODIMP +EditorBase::ResetModificationCount() +{ + bool doNotify = (mModCount != 0); + + mModCount = 0; + + if (doNotify) { + NotifyDocumentListeners(eDocumentStateChanged); + } + return NS_OK; +} + +nsIAtom* +EditorBase::GetTag(nsIDOMNode* aNode) +{ + nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); + + if (!content) { + NS_ASSERTION(aNode, "null node passed to EditorBase::GetTag()"); + return nullptr; + } + + return content->NodeInfo()->NameAtom(); +} + +nsresult +EditorBase::GetTagString(nsIDOMNode* aNode, + nsAString& outString) +{ + if (!aNode) { + NS_NOTREACHED("null node passed to EditorBase::GetTagString()"); + return NS_ERROR_NULL_POINTER; + } + + nsIAtom *atom = GetTag(aNode); + if (!atom) { + return NS_ERROR_FAILURE; + } + + atom->ToString(outString); + return NS_OK; +} + +bool +EditorBase::NodesSameType(nsIDOMNode* aNode1, + nsIDOMNode* aNode2) +{ + if (!aNode1 || !aNode2) { + NS_NOTREACHED("null node passed to EditorBase::NodesSameType()"); + return false; + } + + nsCOMPtr<nsIContent> content1 = do_QueryInterface(aNode1); + NS_ENSURE_TRUE(content1, false); + + nsCOMPtr<nsIContent> content2 = do_QueryInterface(aNode2); + NS_ENSURE_TRUE(content2, false); + + return AreNodesSameType(content1, content2); +} + +bool +EditorBase::AreNodesSameType(nsIContent* aNode1, + nsIContent* aNode2) +{ + MOZ_ASSERT(aNode1); + MOZ_ASSERT(aNode2); + return aNode1->NodeInfo()->NameAtom() == aNode2->NodeInfo()->NameAtom(); +} + +bool +EditorBase::IsTextNode(nsIDOMNode* aNode) +{ + if (!aNode) { + NS_NOTREACHED("null node passed to IsTextNode()"); + return false; + } + + uint16_t nodeType; + aNode->GetNodeType(&nodeType); + return (nodeType == nsIDOMNode::TEXT_NODE); +} + +bool +EditorBase::IsTextNode(nsINode* aNode) +{ + return aNode->NodeType() == nsIDOMNode::TEXT_NODE; +} + +nsCOMPtr<nsIDOMNode> +EditorBase::GetChildAt(nsIDOMNode* aParent, int32_t aOffset) +{ + nsCOMPtr<nsIDOMNode> resultNode; + + nsCOMPtr<nsIContent> parent = do_QueryInterface(aParent); + + NS_ENSURE_TRUE(parent, resultNode); + + resultNode = do_QueryInterface(parent->GetChildAt(aOffset)); + + return resultNode; +} + +/** + * GetNodeAtRangeOffsetPoint() returns the node at this position in a range, + * assuming that aParentOrNode is the node itself if it's a text node, or + * the node's parent otherwise. + */ +nsIContent* +EditorBase::GetNodeAtRangeOffsetPoint(nsIDOMNode* aParentOrNode, + int32_t aOffset) +{ + nsCOMPtr<nsINode> parentOrNode = do_QueryInterface(aParentOrNode); + NS_ENSURE_TRUE(parentOrNode || !aParentOrNode, nullptr); + if (parentOrNode->GetAsText()) { + return parentOrNode->AsContent(); + } + return parentOrNode->GetChildAt(aOffset); +} + +/** + * GetStartNodeAndOffset() returns whatever the start parent & offset is of + * the first range in the selection. + */ +nsresult +EditorBase::GetStartNodeAndOffset(Selection* aSelection, + nsIDOMNode** outStartNode, + int32_t* outStartOffset) +{ + NS_ENSURE_TRUE(outStartNode && outStartOffset && aSelection, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> startNode; + nsresult rv = GetStartNodeAndOffset(aSelection, getter_AddRefs(startNode), + outStartOffset); + if (NS_FAILED(rv)) { + return rv; + } + + if (startNode) { + NS_ADDREF(*outStartNode = startNode->AsDOMNode()); + } else { + *outStartNode = nullptr; + } + return NS_OK; +} + +nsresult +EditorBase::GetStartNodeAndOffset(Selection* aSelection, + nsINode** aStartNode, + int32_t* aStartOffset) +{ + MOZ_ASSERT(aSelection); + MOZ_ASSERT(aStartNode); + MOZ_ASSERT(aStartOffset); + + *aStartNode = nullptr; + *aStartOffset = 0; + + if (!aSelection->RangeCount()) { + return NS_ERROR_FAILURE; + } + + const nsRange* range = aSelection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + NS_ENSURE_TRUE(range->IsPositioned(), NS_ERROR_FAILURE); + + NS_IF_ADDREF(*aStartNode = range->GetStartParent()); + *aStartOffset = range->StartOffset(); + return NS_OK; +} + +/** + * GetEndNodeAndOffset() returns whatever the end parent & offset is of + * the first range in the selection. + */ +nsresult +EditorBase::GetEndNodeAndOffset(Selection* aSelection, + nsIDOMNode** outEndNode, + int32_t* outEndOffset) +{ + NS_ENSURE_TRUE(outEndNode && outEndOffset && aSelection, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> endNode; + nsresult rv = GetEndNodeAndOffset(aSelection, getter_AddRefs(endNode), + outEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + + if (endNode) { + NS_ADDREF(*outEndNode = endNode->AsDOMNode()); + } else { + *outEndNode = nullptr; + } + return NS_OK; +} + +nsresult +EditorBase::GetEndNodeAndOffset(Selection* aSelection, + nsINode** aEndNode, + int32_t* aEndOffset) +{ + MOZ_ASSERT(aSelection); + MOZ_ASSERT(aEndNode); + MOZ_ASSERT(aEndOffset); + + *aEndNode = nullptr; + *aEndOffset = 0; + + NS_ENSURE_TRUE(aSelection->RangeCount(), NS_ERROR_FAILURE); + + const nsRange* range = aSelection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + NS_ENSURE_TRUE(range->IsPositioned(), NS_ERROR_FAILURE); + + NS_IF_ADDREF(*aEndNode = range->GetEndParent()); + *aEndOffset = range->EndOffset(); + return NS_OK; +} + +/** + * IsPreformatted() checks the style info for the node for the preformatted + * text style. + */ +nsresult +EditorBase::IsPreformatted(nsIDOMNode* aNode, + bool* aResult) +{ + nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); + + NS_ENSURE_TRUE(aResult && content, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + // Look at the node (and its parent if it's not an element), and grab its style context + RefPtr<nsStyleContext> elementStyle; + if (!content->IsElement()) { + content = content->GetParent(); + } + if (content && content->IsElement()) { + elementStyle = nsComputedDOMStyle::GetStyleContextForElementNoFlush(content->AsElement(), + nullptr, + ps); + } + + if (!elementStyle) { + // Consider nodes without a style context to be NOT preformatted: + // For instance, this is true of JS tags inside the body (which show + // up as #text nodes but have no style context). + *aResult = false; + return NS_OK; + } + + const nsStyleText* styleText = elementStyle->StyleText(); + + *aResult = styleText->WhiteSpaceIsSignificant(); + return NS_OK; +} + + +/** + * This splits a node "deeply", splitting children as appropriate. The place + * to split is represented by a DOM point at {splitPointParent, + * splitPointOffset}. That DOM point must be inside aNode, which is the node + * to split. We return the offset in the parent of aNode where the split + * terminates - where you would want to insert a new element, for instance, if + * that's why you were splitting the node. + * + * -1 is returned on failure, in unlikely cases like the selection being + * unavailable or cloning the node failing. Make sure not to use the returned + * offset for anything without checking that it's valid! If you're not using + * the offset, it's okay to ignore the return value. + */ +int32_t +EditorBase::SplitNodeDeep(nsIContent& aNode, + nsIContent& aSplitPointParent, + int32_t aSplitPointOffset, + EmptyContainers aEmptyContainers, + nsIContent** aOutLeftNode, + nsIContent** aOutRightNode) +{ + MOZ_ASSERT(&aSplitPointParent == &aNode || + EditorUtils::IsDescendantOf(&aSplitPointParent, &aNode)); + int32_t offset = aSplitPointOffset; + + nsCOMPtr<nsIContent> leftNode, rightNode; + OwningNonNull<nsIContent> nodeToSplit = aSplitPointParent; + while (true) { + // Need to insert rules code call here to do things like not split a list + // if you are after the last <li> or before the first, etc. For now we + // just have some smarts about unneccessarily splitting text nodes, which + // should be universal enough to put straight in this EditorBase routine. + + bool didSplit = false; + + if ((aEmptyContainers == EmptyContainers::yes && + !nodeToSplit->GetAsText()) || + (offset && offset != (int32_t)nodeToSplit->Length())) { + didSplit = true; + ErrorResult rv; + nsCOMPtr<nsIContent> newLeftNode = SplitNode(nodeToSplit, offset, rv); + NS_ENSURE_TRUE(!NS_FAILED(rv.StealNSResult()), -1); + + rightNode = nodeToSplit; + leftNode = newLeftNode; + } + + NS_ENSURE_TRUE(nodeToSplit->GetParent(), -1); + OwningNonNull<nsIContent> parentNode = *nodeToSplit->GetParent(); + + if (!didSplit && offset) { + // Must be "end of text node" case, we didn't split it, just move past it + offset = parentNode->IndexOf(nodeToSplit) + 1; + leftNode = nodeToSplit; + } else { + offset = parentNode->IndexOf(nodeToSplit); + rightNode = nodeToSplit; + } + + if (nodeToSplit == &aNode) { + // we split all the way up to (and including) aNode; we're done + break; + } + + nodeToSplit = parentNode; + } + + if (aOutLeftNode) { + leftNode.forget(aOutLeftNode); + } + if (aOutRightNode) { + rightNode.forget(aOutRightNode); + } + + return offset; +} + +/** + * This joins two like nodes "deeply", joining children as appropriate. + * Returns the point of the join, or (nullptr, -1) in case of error. + */ +EditorDOMPoint +EditorBase::JoinNodeDeep(nsIContent& aLeftNode, + nsIContent& aRightNode) +{ + // While the rightmost children and their descendants of the left node match + // the leftmost children and their descendants of the right node, join them + // up. + + nsCOMPtr<nsIContent> leftNodeToJoin = &aLeftNode; + nsCOMPtr<nsIContent> rightNodeToJoin = &aRightNode; + nsCOMPtr<nsINode> parentNode = aRightNode.GetParentNode(); + + EditorDOMPoint ret; + + while (leftNodeToJoin && rightNodeToJoin && parentNode && + AreNodesSameType(leftNodeToJoin, rightNodeToJoin)) { + uint32_t length = leftNodeToJoin->Length(); + + ret.node = rightNodeToJoin; + ret.offset = length; + + // Do the join + nsresult rv = JoinNodes(*leftNodeToJoin, *rightNodeToJoin); + NS_ENSURE_SUCCESS(rv, EditorDOMPoint()); + + if (parentNode->GetAsText()) { + // We've joined all the way down to text nodes, we're done! + return ret; + } + + // Get new left and right nodes, and begin anew + parentNode = rightNodeToJoin; + leftNodeToJoin = parentNode->GetChildAt(length - 1); + rightNodeToJoin = parentNode->GetChildAt(length); + + // Skip over non-editable nodes + while (leftNodeToJoin && !IsEditable(leftNodeToJoin)) { + leftNodeToJoin = leftNodeToJoin->GetPreviousSibling(); + } + if (!leftNodeToJoin) { + return ret; + } + + while (rightNodeToJoin && !IsEditable(rightNodeToJoin)) { + rightNodeToJoin = rightNodeToJoin->GetNextSibling(); + } + if (!rightNodeToJoin) { + return ret; + } + } + + return ret; +} + +void +EditorBase::BeginUpdateViewBatch() +{ + NS_PRECONDITION(mUpdateCount >= 0, "bad state"); + + if (!mUpdateCount) { + // Turn off selection updates and notifications. + RefPtr<Selection> selection = GetSelection(); + if (selection) { + selection->StartBatchChanges(); + } + } + + mUpdateCount++; +} + +nsresult +EditorBase::EndUpdateViewBatch() +{ + NS_PRECONDITION(mUpdateCount > 0, "bad state"); + + if (mUpdateCount <= 0) { + mUpdateCount = 0; + return NS_ERROR_FAILURE; + } + + mUpdateCount--; + + if (!mUpdateCount) { + // Turn selection updating and notifications back on. + RefPtr<Selection> selection = GetSelection(); + if (selection) { + selection->EndBatchChanges(); + } + } + + return NS_OK; +} + +bool +EditorBase::GetShouldTxnSetSelection() +{ + return mShouldTxnSetSelection; +} + +NS_IMETHODIMP +EditorBase::DeleteSelectionImpl(EDirection aAction, + EStripWrappers aStripWrappers) +{ + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + RefPtr<EditAggregateTransaction> transaction; + nsCOMPtr<nsINode> deleteNode; + int32_t deleteCharOffset = 0, deleteCharLength = 0; + nsresult rv = CreateTxnForDeleteSelection(aAction, + getter_AddRefs(transaction), + getter_AddRefs(deleteNode), + &deleteCharOffset, + &deleteCharLength); + nsCOMPtr<nsIDOMCharacterData> deleteCharData(do_QueryInterface(deleteNode)); + + if (NS_SUCCEEDED(rv)) { + AutoRules beginRulesSniffing(this, EditAction::deleteSelection, aAction); + // Notify nsIEditActionListener::WillDelete[Selection|Text|Node] + if (!deleteNode) { + for (auto& listener : mActionListeners) { + listener->WillDeleteSelection(selection); + } + } else if (deleteCharData) { + for (auto& listener : mActionListeners) { + listener->WillDeleteText(deleteCharData, deleteCharOffset, 1); + } + } else { + for (auto& listener : mActionListeners) { + listener->WillDeleteNode(deleteNode->AsDOMNode()); + } + } + + // Delete the specified amount + rv = DoTransaction(transaction); + + // Notify nsIEditActionListener::DidDelete[Selection|Text|Node] + if (!deleteNode) { + for (auto& listener : mActionListeners) { + listener->DidDeleteSelection(selection); + } + } else if (deleteCharData) { + for (auto& listener : mActionListeners) { + listener->DidDeleteText(deleteCharData, deleteCharOffset, 1, rv); + } + } else { + for (auto& listener : mActionListeners) { + listener->DidDeleteNode(deleteNode->AsDOMNode(), rv); + } + } + } + + return rv; +} + +already_AddRefed<Element> +EditorBase::DeleteSelectionAndCreateElement(nsIAtom& aTag) +{ + nsresult rv = DeleteSelectionAndPrepareToCreateNode(); + NS_ENSURE_SUCCESS(rv, nullptr); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, nullptr); + + nsCOMPtr<nsINode> node = selection->GetAnchorNode(); + uint32_t offset = selection->AnchorOffset(); + + nsCOMPtr<Element> newElement = CreateNode(&aTag, node, offset); + + // We want the selection to be just after the new node + rv = selection->Collapse(node, offset + 1); + NS_ENSURE_SUCCESS(rv, nullptr); + + return newElement.forget(); +} + +TextComposition* +EditorBase::GetComposition() const +{ + return mComposition; +} + +bool +EditorBase::IsIMEComposing() const +{ + return mComposition && mComposition->IsComposing(); +} + +bool +EditorBase::ShouldHandleIMEComposition() const +{ + // When the editor is being reframed, the old value may be restored with + // InsertText(). In this time, the text should be inserted as not a part + // of the composition. + return mComposition && mDidPostCreate; +} + +nsresult +EditorBase::DeleteSelectionAndPrepareToCreateNode() +{ + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + MOZ_ASSERT(selection->GetAnchorFocusRange()); + + if (!selection->GetAnchorFocusRange()->Collapsed()) { + nsresult rv = DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(selection->GetAnchorFocusRange() && + selection->GetAnchorFocusRange()->Collapsed(), + "Selection not collapsed after delete"); + } + + // If the selection is a chardata node, split it if necessary and compute + // where to put the new node + nsCOMPtr<nsINode> node = selection->GetAnchorNode(); + MOZ_ASSERT(node, "Selection has no ranges in it"); + + if (node && node->IsNodeOfType(nsINode::eDATA_NODE)) { + NS_ASSERTION(node->GetParentNode(), + "It's impossible to insert into chardata with no parent -- " + "fix the caller"); + NS_ENSURE_STATE(node->GetParentNode()); + + uint32_t offset = selection->AnchorOffset(); + + if (!offset) { + nsresult rv = selection->Collapse(node->GetParentNode(), + node->GetParentNode()->IndexOf(node)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, rv); + } else if (offset == node->Length()) { + nsresult rv = + selection->Collapse(node->GetParentNode(), + node->GetParentNode()->IndexOf(node) + 1); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCOMPtr<nsIDOMNode> tmp; + nsresult rv = SplitNode(node->AsDOMNode(), offset, getter_AddRefs(tmp)); + NS_ENSURE_SUCCESS(rv, rv); + rv = selection->Collapse(node->GetParentNode(), + node->GetParentNode()->IndexOf(node)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +void +EditorBase::DoAfterDoTransaction(nsITransaction* aTxn) +{ + bool isTransientTransaction; + MOZ_ALWAYS_SUCCEEDS(aTxn->GetIsTransient(&isTransientTransaction)); + + if (!isTransientTransaction) { + // we need to deal here with the case where the user saved after some + // edits, then undid one or more times. Then, the undo count is -ve, + // but we can't let a do take it back to zero. So we flip it up to + // a +ve number. + int32_t modCount; + GetModificationCount(&modCount); + if (modCount < 0) { + modCount = -modCount; + } + + // don't count transient transactions + MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1)); + } +} + +void +EditorBase::DoAfterUndoTransaction() +{ + // all undoable transactions are non-transient + MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(-1)); +} + +void +EditorBase::DoAfterRedoTransaction() +{ + // all redoable transactions are non-transient + MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1)); +} + +already_AddRefed<ChangeAttributeTransaction> +EditorBase::CreateTxnForSetAttribute(Element& aElement, + nsIAtom& aAttribute, + const nsAString& aValue) +{ + RefPtr<ChangeAttributeTransaction> transaction = + new ChangeAttributeTransaction(aElement, aAttribute, &aValue); + + return transaction.forget(); +} + +already_AddRefed<ChangeAttributeTransaction> +EditorBase::CreateTxnForRemoveAttribute(Element& aElement, + nsIAtom& aAttribute) +{ + RefPtr<ChangeAttributeTransaction> transaction = + new ChangeAttributeTransaction(aElement, aAttribute, nullptr); + + return transaction.forget(); +} + +already_AddRefed<CreateElementTransaction> +EditorBase::CreateTxnForCreateElement(nsIAtom& aTag, + nsINode& aParent, + int32_t aPosition) +{ + RefPtr<CreateElementTransaction> transaction = + new CreateElementTransaction(*this, aTag, aParent, aPosition); + + return transaction.forget(); +} + + +already_AddRefed<InsertNodeTransaction> +EditorBase::CreateTxnForInsertNode(nsIContent& aNode, + nsINode& aParent, + int32_t aPosition) +{ + RefPtr<InsertNodeTransaction> transaction = + new InsertNodeTransaction(aNode, aParent, aPosition, *this); + return transaction.forget(); +} + +nsresult +EditorBase::CreateTxnForDeleteNode(nsINode* aNode, + DeleteNodeTransaction** aTransaction) +{ + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + + RefPtr<DeleteNodeTransaction> transaction = new DeleteNodeTransaction(); + + nsresult rv = transaction->Init(this, aNode, &mRangeUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + transaction.forget(aTransaction); + return NS_OK; +} + +already_AddRefed<CompositionTransaction> +EditorBase::CreateTxnForComposition(const nsAString& aStringToInsert) +{ + MOZ_ASSERT(mIMETextNode); + // During handling IME composition, mComposition must have been initialized. + // TODO: We can simplify CompositionTransaction::Init() with TextComposition + // class. + RefPtr<CompositionTransaction> transaction = + new CompositionTransaction(*mIMETextNode, mIMETextOffset, mIMETextLength, + mComposition->GetRanges(), aStringToInsert, + *this, &mRangeUpdater); + return transaction.forget(); +} + +NS_IMETHODIMP +EditorBase::CreateTxnForAddStyleSheet(StyleSheet* aSheet, + AddStyleSheetTransaction** aTransaction) +{ + RefPtr<AddStyleSheetTransaction> transaction = new AddStyleSheetTransaction(); + + nsresult rv = transaction->Init(this, aSheet); + if (NS_SUCCEEDED(rv)) { + transaction.forget(aTransaction); + } + + return rv; +} + +NS_IMETHODIMP +EditorBase::CreateTxnForRemoveStyleSheet( + StyleSheet* aSheet, + RemoveStyleSheetTransaction** aTransaction) +{ + RefPtr<RemoveStyleSheetTransaction> transaction = + new RemoveStyleSheetTransaction(); + + nsresult rv = transaction->Init(this, aSheet); + if (NS_SUCCEEDED(rv)) { + transaction.forget(aTransaction); + } + + return rv; +} + +nsresult +EditorBase::CreateTxnForDeleteSelection(EDirection aAction, + EditAggregateTransaction** aTransaction, + nsINode** aNode, + int32_t* aOffset, + int32_t* aLength) +{ + MOZ_ASSERT(aTransaction); + *aTransaction = nullptr; + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + // Check whether the selection is collapsed and we should do nothing: + if (selection->Collapsed() && aAction == eNone) { + return NS_OK; + } + + // allocate the out-param transaction + RefPtr<EditAggregateTransaction> aggregateTransaction = + new EditAggregateTransaction(); + + for (uint32_t rangeIdx = 0; rangeIdx < selection->RangeCount(); ++rangeIdx) { + RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); + NS_ENSURE_STATE(range); + + // Same with range as with selection; if it is collapsed and action + // is eNone, do nothing. + if (!range->Collapsed()) { + RefPtr<DeleteRangeTransaction> transaction = new DeleteRangeTransaction(); + transaction->Init(this, range, &mRangeUpdater); + aggregateTransaction->AppendChild(transaction); + } else if (aAction != eNone) { + // we have an insertion point. delete the thing in front of it or + // behind it, depending on aAction + nsresult rv = CreateTxnForDeleteInsertionPoint(range, aAction, + aggregateTransaction, + aNode, aOffset, aLength); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + aggregateTransaction.forget(aTransaction); + + return NS_OK; +} + +already_AddRefed<DeleteTextTransaction> +EditorBase::CreateTxnForDeleteCharacter(nsGenericDOMDataNode& aData, + uint32_t aOffset, + EDirection aDirection) +{ + NS_ASSERTION(aDirection == eNext || aDirection == ePrevious, + "Invalid direction"); + nsAutoString data; + aData.GetData(data); + NS_ASSERTION(data.Length(), "Trying to delete from a zero-length node"); + NS_ENSURE_TRUE(data.Length(), nullptr); + + uint32_t segOffset = aOffset, segLength = 1; + if (aDirection == eNext) { + if (segOffset + 1 < data.Length() && + NS_IS_HIGH_SURROGATE(data[segOffset]) && + NS_IS_LOW_SURROGATE(data[segOffset+1])) { + // Delete both halves of the surrogate pair + ++segLength; + } + } else if (aOffset > 0) { + --segOffset; + if (segOffset > 0 && + NS_IS_LOW_SURROGATE(data[segOffset]) && + NS_IS_HIGH_SURROGATE(data[segOffset-1])) { + ++segLength; + --segOffset; + } + } else { + return nullptr; + } + return CreateTxnForDeleteText(aData, segOffset, segLength); +} + +//XXX: currently, this doesn't handle edge conditions because GetNext/GetPrior +//are not implemented +nsresult +EditorBase::CreateTxnForDeleteInsertionPoint( + nsRange* aRange, + EDirection aAction, + EditAggregateTransaction* aTransaction, + nsINode** aNode, + int32_t* aOffset, + int32_t* aLength) +{ + MOZ_ASSERT(aAction != eNone); + + // get the node and offset of the insertion point + nsCOMPtr<nsINode> node = aRange->GetStartParent(); + NS_ENSURE_STATE(node); + + int32_t offset = aRange->StartOffset(); + + // determine if the insertion point is at the beginning, middle, or end of + // the node + + uint32_t count = node->Length(); + + bool isFirst = !offset; + bool isLast = (count == (uint32_t)offset); + + // XXX: if isFirst && isLast, then we'll need to delete the node + // as well as the 1 child + + // build a transaction for deleting the appropriate data + // XXX: this has to come from rule section + if (aAction == ePrevious && isFirst) { + // we're backspacing from the beginning of the node. Delete the first + // thing to our left + nsCOMPtr<nsIContent> priorNode = GetPriorNode(node, true); + NS_ENSURE_STATE(priorNode); + + // there is a priorNode, so delete its last child (if chardata, delete the + // last char). if it has no children, delete it + if (priorNode->IsNodeOfType(nsINode::eDATA_NODE)) { + RefPtr<nsGenericDOMDataNode> priorNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(priorNode.get()); + uint32_t length = priorNode->Length(); + // Bail out for empty chardata XXX: Do we want to do something else? + NS_ENSURE_STATE(length); + RefPtr<DeleteTextTransaction> transaction = + CreateTxnForDeleteCharacter(*priorNodeAsCharData, length, ePrevious); + NS_ENSURE_STATE(transaction); + + *aOffset = transaction->GetOffset(); + *aLength = transaction->GetNumCharsToDelete(); + aTransaction->AppendChild(transaction); + } else { + // priorNode is not chardata, so tell its parent to delete it + RefPtr<DeleteNodeTransaction> transaction; + nsresult rv = + CreateTxnForDeleteNode(priorNode, getter_AddRefs(transaction)); + NS_ENSURE_SUCCESS(rv, rv); + + aTransaction->AppendChild(transaction); + } + + NS_ADDREF(*aNode = priorNode); + + return NS_OK; + } + + if (aAction == eNext && isLast) { + // we're deleting from the end of the node. Delete the first thing to our + // right + nsCOMPtr<nsIContent> nextNode = GetNextNode(node, true); + NS_ENSURE_STATE(nextNode); + + // there is a nextNode, so delete its first child (if chardata, delete the + // first char). if it has no children, delete it + if (nextNode->IsNodeOfType(nsINode::eDATA_NODE)) { + RefPtr<nsGenericDOMDataNode> nextNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(nextNode.get()); + uint32_t length = nextNode->Length(); + // Bail out for empty chardata XXX: Do we want to do something else? + NS_ENSURE_STATE(length); + RefPtr<DeleteTextTransaction> transaction = + CreateTxnForDeleteCharacter(*nextNodeAsCharData, 0, eNext); + NS_ENSURE_STATE(transaction); + + *aOffset = transaction->GetOffset(); + *aLength = transaction->GetNumCharsToDelete(); + aTransaction->AppendChild(transaction); + } else { + // nextNode is not chardata, so tell its parent to delete it + RefPtr<DeleteNodeTransaction> transaction; + nsresult rv = + CreateTxnForDeleteNode(nextNode, getter_AddRefs(transaction)); + NS_ENSURE_SUCCESS(rv, rv); + aTransaction->AppendChild(transaction); + } + + NS_ADDREF(*aNode = nextNode); + + return NS_OK; + } + + if (node->IsNodeOfType(nsINode::eDATA_NODE)) { + RefPtr<nsGenericDOMDataNode> nodeAsCharData = + static_cast<nsGenericDOMDataNode*>(node.get()); + // we have chardata, so delete a char at the proper offset + RefPtr<DeleteTextTransaction> transaction = + CreateTxnForDeleteCharacter(*nodeAsCharData, offset, aAction); + NS_ENSURE_STATE(transaction); + + aTransaction->AppendChild(transaction); + NS_ADDREF(*aNode = node); + *aOffset = transaction->GetOffset(); + *aLength = transaction->GetNumCharsToDelete(); + } else { + // we're either deleting a node or chardata, need to dig into the next/prev + // node to find out + nsCOMPtr<nsINode> selectedNode; + if (aAction == ePrevious) { + selectedNode = GetPriorNode(node, offset, true); + } else if (aAction == eNext) { + selectedNode = GetNextNode(node, offset, true); + } + + while (selectedNode && + selectedNode->IsNodeOfType(nsINode::eDATA_NODE) && + !selectedNode->Length()) { + // Can't delete an empty chardata node (bug 762183) + if (aAction == ePrevious) { + selectedNode = GetPriorNode(selectedNode, true); + } else if (aAction == eNext) { + selectedNode = GetNextNode(selectedNode, true); + } + } + NS_ENSURE_STATE(selectedNode); + + if (selectedNode->IsNodeOfType(nsINode::eDATA_NODE)) { + RefPtr<nsGenericDOMDataNode> selectedNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(selectedNode.get()); + // we are deleting from a chardata node, so do a character deletion + uint32_t position = 0; + if (aAction == ePrevious) { + position = selectedNode->Length(); + } + RefPtr<DeleteTextTransaction> deleteTextTransaction = + CreateTxnForDeleteCharacter(*selectedNodeAsCharData, position, + aAction); + NS_ENSURE_TRUE(deleteTextTransaction, NS_ERROR_NULL_POINTER); + + aTransaction->AppendChild(deleteTextTransaction); + *aOffset = deleteTextTransaction->GetOffset(); + *aLength = deleteTextTransaction->GetNumCharsToDelete(); + } else { + RefPtr<DeleteNodeTransaction> deleteNodeTransaction; + nsresult rv = + CreateTxnForDeleteNode(selectedNode, + getter_AddRefs(deleteNodeTransaction)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(deleteNodeTransaction, NS_ERROR_NULL_POINTER); + + aTransaction->AppendChild(deleteNodeTransaction); + } + + NS_ADDREF(*aNode = selectedNode); + } + + return NS_OK; +} + +nsresult +EditorBase::CreateRange(nsIDOMNode* aStartParent, + int32_t aStartOffset, + nsIDOMNode* aEndParent, + int32_t aEndOffset, + nsRange** aRange) +{ + return nsRange::CreateRange(aStartParent, aStartOffset, aEndParent, + aEndOffset, aRange); +} + +nsresult +EditorBase::AppendNodeToSelectionAsRange(nsIDOMNode* aNode) +{ + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMNode> parentNode; + nsresult rv = aNode->GetParentNode(getter_AddRefs(parentNode)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parentNode, NS_ERROR_NULL_POINTER); + + int32_t offset = GetChildOffset(aNode, parentNode); + + RefPtr<nsRange> range; + rv = CreateRange(parentNode, offset, parentNode, offset + 1, + getter_AddRefs(range)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER); + + return selection->AddRange(range); +} + +nsresult +EditorBase::ClearSelection() +{ + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + return selection->RemoveAllRanges(); +} + +already_AddRefed<Element> +EditorBase::CreateHTMLContent(nsIAtom* aTag) +{ + MOZ_ASSERT(aTag); + + nsCOMPtr<nsIDocument> doc = GetDocument(); + if (!doc) { + return nullptr; + } + + // XXX Wallpaper over editor bug (editor tries to create elements with an + // empty nodename). + if (aTag == nsGkAtoms::_empty) { + NS_ERROR("Don't pass an empty tag to EditorBase::CreateHTMLContent, " + "check caller."); + return nullptr; + } + + return doc->CreateElem(nsDependentAtomString(aTag), nullptr, + kNameSpaceID_XHTML); +} + +nsresult +EditorBase::SetAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue, + bool aSuppressTransaction) +{ + return SetAttribute(aElement, aAttribute, aValue); +} + +nsresult +EditorBase::RemoveAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + bool aSuppressTransaction) +{ + return RemoveAttribute(aElement, aAttribute); +} + +nsresult +EditorBase::HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) +{ + // NOTE: When you change this method, you should also change: + // * editor/libeditor/tests/test_texteditor_keyevent_handling.html + // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html + // + // And also when you add new key handling, you need to change the subclass's + // HandleKeyPressEvent()'s switch statement. + + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent(); + NS_ENSURE_TRUE(nativeKeyEvent, NS_ERROR_UNEXPECTED); + NS_ASSERTION(nativeKeyEvent->mMessage == eKeyPress, + "HandleKeyPressEvent gets non-keypress event"); + + // if we are readonly or disabled, then do nothing. + if (IsReadonly() || IsDisabled()) { + // consume backspace for disabled and readonly textfields, to prevent + // back in history, which could be confusing to users + if (nativeKeyEvent->mKeyCode == NS_VK_BACK) { + aKeyEvent->AsEvent()->PreventDefault(); + } + return NS_OK; + } + + switch (nativeKeyEvent->mKeyCode) { + case NS_VK_META: + case NS_VK_WIN: + case NS_VK_SHIFT: + case NS_VK_CONTROL: + case NS_VK_ALT: + aKeyEvent->AsEvent()->PreventDefault(); // consumed + return NS_OK; + case NS_VK_BACK: + if (nativeKeyEvent->IsControl() || nativeKeyEvent->IsAlt() || + nativeKeyEvent->IsMeta() || nativeKeyEvent->IsOS()) { + return NS_OK; + } + DeleteSelection(nsIEditor::ePrevious, nsIEditor::eStrip); + aKeyEvent->AsEvent()->PreventDefault(); // consumed + return NS_OK; + case NS_VK_DELETE: + // on certain platforms (such as windows) the shift key + // modifies what delete does (cmd_cut in this case). + // bailing here to allow the keybindings to do the cut. + if (nativeKeyEvent->IsShift() || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + return NS_OK; + } + DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip); + aKeyEvent->AsEvent()->PreventDefault(); // consumed + return NS_OK; + } + return NS_OK; +} + +nsresult +EditorBase::HandleInlineSpellCheck(EditAction action, + Selection* aSelection, + nsIDOMNode* previousSelectedNode, + int32_t previousSelectedOffset, + nsIDOMNode* aStartNode, + int32_t aStartOffset, + nsIDOMNode* aEndNode, + int32_t aEndOffset) +{ + // Have to cast action here because this method is from an IDL + return mInlineSpellChecker ? mInlineSpellChecker->SpellCheckAfterEditorChange( + (int32_t)action, aSelection, + previousSelectedNode, previousSelectedOffset, + aStartNode, aStartOffset, aEndNode, + aEndOffset) + : NS_OK; +} + +already_AddRefed<nsIContent> +EditorBase::FindSelectionRoot(nsINode* aNode) +{ + nsCOMPtr<nsIContent> rootContent = GetRoot(); + return rootContent.forget(); +} + +nsresult +EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget) +{ + nsCOMPtr<nsINode> targetNode = do_QueryInterface(aFocusEventTarget); + NS_ENSURE_TRUE(targetNode, NS_ERROR_INVALID_ARG); + nsCOMPtr<nsIContent> selectionRootContent = FindSelectionRoot(targetNode); + if (!selectionRootContent) { + return NS_OK; + } + + bool isTargetDoc = + targetNode->NodeType() == nsIDOMNode::DOCUMENT_NODE && + targetNode->HasFlag(NODE_IS_EDITABLE); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsISelectionController> selCon; + nsresult rv = GetSelectionController(getter_AddRefs(selCon)); + NS_ENSURE_SUCCESS(rv, rv); + + // Init the caret + RefPtr<nsCaret> caret = presShell->GetCaret(); + NS_ENSURE_TRUE(caret, NS_ERROR_UNEXPECTED); + caret->SetIgnoreUserModify(false); + caret->SetSelection(selection); + selCon->SetCaretReadOnly(IsReadonly()); + selCon->SetCaretEnabled(true); + + // Init selection + selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON); + selCon->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL); + selCon->RepaintSelection(nsISelectionController::SELECTION_NORMAL); + // If the computed selection root isn't root content, we should set it + // as selection ancestor limit. However, if that is root element, it means + // there is not limitation of the selection, then, we must set nullptr. + // NOTE: If we set a root element to the ancestor limit, some selection + // methods don't work fine. + if (selectionRootContent->GetParent()) { + selection->SetAncestorLimiter(selectionRootContent); + } else { + selection->SetAncestorLimiter(nullptr); + } + + // XXX What case needs this? + if (isTargetDoc) { + int32_t rangeCount; + selection->GetRangeCount(&rangeCount); + if (!rangeCount) { + BeginningOfDocument(); + } + } + + // If there is composition when this is called, we may need to restore IME + // selection because if the editor is reframed, this already forgot IME + // selection and the transaction. + if (mComposition && !mIMETextNode && mIMETextLength) { + // We need to look for the new mIMETextNode from current selection. + // XXX If selection is changed during reframe, this doesn't work well! + nsRange* firstRange = selection->GetRangeAt(0); + NS_ENSURE_TRUE(firstRange, NS_ERROR_FAILURE); + nsCOMPtr<nsINode> startNode = firstRange->GetStartParent(); + int32_t startOffset = firstRange->StartOffset(); + FindBetterInsertionPoint(startNode, startOffset); + Text* textNode = startNode->GetAsText(); + MOZ_ASSERT(textNode, + "There must be text node if mIMETextLength is larger than 0"); + if (textNode) { + MOZ_ASSERT(textNode->Length() >= mIMETextOffset + mIMETextLength, + "The text node must be different from the old mIMETextNode"); + CompositionTransaction::SetIMESelection(*this, textNode, mIMETextOffset, + mIMETextLength, + mComposition->GetRanges()); + } + } + + return NS_OK; +} + +class RepaintSelectionRunner final : public Runnable { +public: + explicit RepaintSelectionRunner(nsISelectionController* aSelectionController) + : mSelectionController(aSelectionController) + { + } + + NS_IMETHOD Run() override + { + mSelectionController->RepaintSelection( + nsISelectionController::SELECTION_NORMAL); + return NS_OK; + } + +private: + nsCOMPtr<nsISelectionController> mSelectionController; +}; + +NS_IMETHODIMP +EditorBase::FinalizeSelection() +{ + nsCOMPtr<nsISelectionController> selCon; + nsresult rv = GetSelectionController(getter_AddRefs(selCon)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + selection->SetAncestorLimiter(nullptr); + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED); + + selCon->SetCaretEnabled(false); + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, NS_ERROR_NOT_INITIALIZED); + fm->UpdateCaretForCaretBrowsingMode(); + + if (!HasIndependentSelection()) { + // If this editor doesn't have an independent selection, i.e., it must + // mean that it is an HTML editor, the selection controller is shared with + // presShell. So, even this editor loses focus, other part of the document + // may still have focus. + nsCOMPtr<nsIDocument> doc = GetDocument(); + ErrorResult ret; + if (!doc || !doc->HasFocus(ret)) { + // If the document already lost focus, mark the selection as disabled. + selCon->SetDisplaySelection(nsISelectionController::SELECTION_DISABLED); + } else { + // Otherwise, mark selection as normal because outside of a + // contenteditable element should be selected with normal selection + // color after here. + selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON); + } + } else if (IsFormWidget() || IsPasswordEditor() || + IsReadonly() || IsDisabled() || IsInputFiltered()) { + // In <input> or <textarea>, the independent selection should be hidden + // while this editor doesn't have focus. + selCon->SetDisplaySelection(nsISelectionController::SELECTION_HIDDEN); + } else { + // Otherwise, although we're not sure how this case happens, the + // independent selection should be marked as disabled. + selCon->SetDisplaySelection(nsISelectionController::SELECTION_DISABLED); + } + + + // FinalizeSelection might be called from ContentRemoved even if selection + // isn't updated. So we need to call RepaintSelection after updated it. + nsContentUtils::AddScriptRunner( + new RepaintSelectionRunner(selCon)); + return NS_OK; +} + +Element* +EditorBase::GetRoot() +{ + if (!mRootElement) { + // Let GetRootElement() do the work + nsCOMPtr<nsIDOMElement> root; + GetRootElement(getter_AddRefs(root)); + } + + return mRootElement; +} + +Element* +EditorBase::GetEditorRoot() +{ + return GetRoot(); +} + +Element* +EditorBase::GetExposedRoot() +{ + Element* rootElement = GetRoot(); + + // For plaintext editors, we need to ask the input/textarea element directly. + if (rootElement && rootElement->IsRootOfNativeAnonymousSubtree()) { + rootElement = rootElement->GetParent()->AsElement(); + } + + return rootElement; +} + +nsresult +EditorBase::DetermineCurrentDirection() +{ + // Get the current root direction from its frame + nsIContent* rootElement = GetExposedRoot(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE); + + // If we don't have an explicit direction, determine our direction + // from the content's direction + if (!(mFlags & (nsIPlaintextEditor::eEditorLeftToRight | + nsIPlaintextEditor::eEditorRightToLeft))) { + nsIFrame* frame = rootElement->GetPrimaryFrame(); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + + // Set the flag here, to enable us to use the same code path below. + // It will be flipped before returning from the function. + if (frame->StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL) { + mFlags |= nsIPlaintextEditor::eEditorRightToLeft; + } else { + mFlags |= nsIPlaintextEditor::eEditorLeftToRight; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SwitchTextDirection() +{ + // Get the current root direction from its frame + nsIContent* rootElement = GetExposedRoot(); + + nsresult rv = DetermineCurrentDirection(); + NS_ENSURE_SUCCESS(rv, rv); + + // Apply the opposite direction + if (mFlags & nsIPlaintextEditor::eEditorRightToLeft) { + NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorLeftToRight), + "Unexpected mutually exclusive flag"); + mFlags &= ~nsIPlaintextEditor::eEditorRightToLeft; + mFlags |= nsIPlaintextEditor::eEditorLeftToRight; + rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("ltr"), true); + } else if (mFlags & nsIPlaintextEditor::eEditorLeftToRight) { + NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorRightToLeft), + "Unexpected mutually exclusive flag"); + mFlags |= nsIPlaintextEditor::eEditorRightToLeft; + mFlags &= ~nsIPlaintextEditor::eEditorLeftToRight; + rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("rtl"), true); + } + + if (NS_SUCCEEDED(rv)) { + FireInputEvent(); + } + + return rv; +} + +void +EditorBase::SwitchTextDirectionTo(uint32_t aDirection) +{ + // Get the current root direction from its frame + nsIContent* rootElement = GetExposedRoot(); + + nsresult rv = DetermineCurrentDirection(); + NS_ENSURE_SUCCESS_VOID(rv); + + // Apply the requested direction + if (aDirection == nsIPlaintextEditor::eEditorLeftToRight && + (mFlags & nsIPlaintextEditor::eEditorRightToLeft)) { + NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorLeftToRight), + "Unexpected mutually exclusive flag"); + mFlags &= ~nsIPlaintextEditor::eEditorRightToLeft; + mFlags |= nsIPlaintextEditor::eEditorLeftToRight; + rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("ltr"), true); + } else if (aDirection == nsIPlaintextEditor::eEditorRightToLeft && + (mFlags & nsIPlaintextEditor::eEditorLeftToRight)) { + NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorRightToLeft), + "Unexpected mutually exclusive flag"); + mFlags |= nsIPlaintextEditor::eEditorRightToLeft; + mFlags &= ~nsIPlaintextEditor::eEditorLeftToRight; + rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("rtl"), true); + } + + if (NS_SUCCEEDED(rv)) { + FireInputEvent(); + } +} + +#if DEBUG_JOE +void +EditorBase::DumpNode(nsIDOMNode* aNode, + int32_t indent) +{ + for (int32_t i = 0; i < indent; i++) { + printf(" "); + } + + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(aNode); + nsCOMPtr<nsIDOMDocumentFragment> docfrag = do_QueryInterface(aNode); + + if (element || docfrag) { + if (element) { + nsAutoString tag; + element->GetTagName(tag); + printf("<%s>\n", NS_LossyConvertUTF16toASCII(tag).get()); + } else { + printf("<document fragment>\n"); + } + nsCOMPtr<nsIDOMNodeList> childList; + aNode->GetChildNodes(getter_AddRefs(childList)); + NS_ENSURE_TRUE(childList, NS_ERROR_NULL_POINTER); + uint32_t numChildren; + childList->GetLength(&numChildren); + nsCOMPtr<nsIDOMNode> child, tmp; + aNode->GetFirstChild(getter_AddRefs(child)); + for (uint32_t i = 0; i < numChildren; i++) { + DumpNode(child, indent + 1); + child->GetNextSibling(getter_AddRefs(tmp)); + child = tmp; + } + } else if (IsTextNode(aNode)) { + nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(aNode); + nsAutoString str; + textNode->GetData(str); + nsAutoCString cstr; + LossyCopyUTF16toASCII(str, cstr); + cstr.ReplaceChar('\n', ' '); + printf("<textnode> %s\n", cstr.get()); + } +} +#endif + +bool +EditorBase::IsModifiableNode(nsIDOMNode* aNode) +{ + return true; +} + +bool +EditorBase::IsModifiableNode(nsINode* aNode) +{ + return true; +} + +already_AddRefed<nsIContent> +EditorBase::GetFocusedContent() +{ + nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget(); + if (!piTarget) { + return nullptr; + } + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, nullptr); + + nsCOMPtr<nsIContent> content = fm->GetFocusedContent(); + return SameCOMIdentity(content, piTarget) ? content.forget() : nullptr; +} + +already_AddRefed<nsIContent> +EditorBase::GetFocusedContentForIME() +{ + return GetFocusedContent(); +} + +bool +EditorBase::IsActiveInDOMWindow() +{ + nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget(); + if (!piTarget) { + return false; + } + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, false); + + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + nsPIDOMWindowOuter* ourWindow = doc->GetWindow(); + nsCOMPtr<nsPIDOMWindowOuter> win; + nsIContent* content = + nsFocusManager::GetFocusedDescendant(ourWindow, false, + getter_AddRefs(win)); + return SameCOMIdentity(content, piTarget); +} + +bool +EditorBase::IsAcceptableInputEvent(nsIDOMEvent* aEvent) +{ + // If the event is trusted, the event should always cause input. + NS_ENSURE_TRUE(aEvent, false); + + WidgetEvent* widgetEvent = aEvent->WidgetEventPtr(); + if (NS_WARN_IF(!widgetEvent)) { + return false; + } + + // If this is dispatched by using cordinates but this editor doesn't have + // focus, we shouldn't handle it. + if (widgetEvent->IsUsingCoordinates()) { + nsCOMPtr<nsIContent> focusedContent = GetFocusedContent(); + if (!focusedContent) { + return false; + } + } + + // If a composition event isn't dispatched via widget, we need to ignore them + // since they cannot be managed by TextComposition. E.g., the event was + // created by chrome JS. + // Note that if we allow to handle such events, editor may be confused by + // strange event order. + bool needsWidget = false; + WidgetGUIEvent* widgetGUIEvent = nullptr; + switch (widgetEvent->mMessage) { + case eUnidentifiedEvent: + // If events are not created with proper event interface, their message + // are initialized with eUnidentifiedEvent. Let's ignore such event. + return false; + case eCompositionStart: + case eCompositionEnd: + case eCompositionUpdate: + case eCompositionChange: + case eCompositionCommitAsIs: + // Don't allow composition events whose internal event are not + // WidgetCompositionEvent. + widgetGUIEvent = aEvent->WidgetEventPtr()->AsCompositionEvent(); + needsWidget = true; + break; + default: + break; + } + if (needsWidget && + (!widgetGUIEvent || !widgetGUIEvent->mWidget)) { + return false; + } + + // Accept all trusted events. + if (widgetEvent->IsTrusted()) { + return true; + } + + // Ignore untrusted mouse event. + // XXX Why are we handling other untrusted input events? + if (widgetEvent->AsMouseEventBase()) { + return false; + } + + // Otherwise, we shouldn't handle any input events when we're not an active + // element of the DOM window. + return IsActiveInDOMWindow(); +} + +void +EditorBase::OnFocus(nsIDOMEventTarget* aFocusEventTarget) +{ + InitializeSelection(aFocusEventTarget); + if (mInlineSpellChecker) { + mInlineSpellChecker->UpdateCurrentDictionary(); + } +} + +NS_IMETHODIMP +EditorBase::GetSuppressDispatchingInputEvent(bool* aSuppressed) +{ + NS_ENSURE_ARG_POINTER(aSuppressed); + *aSuppressed = !mDispatchInputEvent; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetSuppressDispatchingInputEvent(bool aSuppress) +{ + mDispatchInputEvent = !aSuppress; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetIsInEditAction(bool* aIsInEditAction) +{ + MOZ_ASSERT(aIsInEditAction, "aIsInEditAction must not be null"); + *aIsInEditAction = mIsInEditAction; + return NS_OK; +} + +int32_t +EditorBase::GetIMESelectionStartOffsetIn(nsINode* aTextNode) +{ + MOZ_ASSERT(aTextNode, "aTextNode must not be nullptr"); + + nsCOMPtr<nsISelectionController> selectionController; + nsresult rv = GetSelectionController(getter_AddRefs(selectionController)); + NS_ENSURE_SUCCESS(rv, -1); + NS_ENSURE_TRUE(selectionController, -1); + + int32_t minOffset = INT32_MAX; + static const SelectionType kIMESelectionTypes[] = { + SelectionType::eIMERawClause, + SelectionType::eIMESelectedRawClause, + SelectionType::eIMEConvertedClause, + SelectionType::eIMESelectedClause + }; + for (auto selectionType : kIMESelectionTypes) { + RefPtr<Selection> selection = GetSelection(selectionType); + if (!selection) { + continue; + } + for (uint32_t i = 0; i < selection->RangeCount(); i++) { + RefPtr<nsRange> range = selection->GetRangeAt(i); + if (NS_WARN_IF(!range)) { + continue; + } + if (NS_WARN_IF(range->GetStartParent() != aTextNode)) { + // ignore the start offset... + } else { + MOZ_ASSERT(range->StartOffset() >= 0, + "start offset shouldn't be negative"); + minOffset = std::min(minOffset, range->StartOffset()); + } + if (NS_WARN_IF(range->GetEndParent() != aTextNode)) { + // ignore the end offset... + } else { + MOZ_ASSERT(range->EndOffset() >= 0, + "start offset shouldn't be negative"); + minOffset = std::min(minOffset, range->EndOffset()); + } + } + } + return minOffset < INT32_MAX ? minOffset : -1; +} + +void +EditorBase::HideCaret(bool aHide) +{ + if (mHidingCaret == aHide) { + return; + } + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE_VOID(presShell); + RefPtr<nsCaret> caret = presShell->GetCaret(); + NS_ENSURE_TRUE_VOID(caret); + + mHidingCaret = aHide; + if (aHide) { + caret->AddForceHide(); + } else { + caret->RemoveForceHide(); + } +} + +} // namespace mozilla diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h new file mode 100644 index 000000000..dd4b9695e --- /dev/null +++ b/editor/libeditor/EditorBase.h @@ -0,0 +1,1046 @@ +/* -*- 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/. */ + +#ifndef mozilla_EditorBase_h +#define mozilla_EditorBase_h + +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc. +#include "mozFlushType.h" // for mozFlushType enum +#include "mozilla/OwningNonNull.h" // for OwningNonNull +#include "mozilla/SelectionState.h" // for RangeUpdater, etc. +#include "mozilla/StyleSheet.h" // for StyleSheet +#include "mozilla/dom/Text.h" +#include "nsCOMPtr.h" // for already_AddRefed, nsCOMPtr +#include "nsCycleCollectionParticipant.h" +#include "nsGkAtoms.h" +#include "nsIEditor.h" // for nsIEditor::EDirection, etc. +#include "nsIEditorIMESupport.h" // for NS_DECL_NSIEDITORIMESUPPORT, etc. +#include "nsIObserver.h" // for NS_DECL_NSIOBSERVER, etc. +#include "nsIPhonetic.h" // for NS_DECL_NSIPHONETIC, etc. +#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc. +#include "nsISelectionController.h" // for nsISelectionController constants +#include "nsISupportsImpl.h" // for EditorBase::Release, etc. +#include "nsIWeakReferenceUtils.h" // for nsWeakPtr +#include "nsLiteralString.h" // for NS_LITERAL_STRING +#include "nsString.h" // for nsCString +#include "nsWeakReference.h" // for nsSupportsWeakReference +#include "nscore.h" // for nsresult, nsAString, etc. + +class nsIAtom; +class nsIContent; +class nsIDOMDocument; +class nsIDOMEvent; +class nsIDOMEventListener; +class nsIDOMEventTarget; +class nsIDOMKeyEvent; +class nsIDOMNode; +class nsIDocument; +class nsIDocumentStateListener; +class nsIEditActionListener; +class nsIEditorObserver; +class nsIInlineSpellChecker; +class nsINode; +class nsIPresShell; +class nsISupports; +class nsITransaction; +class nsIWidget; +class nsRange; +class nsString; +class nsTransactionManager; + +// This is int32_t instead of int16_t because nsIInlineSpellChecker.idl's +// spellCheckAfterEditorChange is defined to take it as a long. +// XXX EditAction causes unnecessary include of EditorBase from some places. +// Why don't you move this to nsIEditor.idl? +enum class EditAction : int32_t +{ + ignore = -1, + none = 0, + undo, + redo, + insertNode, + createNode, + deleteNode, + splitNode, + joinNode, + deleteText = 1003, + + // text commands + insertText = 2000, + insertIMEText = 2001, + deleteSelection = 2002, + setTextProperty = 2003, + removeTextProperty = 2004, + outputText = 2005, + + // html only action + insertBreak = 3000, + makeList = 3001, + indent = 3002, + outdent = 3003, + align = 3004, + makeBasicBlock = 3005, + removeList = 3006, + makeDefListItem = 3007, + insertElement = 3008, + insertQuotation = 3009, + htmlPaste = 3012, + loadHTML = 3013, + resetTextProperties = 3014, + setAbsolutePosition = 3015, + removeAbsolutePosition = 3016, + decreaseZIndex = 3017, + increaseZIndex = 3018 +}; + +inline bool operator!(const EditAction& aOp) +{ + return aOp == EditAction::none; +} + +namespace mozilla { +class AddStyleSheetTransaction; +class AutoRules; +class AutoSelectionRestorer; +class AutoTransactionsConserveSelection; +class ChangeAttributeTransaction; +class CompositionTransaction; +class CreateElementTransaction; +class DeleteNodeTransaction; +class DeleteTextTransaction; +class EditAggregateTransaction; +class ErrorResult; +class InsertNodeTransaction; +class InsertTextTransaction; +class JoinNodeTransaction; +class RemoveStyleSheetTransaction; +class SplitNodeTransaction; +class TextComposition; +struct EditorDOMPoint; + +namespace dom { +class DataTransfer; +class Element; +class EventTarget; +class Selection; +class Text; +} // namespace dom + +namespace widget { +struct IMEState; +} // namespace widget + +#define kMOZEditorBogusNodeAttrAtom nsGkAtoms::mozeditorbogusnode +#define kMOZEditorBogusNodeValue NS_LITERAL_STRING("TRUE") + +/** + * Implementation of an editor object. it will be the controller/focal point + * for the main editor services. i.e. the GUIManager, publishing, transaction + * manager, event interfaces. the idea for the event interfaces is to have them + * delegate the actual commands to the editor independent of the XPFE + * implementation. + */ +class EditorBase : public nsIEditor + , public nsIEditorIMESupport + , public nsSupportsWeakReference + , public nsIPhonetic +{ +public: + typedef dom::Element Element; + typedef dom::Selection Selection; + typedef dom::Text Text; + + enum IterDirection + { + kIterForward, + kIterBackward + }; + + /** + * The default constructor. This should suffice. the setting of the + * interfaces is done after the construction of the editor class. + */ + EditorBase(); + +protected: + /** + * The default destructor. This should suffice. Should this be pure virtual + * for someone to derive from the EditorBase later? I don't believe so. + */ + virtual ~EditorBase(); + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(EditorBase, nsIEditor) + + already_AddRefed<nsIDOMDocument> GetDOMDocument(); + already_AddRefed<nsIDocument> GetDocument(); + already_AddRefed<nsIPresShell> GetPresShell(); + already_AddRefed<nsIWidget> GetWidget(); + enum NotificationForEditorObservers + { + eNotifyEditorObserversOfEnd, + eNotifyEditorObserversOfBefore, + eNotifyEditorObserversOfCancel + }; + void NotifyEditorObservers(NotificationForEditorObservers aNotification); + + // nsIEditor methods + NS_DECL_NSIEDITOR + + // nsIEditorIMESupport methods + NS_DECL_NSIEDITORIMESUPPORT + + // nsIPhonetic + NS_DECL_NSIPHONETIC + +public: + virtual bool IsModifiableNode(nsINode* aNode); + + virtual nsresult InsertTextImpl(const nsAString& aStringToInsert, + nsCOMPtr<nsINode>* aInOutNode, + int32_t* aInOutOffset, + nsIDocument* aDoc); + nsresult InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert, + Text& aTextNode, int32_t aOffset, + bool aSuppressIME = false); + NS_IMETHOD DeleteSelectionImpl(EDirection aAction, + EStripWrappers aStripWrappers); + + already_AddRefed<Element> DeleteSelectionAndCreateElement(nsIAtom& aTag); + + /** + * Helper routines for node/parent manipulations. + */ + nsresult DeleteNode(nsINode* aNode); + nsresult InsertNode(nsIContent& aNode, nsINode& aParent, int32_t aPosition); + enum ECloneAttributes { eDontCloneAttributes, eCloneAttributes }; + already_AddRefed<Element> ReplaceContainer(Element* aOldContainer, + nsIAtom* aNodeType, + nsIAtom* aAttribute = nullptr, + const nsAString* aValue = nullptr, + ECloneAttributes aCloneAttributes + = eDontCloneAttributes); + void CloneAttributes(Element* aDest, Element* aSource); + + nsresult RemoveContainer(nsIContent* aNode); + already_AddRefed<Element> InsertContainerAbove(nsIContent* aNode, + nsIAtom* aNodeType, + nsIAtom* aAttribute = nullptr, + const nsAString* aValue = + nullptr); + nsIContent* SplitNode(nsIContent& aNode, int32_t aOffset, + ErrorResult& aResult); + nsresult JoinNodes(nsINode& aLeftNode, nsINode& aRightNode); + nsresult MoveNode(nsIContent* aNode, nsINode* aParent, int32_t aOffset); + + /** + * Method to replace certain CreateElementNS() calls. + * + * @param aTag Tag you want. + */ + already_AddRefed<Element> CreateHTMLContent(nsIAtom* aTag); + + /** + * IME event handlers. + */ + virtual nsresult BeginIMEComposition(WidgetCompositionEvent* aEvent); + virtual nsresult UpdateIMEComposition(nsIDOMEvent* aDOMTextEvent) = 0; + void EndIMEComposition(); + + void SwitchTextDirectionTo(uint32_t aDirection); + +protected: + nsresult DetermineCurrentDirection(); + void FireInputEvent(); + + /** + * Create a transaction for setting aAttribute to aValue on aElement. Never + * returns null. + */ + already_AddRefed<ChangeAttributeTransaction> + CreateTxnForSetAttribute(Element& aElement, nsIAtom& aAttribute, + const nsAString& aValue); + + /** + * Create a transaction for removing aAttribute on aElement. Never returns + * null. + */ + already_AddRefed<ChangeAttributeTransaction> + CreateTxnForRemoveAttribute(Element& aElement, nsIAtom& aAttribute); + + /** + * Create a transaction for creating a new child node of aParent of type aTag. + */ + already_AddRefed<CreateElementTransaction> + CreateTxnForCreateElement(nsIAtom& aTag, + nsINode& aParent, + int32_t aPosition); + + already_AddRefed<Element> CreateNode(nsIAtom* aTag, nsINode* aParent, + int32_t aPosition); + + /** + * Create a transaction for inserting aNode as a child of aParent. + */ + already_AddRefed<InsertNodeTransaction> + CreateTxnForInsertNode(nsIContent& aNode, nsINode& aParent, + int32_t aOffset); + + /** + * Create a transaction for removing aNode from its parent. + */ + nsresult CreateTxnForDeleteNode(nsINode* aNode, + DeleteNodeTransaction** aTransaction); + + nsresult CreateTxnForDeleteSelection( + EDirection aAction, + EditAggregateTransaction** aTransaction, + nsINode** aNode, + int32_t* aOffset, + int32_t* aLength); + + nsresult CreateTxnForDeleteInsertionPoint( + nsRange* aRange, + EDirection aAction, + EditAggregateTransaction* aTransaction, + nsINode** aNode, + int32_t* aOffset, + int32_t* aLength); + + + /** + * Create a transaction for inserting aStringToInsert into aTextNode. Never + * returns null. + */ + already_AddRefed<mozilla::InsertTextTransaction> + CreateTxnForInsertText(const nsAString& aStringToInsert, Text& aTextNode, + int32_t aOffset); + + /** + * Never returns null. + */ + already_AddRefed<mozilla::CompositionTransaction> + CreateTxnForComposition(const nsAString& aStringToInsert); + + /** + * Create a transaction for adding a style sheet. + */ + NS_IMETHOD CreateTxnForAddStyleSheet( + StyleSheet* aSheet, + AddStyleSheetTransaction** aTransaction); + + /** + * Create a transaction for removing a style sheet. + */ + NS_IMETHOD CreateTxnForRemoveStyleSheet( + StyleSheet* aSheet, + RemoveStyleSheetTransaction** aTransaction); + + nsresult DeleteText(nsGenericDOMDataNode& aElement, + uint32_t aOffset, uint32_t aLength); + + already_AddRefed<DeleteTextTransaction> + CreateTxnForDeleteText(nsGenericDOMDataNode& aElement, + uint32_t aOffset, uint32_t aLength); + + already_AddRefed<DeleteTextTransaction> + CreateTxnForDeleteCharacter(nsGenericDOMDataNode& aData, uint32_t aOffset, + EDirection aDirection); + + already_AddRefed<SplitNodeTransaction> + CreateTxnForSplitNode(nsIContent& aNode, uint32_t aOffset); + + already_AddRefed<JoinNodeTransaction> + CreateTxnForJoinNode(nsINode& aLeftNode, nsINode& aRightNode); + + /** + * This method first deletes the selection, if it's not collapsed. Then if + * the selection lies in a CharacterData node, it splits it. If the + * selection is at this point collapsed in a CharacterData node, it's + * adjusted to be collapsed right before or after the node instead (which is + * always possible, since the node was split). + */ + nsresult DeleteSelectionAndPrepareToCreateNode(); + + /** + * Called after a transaction is done successfully. + */ + void DoAfterDoTransaction(nsITransaction *aTxn); + + /** + * Called after a transaction is undone successfully. + */ + + void DoAfterUndoTransaction(); + + /** + * Called after a transaction is redone successfully. + */ + void DoAfterRedoTransaction(); + + enum TDocumentListenerNotification + { + eDocumentCreated, + eDocumentToBeDestroyed, + eDocumentStateChanged + }; + + /** + * Tell the doc state listeners that the doc state has changed. + */ + NS_IMETHOD NotifyDocumentListeners( + TDocumentListenerNotification aNotificationType); + + /** + * Make the given selection span the entire document. + */ + virtual nsresult SelectEntireDocument(Selection* aSelection); + + /** + * Helper method for scrolling the selection into view after + * an edit operation. aScrollToAnchor should be true if you + * want to scroll to the point where the selection was started. + * If false, it attempts to scroll the end of the selection into view. + * + * Editor methods *should* call this method instead of the versions + * in the various selection interfaces, since this version makes sure + * that the editor's sync/async settings for reflowing, painting, and + * scrolling match. + */ + NS_IMETHOD ScrollSelectionIntoView(bool aScrollToAnchor); + + virtual bool IsBlockNode(nsINode* aNode); + + /** + * Helper for GetPriorNode() and GetNextNode(). + */ + nsIContent* FindNextLeafNode(nsINode* aCurrentNode, + bool aGoForward, + bool bNoBlockCrossing); + + virtual nsresult InstallEventListeners(); + virtual void CreateEventListeners(); + virtual void RemoveEventListeners(); + + /** + * Return true if spellchecking should be enabled for this editor. + */ + bool GetDesiredSpellCheckState(); + + bool CanEnableSpellCheck() + { + // Check for password/readonly/disabled, which are not spellchecked + // regardless of DOM. Also, check to see if spell check should be skipped + // or not. + return !IsPasswordEditor() && !IsReadonly() && !IsDisabled() && + !ShouldSkipSpellCheck(); + } + + /** + * EnsureComposition() should be called by composition event handlers. This + * tries to get the composition for the event and set it to mComposition. + * However, this may fail because the composition may be committed before + * the event comes to the editor. + * + * @return true if there is a composition. Otherwise, for example, + * a composition event handler in web contents moved focus + * for committing the composition, returns false. + */ + bool EnsureComposition(WidgetCompositionEvent* aCompositionEvent); + + nsresult GetSelection(SelectionType aSelectionType, + nsISelection** aSelection); + +public: + /** + * All editor operations which alter the doc should be prefaced + * with a call to StartOperation, naming the action and direction. + */ + NS_IMETHOD StartOperation(EditAction opID, + nsIEditor::EDirection aDirection); + + /** + * All editor operations which alter the doc should be followed + * with a call to EndOperation. + */ + NS_IMETHOD EndOperation(); + + /** + * Routines for managing the preservation of selection across + * various editor actions. + */ + bool ArePreservingSelection(); + void PreserveSelectionAcrossActions(Selection* aSel); + nsresult RestorePreservedSelection(Selection* aSel); + void StopPreservingSelection(); + + /** + * SplitNode() creates a new node identical to an existing node, and split + * the contents between the two nodes + * @param aExistingRightNode The node to split. It will become the new + * node's next sibling. + * @param aOffset The offset of aExistingRightNode's + * content|children to do the split at + * @param aNewLeftNode The new node resulting from the split, becomes + * aExistingRightNode's previous sibling. + */ + nsresult SplitNodeImpl(nsIContent& aExistingRightNode, + int32_t aOffset, + nsIContent& aNewLeftNode); + + /** + * JoinNodes() takes 2 nodes and merge their content|children. + * @param aNodeToKeep The node that will remain after the join. + * @param aNodeToJoin The node that will be joined with aNodeToKeep. + * There is no requirement that the two nodes be of the + * same type. + * @param aParent The parent of aNodeToKeep + */ + nsresult JoinNodesImpl(nsINode* aNodeToKeep, + nsINode* aNodeToJoin, + nsINode* aParent); + + /** + * Return the offset of aChild in aParent. Asserts fatally if parent or + * child is null, or parent is not child's parent. + */ + static int32_t GetChildOffset(nsIDOMNode* aChild, + nsIDOMNode* aParent); + + /** + * Set outOffset to the offset of aChild in the parent. + * Returns the parent of aChild. + */ + static already_AddRefed<nsIDOMNode> GetNodeLocation(nsIDOMNode* aChild, + int32_t* outOffset); + static nsINode* GetNodeLocation(nsINode* aChild, int32_t* aOffset); + + /** + * Returns the number of things inside aNode in the out-param aCount. + * @param aNode is the node to get the length of. + * If aNode is text, returns number of characters. + * If not, returns number of children nodes. + * @param aCount [OUT] the result of the above calculation. + */ + static nsresult GetLengthOfDOMNode(nsIDOMNode *aNode, uint32_t &aCount); + + /** + * Get the node immediately prior to aCurrentNode. + * @param aCurrentNode the node from which we start the search + * @param aEditableNode if true, only return an editable node + * @param aResultNode [OUT] the node that occurs before aCurrentNode in + * the tree, skipping non-editable nodes if + * aEditableNode is true. If there is no prior + * node, aResultNode will be nullptr. + * @param bNoBlockCrossing If true, don't move across "block" nodes, + * whatever that means. + */ + nsIContent* GetPriorNode(nsINode* aCurrentNode, bool aEditableNode, + bool aNoBlockCrossing = false); + + /** + * And another version that takes a {parent,offset} pair rather than a node. + */ + nsIContent* GetPriorNode(nsINode* aParentNode, + int32_t aOffset, + bool aEditableNode, + bool aNoBlockCrossing = false); + + + /** + * Get the node immediately after to aCurrentNode. + * @param aCurrentNode the node from which we start the search + * @param aEditableNode if true, only return an editable node + * @param aResultNode [OUT] the node that occurs after aCurrentNode in the + * tree, skipping non-editable nodes if + * aEditableNode is true. If there is no prior + * node, aResultNode will be nullptr. + */ + nsIContent* GetNextNode(nsINode* aCurrentNode, + bool aEditableNode, + bool bNoBlockCrossing = false); + + /** + * And another version that takes a {parent,offset} pair rather than a node. + */ + nsIContent* GetNextNode(nsINode* aParentNode, + int32_t aOffset, + bool aEditableNode, + bool aNoBlockCrossing = false); + + /** + * Helper for GetNextNode() and GetPriorNode(). + */ + nsIContent* FindNode(nsINode* aCurrentNode, + bool aGoForward, + bool aEditableNode, + bool bNoBlockCrossing); + /** + * Get the rightmost child of aCurrentNode; + * return nullptr if aCurrentNode has no children. + */ + nsIContent* GetRightmostChild(nsINode* aCurrentNode, + bool bNoBlockCrossing = false); + + /** + * Get the leftmost child of aCurrentNode; + * return nullptr if aCurrentNode has no children. + */ + nsIContent* GetLeftmostChild(nsINode *aCurrentNode, + bool bNoBlockCrossing = false); + + /** + * Returns true if aNode is of the type implied by aTag. + */ + static inline bool NodeIsType(nsIDOMNode* aNode, nsIAtom* aTag) + { + return GetTag(aNode) == aTag; + } + + /** + * Returns true if aParent can contain a child of type aTag. + */ + bool CanContain(nsINode& aParent, nsIContent& aChild); + bool CanContainTag(nsINode& aParent, nsIAtom& aTag); + bool TagCanContain(nsIAtom& aParentTag, nsIContent& aChild); + virtual bool TagCanContainTag(nsIAtom& aParentTag, nsIAtom& aChildTag); + + /** + * Returns true if aNode is our root node. + */ + bool IsRoot(nsIDOMNode* inNode); + bool IsRoot(nsINode* inNode); + bool IsEditorRoot(nsINode* aNode); + + /** + * Returns true if aNode is a descendant of our root node. + */ + bool IsDescendantOfRoot(nsIDOMNode* inNode); + bool IsDescendantOfRoot(nsINode* inNode); + bool IsDescendantOfEditorRoot(nsINode* aNode); + + /** + * Returns true if aNode is a container. + */ + virtual bool IsContainer(nsINode* aNode); + virtual bool IsContainer(nsIDOMNode* aNode); + + /** + * returns true if aNode is an editable node. + */ + bool IsEditable(nsIDOMNode* aNode); + virtual bool IsEditable(nsINode* aNode); + + /** + * Returns true if aNode is a MozEditorBogus node. + */ + bool IsMozEditorBogusNode(nsINode* aNode); + + /** + * Counts number of editable child nodes. + */ + uint32_t CountEditableChildren(nsINode* aNode); + + /** + * Find the deep first and last children. + */ + nsINode* GetFirstEditableNode(nsINode* aRoot); + + /** + * Returns current composition. + */ + TextComposition* GetComposition() const; + + /** + * Returns true if there is composition string and not fixed. + */ + bool IsIMEComposing() const; + + /** + * Returns true when inserting text should be a part of current composition. + */ + bool ShouldHandleIMEComposition() const; + + /** + * From html rules code - migration in progress. + */ + static nsresult GetTagString(nsIDOMNode* aNode, nsAString& outString); + static nsIAtom* GetTag(nsIDOMNode* aNode); + + bool NodesSameType(nsIDOMNode* aNode1, nsIDOMNode* aNode2); + virtual bool AreNodesSameType(nsIContent* aNode1, nsIContent* aNode2); + + static bool IsTextNode(nsIDOMNode* aNode); + static bool IsTextNode(nsINode* aNode); + + static nsCOMPtr<nsIDOMNode> GetChildAt(nsIDOMNode* aParent, int32_t aOffset); + static nsIContent* GetNodeAtRangeOffsetPoint(nsIDOMNode* aParentOrNode, + int32_t aOffset); + + static nsresult GetStartNodeAndOffset(Selection* aSelection, + nsIDOMNode** outStartNode, + int32_t* outStartOffset); + static nsresult GetStartNodeAndOffset(Selection* aSelection, + nsINode** aStartNode, + int32_t* aStartOffset); + static nsresult GetEndNodeAndOffset(Selection* aSelection, + nsIDOMNode** outEndNode, + int32_t* outEndOffset); + static nsresult GetEndNodeAndOffset(Selection* aSelection, + nsINode** aEndNode, + int32_t* aEndOffset); +#if DEBUG_JOE + static void DumpNode(nsIDOMNode* aNode, int32_t indent = 0); +#endif + Selection* GetSelection(SelectionType aSelectionType = + SelectionType::eNormal); + + /** + * Helpers to add a node to the selection. + * Used by table cell selection methods. + */ + nsresult CreateRange(nsIDOMNode* aStartParent, int32_t aStartOffset, + nsIDOMNode* aEndParent, int32_t aEndOffset, + nsRange** aRange); + + /** + * Creates a range with just the supplied node and appends that to the + * selection. + */ + nsresult AppendNodeToSelectionAsRange(nsIDOMNode *aNode); + + /** + * When you are using AppendNodeToSelectionAsRange(), call this first to + * start a new selection. + */ + nsresult ClearSelection(); + + nsresult IsPreformatted(nsIDOMNode* aNode, bool* aResult); + + enum class EmptyContainers { no, yes }; + int32_t SplitNodeDeep(nsIContent& aNode, nsIContent& aSplitPointParent, + int32_t aSplitPointOffset, + EmptyContainers aEmptyContainers = + EmptyContainers::yes, + nsIContent** outLeftNode = nullptr, + nsIContent** outRightNode = nullptr); + EditorDOMPoint JoinNodeDeep(nsIContent& aLeftNode, + nsIContent& aRightNode); + + nsresult GetString(const nsAString& name, nsAString& value); + + void BeginUpdateViewBatch(); + virtual nsresult EndUpdateViewBatch(); + + bool GetShouldTxnSetSelection(); + + virtual nsresult HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent); + + nsresult HandleInlineSpellCheck(EditAction action, + Selection* aSelection, + nsIDOMNode* previousSelectedNode, + int32_t previousSelectedOffset, + nsIDOMNode* aStartNode, + int32_t aStartOffset, + nsIDOMNode* aEndNode, + int32_t aEndOffset); + + virtual already_AddRefed<dom::EventTarget> GetDOMEventTarget() = 0; + + /** + * Fast non-refcounting editor root element accessor + */ + Element* GetRoot(); + + /** + * Likewise, but gets the editor's root instead, which is different for HTML + * editors. + */ + virtual Element* GetEditorRoot(); + + /** + * Likewise, but gets the text control element instead of the root for + * plaintext editors. + */ + Element* GetExposedRoot(); + + /** + * Accessor methods to flags. + */ + bool IsPlaintextEditor() const + { + return (mFlags & nsIPlaintextEditor::eEditorPlaintextMask) != 0; + } + + bool IsSingleLineEditor() const + { + return (mFlags & nsIPlaintextEditor::eEditorSingleLineMask) != 0; + } + + bool IsPasswordEditor() const + { + return (mFlags & nsIPlaintextEditor::eEditorPasswordMask) != 0; + } + + bool IsReadonly() const + { + return (mFlags & nsIPlaintextEditor::eEditorReadonlyMask) != 0; + } + + bool IsDisabled() const + { + return (mFlags & nsIPlaintextEditor::eEditorDisabledMask) != 0; + } + + bool IsInputFiltered() const + { + return (mFlags & nsIPlaintextEditor::eEditorFilterInputMask) != 0; + } + + bool IsMailEditor() const + { + return (mFlags & nsIPlaintextEditor::eEditorMailMask) != 0; + } + + bool IsWrapHackEnabled() const + { + return (mFlags & nsIPlaintextEditor::eEditorEnableWrapHackMask) != 0; + } + + bool IsFormWidget() const + { + return (mFlags & nsIPlaintextEditor::eEditorWidgetMask) != 0; + } + + bool NoCSS() const + { + return (mFlags & nsIPlaintextEditor::eEditorNoCSSMask) != 0; + } + + bool IsInteractionAllowed() const + { + return (mFlags & nsIPlaintextEditor::eEditorAllowInteraction) != 0; + } + + bool DontEchoPassword() const + { + return (mFlags & nsIPlaintextEditor::eEditorDontEchoPassword) != 0; + } + + bool ShouldSkipSpellCheck() const + { + return (mFlags & nsIPlaintextEditor::eEditorSkipSpellCheck) != 0; + } + + bool IsTabbable() const + { + return IsSingleLineEditor() || IsPasswordEditor() || IsFormWidget() || + IsInteractionAllowed(); + } + + bool HasIndependentSelection() const + { + return !!mSelConWeak; + } + + /** + * Get the input event target. This might return null. + */ + virtual already_AddRefed<nsIContent> GetInputEventTargetContent() = 0; + + /** + * Get the focused content, if we're focused. Returns null otherwise. + */ + virtual already_AddRefed<nsIContent> GetFocusedContent(); + + /** + * Get the focused content for the argument of some IMEStateManager's + * methods. + */ + virtual already_AddRefed<nsIContent> GetFocusedContentForIME(); + + /** + * Whether the editor is active on the DOM window. Note that when this + * returns true but GetFocusedContent() returns null, it means that this editor was + * focused when the DOM window was active. + */ + virtual bool IsActiveInDOMWindow(); + + /** + * Whether the aEvent should be handled by this editor or not. When this + * returns FALSE, The aEvent shouldn't be handled on this editor, + * i.e., The aEvent should be handled by another inner editor or ancestor + * elements. + */ + virtual bool IsAcceptableInputEvent(nsIDOMEvent* aEvent); + + /** + * FindSelectionRoot() returns a selection root of this editor when aNode + * gets focus. aNode must be a content node or a document node. When the + * target isn't a part of this editor, returns nullptr. If this is for + * designMode, you should set the document node to aNode except that an + * element in the document has focus. + */ + virtual already_AddRefed<nsIContent> FindSelectionRoot(nsINode* aNode); + + /** + * Initializes selection and caret for the editor. If aEventTarget isn't + * a host of the editor, i.e., the editor doesn't get focus, this does + * nothing. + */ + nsresult InitializeSelection(nsIDOMEventTarget* aFocusEventTarget); + + /** + * This method has to be called by EditorEventListener::Focus. + * All actions that have to be done when the editor is focused needs to be + * added here. + */ + void OnFocus(nsIDOMEventTarget* aFocusEventTarget); + + /** + * Used to insert content from a data transfer into the editable area. + * This is called for each item in the data transfer, with the index of + * each item passed as aIndex. + */ + virtual nsresult InsertFromDataTransfer(dom::DataTransfer* aDataTransfer, + int32_t aIndex, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) = 0; + + virtual nsresult InsertFromDrop(nsIDOMEvent* aDropEvent) = 0; + + virtual already_AddRefed<nsIDOMNode> FindUserSelectAllNode(nsIDOMNode* aNode) + { + return nullptr; + } + + /** + * GetIMESelectionStartOffsetIn() returns the start offset of IME selection in + * the aTextNode. If there is no IME selection, returns -1. + */ + int32_t GetIMESelectionStartOffsetIn(nsINode* aTextNode); + + /** + * FindBetterInsertionPoint() tries to look for better insertion point which + * is typically the nearest text node and offset in it. + */ + void FindBetterInsertionPoint(nsCOMPtr<nsIDOMNode>& aNode, + int32_t& aOffset); + void FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode, + int32_t& aOffset); + + /** + * HideCaret() hides caret with nsCaret::AddForceHide() or may show carent + * with nsCaret::RemoveForceHide(). This does NOT set visibility of + * nsCaret. Therefore, this is stateless. + */ + void HideCaret(bool aHide); + + void FlushFrames() + { + nsCOMPtr<nsIDocument> doc = GetDocument(); + if (doc) { + doc->FlushPendingNotifications(Flush_Frames); + } + } + +protected: + enum Tristate + { + eTriUnset, + eTriFalse, + eTriTrue + }; + + // MIME type of the doc we are editing. + nsCString mContentMIMEType; + + nsCOMPtr<nsIInlineSpellChecker> mInlineSpellChecker; + + RefPtr<nsTransactionManager> mTxnMgr; + // Cached root node. + nsCOMPtr<Element> mRootElement; + // Current IME text node. + RefPtr<Text> mIMETextNode; + // The form field as an event receiver. + nsCOMPtr<dom::EventTarget> mEventTarget; + nsCOMPtr<nsIDOMEventListener> mEventListener; + // Weak reference to the nsISelectionController. + nsWeakPtr mSelConWeak; + // Weak reference to placeholder for begin/end batch purposes. + nsWeakPtr mPlaceHolderTxn; + // Weak reference to the nsIDOMDocument. + nsWeakPtr mDocWeak; + // Name of placeholder transaction. + nsIAtom* mPlaceHolderName; + // Saved selection state for placeholder transaction batching. + SelectionState* mSelState; + nsString* mPhonetic; + // IME composition this is not null between compositionstart and + // compositionend. + RefPtr<TextComposition> mComposition; + + // Listens to all low level actions on the doc. + nsTArray<OwningNonNull<nsIEditActionListener>> mActionListeners; + // Just notify once per high level change. + nsTArray<OwningNonNull<nsIEditorObserver>> mEditorObservers; + // Listen to overall doc state (dirty or not, just created, etc.). + nsTArray<OwningNonNull<nsIDocumentStateListener>> mDocStateListeners; + + // Cached selection for AutoSelectionRestorer. + SelectionState mSavedSel; + // Utility class object for maintaining preserved ranges. + RangeUpdater mRangeUpdater; + + // Number of modifications (for undo/redo stack). + uint32_t mModCount; + // Behavior flags. See nsIPlaintextEditor.idl for the flags we use. + uint32_t mFlags; + + int32_t mUpdateCount; + + // Nesting count for batching. + int32_t mPlaceHolderBatch; + // The current editor action. + EditAction mAction; + + // Offset in text node where IME comp string begins. + uint32_t mIMETextOffset; + // The Length of the composition string or commit string. If this is length + // of commit string, the length is truncated by maxlength attribute. + uint32_t mIMETextLength; + + // The current direction of editor action. + EDirection mDirection; + // -1 = not initialized + int8_t mDocDirtyState; + // A Tristate value. + uint8_t mSpellcheckCheckboxState; + + // Turn off for conservative selection adjustment by transactions. + bool mShouldTxnSetSelection; + // Whether PreDestroy has been called. + bool mDidPreDestroy; + // Whether PostCreate has been called. + bool mDidPostCreate; + bool mDispatchInputEvent; + // True while the instance is handling an edit action. + bool mIsInEditAction; + // Whether caret is hidden forcibly. + bool mHidingCaret; + + friend bool NSCanUnload(nsISupports* serviceMgr); + friend class AutoRules; + friend class AutoSelectionRestorer; + friend class AutoTransactionsConserveSelection; + friend class RangeUpdater; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_EditorBase_h diff --git a/editor/libeditor/EditorCommands.cpp b/editor/libeditor/EditorCommands.cpp new file mode 100644 index 000000000..2bb32e2aa --- /dev/null +++ b/editor/libeditor/EditorCommands.cpp @@ -0,0 +1,1169 @@ +/* -*- 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 "EditorCommands.h" + +#include "mozFlushType.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/TextEditor.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIClipboard.h" +#include "nsICommandParams.h" +#include "nsID.h" +#include "nsIDOMDocument.h" +#include "nsIDocument.h" +#include "nsIEditor.h" +#include "nsIEditorMailSupport.h" +#include "nsIPlaintextEditor.h" +#include "nsISelection.h" +#include "nsISelectionController.h" +#include "nsITransferable.h" +#include "nsString.h" +#include "nsAString.h" + +class nsISupports; + +#define STATE_ENABLED "state_enabled" +#define STATE_DATA "state_data" + +namespace mozilla { + +/****************************************************************************** + * mozilla::EditorCommandBase + ******************************************************************************/ + +EditorCommandBase::EditorCommandBase() +{ +} + +NS_IMPL_ISUPPORTS(EditorCommandBase, nsIControllerCommand) + +/****************************************************************************** + * mozilla::UndoCommand + ******************************************************************************/ + +NS_IMETHODIMP +UndoCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + bool isEnabled, isEditable = false; + nsresult rv = editor->GetIsSelectionEditable(&isEditable); + NS_ENSURE_SUCCESS(rv, rv); + if (isEditable) + return editor->CanUndo(&isEnabled, aIsEnabled); + } + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +UndoCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->Undo(1); + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +UndoCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +UndoCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::RedoCommand + ******************************************************************************/ + +NS_IMETHODIMP +RedoCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + bool isEnabled, isEditable = false; + nsresult rv = editor->GetIsSelectionEditable(&isEditable); + NS_ENSURE_SUCCESS(rv, rv); + if (isEditable) + return editor->CanRedo(&isEnabled, aIsEnabled); + } + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +RedoCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->Redo(1); + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +RedoCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +RedoCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::ClearUndoCommand + ******************************************************************************/ + +NS_IMETHODIMP +ClearUndoCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->GetIsSelectionEditable(aIsEnabled); + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +ClearUndoCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + NS_ENSURE_TRUE(editor, NS_ERROR_NOT_IMPLEMENTED); + + editor->EnableUndo(false); // Turning off undo clears undo/redo stacks. + editor->EnableUndo(true); // This re-enables undo/redo. + + return NS_OK; +} + +NS_IMETHODIMP +ClearUndoCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +ClearUndoCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + NS_ENSURE_ARG_POINTER(aParams); + + bool enabled; + nsresult rv = IsCommandEnabled(aCommandName, aCommandRefCon, &enabled); + NS_ENSURE_SUCCESS(rv, rv); + + return aParams->SetBooleanValue(STATE_ENABLED, enabled); +} + +/****************************************************************************** + * mozilla::CutCommand + ******************************************************************************/ + +NS_IMETHODIMP +CutCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + bool isEditable = false; + nsresult rv = editor->GetIsSelectionEditable(&isEditable); + NS_ENSURE_SUCCESS(rv, rv); + if (isEditable) + return editor->CanCut(aIsEnabled); + } + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +CutCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->Cut(); + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +CutCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +CutCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::CutOrDeleteCommand + ******************************************************************************/ + +NS_IMETHODIMP +CutOrDeleteCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->GetIsSelectionEditable(aIsEnabled); + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +CutOrDeleteCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + nsCOMPtr<nsISelection> selection; + nsresult rv = editor->GetSelection(getter_AddRefs(selection)); + if (NS_SUCCEEDED(rv) && selection && selection->Collapsed()) { + return editor->DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip); + } + return editor->Cut(); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +CutOrDeleteCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +CutOrDeleteCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::CopyCommand + ******************************************************************************/ + +NS_IMETHODIMP +CopyCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->CanCopy(aIsEnabled); + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +CopyCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->Copy(); + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +CopyCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +CopyCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::CopyOrDeleteCommand + ******************************************************************************/ + +NS_IMETHODIMP +CopyOrDeleteCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->GetIsSelectionEditable(aIsEnabled); + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +CopyOrDeleteCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + nsCOMPtr<nsISelection> selection; + nsresult rv = editor->GetSelection(getter_AddRefs(selection)); + if (NS_SUCCEEDED(rv) && selection && selection->Collapsed()) { + return editor->DeleteSelection(nsIEditor::eNextWord, nsIEditor::eStrip); + } + return editor->Copy(); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +CopyOrDeleteCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +CopyOrDeleteCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::CopyAndCollapseToEndCommand + ******************************************************************************/ + +NS_IMETHODIMP +CopyAndCollapseToEndCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->CanCopy(aIsEnabled); + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +CopyAndCollapseToEndCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + nsresult rv = editor->Copy(); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsISelection> selection; + rv = editor->GetSelection(getter_AddRefs(selection)); + if (NS_SUCCEEDED(rv) && selection) { + selection->CollapseToEnd(); + } + return rv; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +CopyAndCollapseToEndCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +CopyAndCollapseToEndCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::PasteCommand + ******************************************************************************/ + +NS_IMETHODIMP +PasteCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + bool isEditable = false; + nsresult rv = editor->GetIsSelectionEditable(&isEditable); + NS_ENSURE_SUCCESS(rv, rv); + if (isEditable) + return editor->CanPaste(nsIClipboard::kGlobalClipboard, aIsEnabled); + } + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +PasteCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE); + + return editor->Paste(nsIClipboard::kGlobalClipboard); +} + +NS_IMETHODIMP +PasteCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +PasteCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::PasteTransferableCommand + ******************************************************************************/ + +NS_IMETHODIMP +PasteTransferableCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + bool isEditable = false; + nsresult rv = editor->GetIsSelectionEditable(&isEditable); + NS_ENSURE_SUCCESS(rv, rv); + if (isEditable) + return editor->CanPasteTransferable(nullptr, aIsEnabled); + } + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +PasteTransferableCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +PasteTransferableCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE); + + nsCOMPtr<nsISupports> supports; + aParams->GetISupportsValue("transferable", getter_AddRefs(supports)); + NS_ENSURE_TRUE(supports, NS_ERROR_FAILURE); + + nsCOMPtr<nsITransferable> trans = do_QueryInterface(supports); + NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE); + + return editor->PasteTransferable(trans); +} + +NS_IMETHODIMP +PasteTransferableCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE); + + nsCOMPtr<nsITransferable> trans; + + nsCOMPtr<nsISupports> supports; + aParams->GetISupportsValue("transferable", getter_AddRefs(supports)); + if (supports) { + trans = do_QueryInterface(supports); + NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE); + } + + bool canPaste; + nsresult rv = editor->CanPasteTransferable(trans, &canPaste); + NS_ENSURE_SUCCESS(rv, rv); + + return aParams->SetBooleanValue(STATE_ENABLED, canPaste); +} + +/****************************************************************************** + * mozilla::SwitchTextDirectionCommand + ******************************************************************************/ + +NS_IMETHODIMP +SwitchTextDirectionCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->GetIsSelectionEditable(aIsEnabled); + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +SwitchTextDirectionCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE); + + return editor->SwitchTextDirection(); +} + +NS_IMETHODIMP +SwitchTextDirectionCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +SwitchTextDirectionCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canSwitchTextDirection = true; + IsCommandEnabled(aCommandName, aCommandRefCon, &canSwitchTextDirection); + return aParams->SetBooleanValue(STATE_ENABLED, canSwitchTextDirection); +} + +/****************************************************************************** + * mozilla::DeleteCommand + ******************************************************************************/ + +NS_IMETHODIMP +DeleteCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + *aIsEnabled = false; + + if (!editor) { + return NS_OK; + } + + // We can generally delete whenever the selection is editable. However, + // cmd_delete doesn't make sense if the selection is collapsed because it's + // directionless, which is the same condition under which we can't cut. + nsresult rv = editor->GetIsSelectionEditable(aIsEnabled); + NS_ENSURE_SUCCESS(rv, rv); + + if (!nsCRT::strcmp("cmd_delete", aCommandName) && *aIsEnabled) { + rv = editor->CanDelete(aIsEnabled); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +DeleteCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE); + + nsIEditor::EDirection deleteDir = nsIEditor::eNone; + + if (!nsCRT::strcmp("cmd_delete", aCommandName)) { + // Really this should probably be eNone, but it only makes a difference if + // the selection is collapsed, and then this command is disabled. So let's + // keep it as it always was to avoid breaking things. + deleteDir = nsIEditor::ePrevious; + } else if (!nsCRT::strcmp("cmd_deleteCharForward", aCommandName)) { + deleteDir = nsIEditor::eNext; + } else if (!nsCRT::strcmp("cmd_deleteCharBackward", aCommandName)) { + deleteDir = nsIEditor::ePrevious; + } else if (!nsCRT::strcmp("cmd_deleteWordBackward", aCommandName)) { + deleteDir = nsIEditor::ePreviousWord; + } else if (!nsCRT::strcmp("cmd_deleteWordForward", aCommandName)) { + deleteDir = nsIEditor::eNextWord; + } else if (!nsCRT::strcmp("cmd_deleteToBeginningOfLine", aCommandName)) { + deleteDir = nsIEditor::eToBeginningOfLine; + } else if (!nsCRT::strcmp("cmd_deleteToEndOfLine", aCommandName)) { + deleteDir = nsIEditor::eToEndOfLine; + } else { + MOZ_CRASH("Unrecognized nsDeleteCommand"); + } + + return editor->DeleteSelection(deleteDir, nsIEditor::eStrip); +} + +NS_IMETHODIMP +DeleteCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +DeleteCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::SelectAllCommand + ******************************************************************************/ + +NS_IMETHODIMP +SelectAllCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + + nsresult rv = NS_OK; + // You can always select all, unless the selection is editable, + // and the editable region is empty! + *aIsEnabled = true; + bool docIsEmpty; + + // you can select all if there is an editor which is non-empty + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + rv = editor->GetDocumentIsEmpty(&docIsEmpty); + NS_ENSURE_SUCCESS(rv, rv); + *aIsEnabled = !docIsEmpty; + } + + return rv; +} + +NS_IMETHODIMP +SelectAllCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->SelectAll(); + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +SelectAllCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +SelectAllCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::SelectionMoveCommands + ******************************************************************************/ + +NS_IMETHODIMP +SelectionMoveCommands::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->GetIsSelectionEditable(aIsEnabled); + + *aIsEnabled = false; + return NS_OK; +} + +static const struct ScrollCommand { + const char *reverseScroll; + const char *forwardScroll; + nsresult (NS_STDCALL nsISelectionController::*scroll)(bool); +} scrollCommands[] = { + { "cmd_scrollTop", "cmd_scrollBottom", + &nsISelectionController::CompleteScroll }, + { "cmd_scrollPageUp", "cmd_scrollPageDown", + &nsISelectionController::ScrollPage }, + { "cmd_scrollLineUp", "cmd_scrollLineDown", + &nsISelectionController::ScrollLine } +}; + +static const struct MoveCommand { + const char *reverseMove; + const char *forwardMove; + const char *reverseSelect; + const char *forwardSelect; + nsresult (NS_STDCALL nsISelectionController::*move)(bool, bool); +} moveCommands[] = { + { "cmd_charPrevious", "cmd_charNext", + "cmd_selectCharPrevious", "cmd_selectCharNext", + &nsISelectionController::CharacterMove }, + { "cmd_linePrevious", "cmd_lineNext", + "cmd_selectLinePrevious", "cmd_selectLineNext", + &nsISelectionController::LineMove }, + { "cmd_wordPrevious", "cmd_wordNext", + "cmd_selectWordPrevious", "cmd_selectWordNext", + &nsISelectionController::WordMove }, + { "cmd_beginLine", "cmd_endLine", + "cmd_selectBeginLine", "cmd_selectEndLine", + &nsISelectionController::IntraLineMove }, + { "cmd_movePageUp", "cmd_movePageDown", + "cmd_selectPageUp", "cmd_selectPageDown", + &nsISelectionController::PageMove }, + { "cmd_moveTop", "cmd_moveBottom", + "cmd_selectTop", "cmd_selectBottom", + &nsISelectionController::CompleteMove } +}; + +static const struct PhysicalCommand { + const char *move; + const char *select; + int16_t direction; + int16_t amount; +} physicalCommands[] = { + { "cmd_moveLeft", "cmd_selectLeft", + nsISelectionController::MOVE_LEFT, 0 }, + { "cmd_moveRight", "cmd_selectRight", + nsISelectionController::MOVE_RIGHT, 0 }, + { "cmd_moveUp", "cmd_selectUp", + nsISelectionController::MOVE_UP, 0 }, + { "cmd_moveDown", "cmd_selectDown", + nsISelectionController::MOVE_DOWN, 0 }, + { "cmd_moveLeft2", "cmd_selectLeft2", + nsISelectionController::MOVE_LEFT, 1 }, + { "cmd_moveRight2", "cmd_selectRight2", + nsISelectionController::MOVE_RIGHT, 1 }, + { "cmd_moveUp2", "cmd_selectUp2", + nsISelectionController::MOVE_UP, 1 }, + { "cmd_moveDown2", "cmd_selectDown2", + nsISelectionController::MOVE_DOWN, 1 } +}; + +NS_IMETHODIMP +SelectionMoveCommands::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + NS_ENSURE_TRUE(editor, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMDocument> domDoc; + editor->GetDocument(getter_AddRefs(domDoc)); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc); + if (doc) { + // Most of the commands below (possibly all of them) need layout to + // be up to date. + doc->FlushPendingNotifications(Flush_Layout); + } + + nsCOMPtr<nsISelectionController> selCont; + nsresult rv = editor->GetSelectionController(getter_AddRefs(selCont)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(selCont, NS_ERROR_FAILURE); + + // scroll commands + for (size_t i = 0; i < mozilla::ArrayLength(scrollCommands); i++) { + const ScrollCommand &cmd = scrollCommands[i]; + if (!nsCRT::strcmp(aCommandName, cmd.reverseScroll)) { + return (selCont->*(cmd.scroll))(false); + } else if (!nsCRT::strcmp(aCommandName, cmd.forwardScroll)) { + return (selCont->*(cmd.scroll))(true); + } + } + + // caret movement/selection commands + for (size_t i = 0; i < mozilla::ArrayLength(moveCommands); i++) { + const MoveCommand &cmd = moveCommands[i]; + if (!nsCRT::strcmp(aCommandName, cmd.reverseMove)) { + return (selCont->*(cmd.move))(false, false); + } else if (!nsCRT::strcmp(aCommandName, cmd.forwardMove)) { + return (selCont->*(cmd.move))(true, false); + } else if (!nsCRT::strcmp(aCommandName, cmd.reverseSelect)) { + return (selCont->*(cmd.move))(false, true); + } else if (!nsCRT::strcmp(aCommandName, cmd.forwardSelect)) { + return (selCont->*(cmd.move))(true, true); + } + } + + // physical-direction movement/selection + for (size_t i = 0; i < mozilla::ArrayLength(physicalCommands); i++) { + const PhysicalCommand &cmd = physicalCommands[i]; + if (!nsCRT::strcmp(aCommandName, cmd.move)) { + return selCont->PhysicalMove(cmd.direction, cmd.amount, false); + } else if (!nsCRT::strcmp(aCommandName, cmd.select)) { + return selCont->PhysicalMove(cmd.direction, cmd.amount, true); + } + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +SelectionMoveCommands::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +SelectionMoveCommands::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + bool canUndo; + IsCommandEnabled(aCommandName, aCommandRefCon, &canUndo); + return aParams->SetBooleanValue(STATE_ENABLED, canUndo); +} + +/****************************************************************************** + * mozilla::InsertPlaintextCommand + ******************************************************************************/ + +NS_IMETHODIMP +InsertPlaintextCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) + return editor->GetIsSelectionEditable(aIsEnabled); + + *aIsEnabled = false; + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InsertPlaintextCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InsertPlaintextCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + NS_ENSURE_ARG_POINTER(aParams); + + nsCOMPtr<nsIPlaintextEditor> editor = do_QueryInterface(aCommandRefCon); + NS_ENSURE_TRUE(editor, NS_ERROR_NOT_IMPLEMENTED); + + // Get text to insert from command params + nsAutoString text; + nsresult rv = aParams->GetStringValue(STATE_DATA, text); + NS_ENSURE_SUCCESS(rv, rv); + + if (!text.IsEmpty()) + return editor->InsertText(text); + + return NS_OK; +} + +NS_IMETHODIMP +InsertPlaintextCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + if (NS_WARN_IF(!aParams)) { + return NS_ERROR_INVALID_ARG; + } + + bool aIsEnabled = false; + IsCommandEnabled(aCommandName, aCommandRefCon, &aIsEnabled); + return aParams->SetBooleanValue(STATE_ENABLED, aIsEnabled); +} + +/****************************************************************************** + * mozilla::InsertParagraphCommand + ******************************************************************************/ + +NS_IMETHODIMP +InsertParagraphCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + if (NS_WARN_IF(!aIsEnabled)) { + return NS_ERROR_INVALID_ARG; + } + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (NS_WARN_IF(!editor)) { + *aIsEnabled = false; + return NS_ERROR_NOT_IMPLEMENTED; + } + + return editor->GetIsSelectionEditable(aIsEnabled); +} + +NS_IMETHODIMP +InsertParagraphCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIPlaintextEditor> editor = do_QueryInterface(aCommandRefCon); + if (NS_WARN_IF(!editor)) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + TextEditor* textEditor = static_cast<TextEditor*>(editor.get()); + + return textEditor->TypedText(EmptyString(), TextEditor::eTypedBreak); +} + +NS_IMETHODIMP +InsertParagraphCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +InsertParagraphCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + if (NS_WARN_IF(!aParams)) { + return NS_ERROR_INVALID_ARG; + } + + bool aIsEnabled = false; + IsCommandEnabled(aCommandName, aCommandRefCon, &aIsEnabled); + return aParams->SetBooleanValue(STATE_ENABLED, aIsEnabled); +} + +/****************************************************************************** + * mozilla::InsertLineBreakCommand + ******************************************************************************/ + +NS_IMETHODIMP +InsertLineBreakCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + if (NS_WARN_IF(!aIsEnabled)) { + return NS_ERROR_INVALID_ARG; + } + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (NS_WARN_IF(!editor)) { + *aIsEnabled = false; + return NS_ERROR_NOT_IMPLEMENTED; + } + + return editor->GetIsSelectionEditable(aIsEnabled); +} + +NS_IMETHODIMP +InsertLineBreakCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIPlaintextEditor> editor = do_QueryInterface(aCommandRefCon); + if (NS_WARN_IF(!editor)) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + TextEditor* textEditor = static_cast<TextEditor*>(editor.get()); + + return textEditor->TypedText(EmptyString(), TextEditor::eTypedBR); +} + +NS_IMETHODIMP +InsertLineBreakCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + return DoCommand(aCommandName, aCommandRefCon); +} + +NS_IMETHODIMP +InsertLineBreakCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + if (NS_WARN_IF(!aParams)) { + return NS_ERROR_INVALID_ARG; + } + + bool aIsEnabled = false; + IsCommandEnabled(aCommandName, aCommandRefCon, &aIsEnabled); + return aParams->SetBooleanValue(STATE_ENABLED, aIsEnabled); +} + +/****************************************************************************** + * mozilla::PasteQuotationCommand + ******************************************************************************/ + +NS_IMETHODIMP +PasteQuotationCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + NS_ENSURE_ARG_POINTER(aIsEnabled); + + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + nsCOMPtr<nsIEditorMailSupport> mailEditor = do_QueryInterface(aCommandRefCon); + if (editor && mailEditor) { + uint32_t flags; + editor->GetFlags(&flags); + if (!(flags & nsIPlaintextEditor::eEditorSingleLineMask)) + return editor->CanPaste(nsIClipboard::kGlobalClipboard, aIsEnabled); + } + + *aIsEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +PasteQuotationCommand::DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditorMailSupport> mailEditor = do_QueryInterface(aCommandRefCon); + if (mailEditor) + return mailEditor->PasteAsQuotation(nsIClipboard::kGlobalClipboard); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +PasteQuotationCommand::DoCommandParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditorMailSupport> mailEditor = do_QueryInterface(aCommandRefCon); + if (mailEditor) + return mailEditor->PasteAsQuotation(nsIClipboard::kGlobalClipboard); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +PasteQuotationCommand::GetCommandStateParams(const char* aCommandName, + nsICommandParams* aParams, + nsISupports* aCommandRefCon) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon); + if (editor) { + bool enabled = false; + editor->CanPaste(nsIClipboard::kGlobalClipboard, &enabled); + aParams->SetBooleanValue(STATE_ENABLED, enabled); + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/EditorCommands.h b/editor/libeditor/EditorCommands.h new file mode 100644 index 000000000..d7dc27b94 --- /dev/null +++ b/editor/libeditor/EditorCommands.h @@ -0,0 +1,106 @@ +/* -*- 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/. */ + +#ifndef EditorCommands_h_ +#define EditorCommands_h_ + +#include "nsIControllerCommand.h" +#include "nsISupportsImpl.h" +#include "nscore.h" + +class nsICommandParams; +class nsISupports; + +namespace mozilla { + +/** + * This is a virtual base class for commands registered with the editor + * controller. Note that such commands can be shared by more than on editor + * instance, so MUST be stateless. Any state must be stored via the refCon + * (an nsIEditor). + */ + +class EditorCommandBase : public nsIControllerCommand +{ +public: + EditorCommandBase(); + + NS_DECL_ISUPPORTS + + NS_IMETHOD IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) override = 0; + NS_IMETHOD DoCommand(const char* aCommandName, + nsISupports* aCommandRefCon) override = 0; + +protected: + virtual ~EditorCommandBase() {} +}; + + +#define NS_DECL_EDITOR_COMMAND(_cmd) \ +class _cmd final : public EditorCommandBase \ +{ \ +public: \ + NS_IMETHOD IsCommandEnabled(const char* aCommandName, \ + nsISupports* aCommandRefCon, \ + bool* aIsEnabled) override; \ + NS_IMETHOD DoCommand(const char* aCommandName, \ + nsISupports* aCommandRefCon) override; \ + NS_IMETHOD DoCommandParams(const char* aCommandName, \ + nsICommandParams* aParams, \ + nsISupports* aCommandRefCon) override; \ + NS_IMETHOD GetCommandStateParams(const char* aCommandName, \ + nsICommandParams* aParams, \ + nsISupports* aCommandRefCon) override; \ +}; + +// basic editor commands +NS_DECL_EDITOR_COMMAND(UndoCommand) +NS_DECL_EDITOR_COMMAND(RedoCommand) +NS_DECL_EDITOR_COMMAND(ClearUndoCommand) + +NS_DECL_EDITOR_COMMAND(CutCommand) +NS_DECL_EDITOR_COMMAND(CutOrDeleteCommand) +NS_DECL_EDITOR_COMMAND(CopyCommand) +NS_DECL_EDITOR_COMMAND(CopyOrDeleteCommand) +NS_DECL_EDITOR_COMMAND(CopyAndCollapseToEndCommand) +NS_DECL_EDITOR_COMMAND(PasteCommand) +NS_DECL_EDITOR_COMMAND(PasteTransferableCommand) +NS_DECL_EDITOR_COMMAND(SwitchTextDirectionCommand) +NS_DECL_EDITOR_COMMAND(DeleteCommand) +NS_DECL_EDITOR_COMMAND(SelectAllCommand) + +NS_DECL_EDITOR_COMMAND(SelectionMoveCommands) + +// Insert content commands +NS_DECL_EDITOR_COMMAND(InsertPlaintextCommand) +NS_DECL_EDITOR_COMMAND(InsertParagraphCommand) +NS_DECL_EDITOR_COMMAND(InsertLineBreakCommand) +NS_DECL_EDITOR_COMMAND(PasteQuotationCommand) + + +#if 0 +// template for new command +NS_IMETHODIMP +FooCommand::IsCommandEnabled(const char* aCommandName, + nsISupports* aCommandRefCon, + bool* aIsEnabled) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +FooCommand::DoCommand(const char* aCommandName, + const nsAString& aCommandParams, + nsISupports* aCommandRefCon) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} +#endif + +} // namespace mozilla + +#endif // #ifndef EditorCommands_h_ diff --git a/editor/libeditor/EditorController.cpp b/editor/libeditor/EditorController.cpp new file mode 100644 index 000000000..b9e499978 --- /dev/null +++ b/editor/libeditor/EditorController.cpp @@ -0,0 +1,146 @@ +/* -*- 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/EditorController.h" + +#include "EditorCommands.h" +#include "mozilla/mozalloc.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIControllerCommandTable.h" + +class nsIControllerCommand; + +namespace mozilla { + +#define NS_REGISTER_ONE_COMMAND(_cmdClass, _cmdName) \ + { \ + _cmdClass* theCmd = new _cmdClass(); \ + NS_ENSURE_TRUE(theCmd, NS_ERROR_OUT_OF_MEMORY); \ + aCommandTable->RegisterCommand( \ + _cmdName, \ + static_cast<nsIControllerCommand *>(theCmd)); \ + } + +#define NS_REGISTER_FIRST_COMMAND(_cmdClass, _cmdName) \ + { \ + _cmdClass* theCmd = new _cmdClass(); \ + NS_ENSURE_TRUE(theCmd, NS_ERROR_OUT_OF_MEMORY); \ + aCommandTable->RegisterCommand( \ + _cmdName, \ + static_cast<nsIControllerCommand *>(theCmd)); + +#define NS_REGISTER_NEXT_COMMAND(_cmdClass, _cmdName) \ + aCommandTable->RegisterCommand( \ + _cmdName, \ + static_cast<nsIControllerCommand *>(theCmd)); + +#define NS_REGISTER_LAST_COMMAND(_cmdClass, _cmdName) \ + aCommandTable->RegisterCommand( \ + _cmdName, \ + static_cast<nsIControllerCommand *>(theCmd)); \ + } + +// static +nsresult +EditorController::RegisterEditingCommands( + nsIControllerCommandTable* aCommandTable) +{ + // now register all our commands + // These are commands that will be used in text widgets, and in composer + + NS_REGISTER_ONE_COMMAND(UndoCommand, "cmd_undo"); + NS_REGISTER_ONE_COMMAND(RedoCommand, "cmd_redo"); + NS_REGISTER_ONE_COMMAND(ClearUndoCommand, "cmd_clearUndo"); + + NS_REGISTER_ONE_COMMAND(CutCommand, "cmd_cut"); + NS_REGISTER_ONE_COMMAND(CutOrDeleteCommand, "cmd_cutOrDelete"); + NS_REGISTER_ONE_COMMAND(CopyCommand, "cmd_copy"); + NS_REGISTER_ONE_COMMAND(CopyOrDeleteCommand, "cmd_copyOrDelete"); + NS_REGISTER_ONE_COMMAND(CopyAndCollapseToEndCommand, + "cmd_copyAndCollapseToEnd"); + NS_REGISTER_ONE_COMMAND(SelectAllCommand, "cmd_selectAll"); + + NS_REGISTER_ONE_COMMAND(PasteCommand, "cmd_paste"); + NS_REGISTER_ONE_COMMAND(PasteTransferableCommand, "cmd_pasteTransferable"); + + NS_REGISTER_ONE_COMMAND(SwitchTextDirectionCommand, + "cmd_switchTextDirection"); + + NS_REGISTER_FIRST_COMMAND(DeleteCommand, "cmd_delete"); + NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteCharBackward"); + NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteCharForward"); + NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteWordBackward"); + NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteWordForward"); + NS_REGISTER_NEXT_COMMAND(DeleteCommand, "cmd_deleteToBeginningOfLine"); + NS_REGISTER_LAST_COMMAND(DeleteCommand, "cmd_deleteToEndOfLine"); + + // Insert content + NS_REGISTER_ONE_COMMAND(InsertPlaintextCommand, "cmd_insertText"); + NS_REGISTER_ONE_COMMAND(InsertParagraphCommand, "cmd_insertParagraph"); + NS_REGISTER_ONE_COMMAND(InsertLineBreakCommand, "cmd_insertLineBreak"); + NS_REGISTER_ONE_COMMAND(PasteQuotationCommand, "cmd_pasteQuote"); + + return NS_OK; +} + +// static +nsresult +EditorController::RegisterEditorCommands( + nsIControllerCommandTable* aCommandTable) +{ + // These are commands that will be used in text widgets only. + + NS_REGISTER_FIRST_COMMAND(SelectionMoveCommands, "cmd_scrollTop"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollBottom"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveTop"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveBottom"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectTop"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectBottom"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_lineNext"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_linePrevious"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectLineNext"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectLinePrevious"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_charPrevious"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_charNext"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectCharPrevious"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectCharNext"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_beginLine"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_endLine"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectBeginLine"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectEndLine"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_wordPrevious"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_wordNext"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectWordPrevious"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectWordNext"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollPageUp"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollPageDown"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollLineUp"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_scrollLineDown"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_movePageUp"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_movePageDown"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectPageUp"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectPageDown"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveLeft"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveRight"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveUp"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveDown"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveLeft2"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveRight2"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveUp2"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_moveDown2"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectLeft"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectRight"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectUp"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectDown"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectLeft2"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectRight2"); + NS_REGISTER_NEXT_COMMAND(SelectionMoveCommands, "cmd_selectUp2"); + NS_REGISTER_LAST_COMMAND(SelectionMoveCommands, "cmd_selectDown2"); + + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/EditorController.h b/editor/libeditor/EditorController.h new file mode 100644 index 000000000..e9fb654b8 --- /dev/null +++ b/editor/libeditor/EditorController.h @@ -0,0 +1,37 @@ +/* -*- 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/. */ + +#ifndef mozilla_EditorController_h +#define mozilla_EditorController_h + +#include "nscore.h" + +#define NS_EDITORCONTROLLER_CID \ +{ 0x26fb965c, 0x9de6, 0x11d3, \ + { 0xbc, 0xcc, 0x0, 0x60, 0xb0, 0xfc, 0x76, 0xbd } } + +#define NS_EDITINGCONTROLLER_CID \ +{ 0x2c5a5cdd, 0xe742, 0x4dfe, \ + { 0x86, 0xb8, 0x06, 0x93, 0x09, 0xbf, 0x6c, 0x91 } } + +class nsIControllerCommandTable; + +namespace mozilla { + +// the editor controller is used for both text widgets, and basic text editing +// commands in composer. The refCon that gets passed to its commands is an nsIEditor. + +class EditorController final +{ +public: + static nsresult RegisterEditorCommands( + nsIControllerCommandTable* aCommandTable); + static nsresult RegisterEditingCommands( + nsIControllerCommandTable* aCommandTable); +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_EditorController_h diff --git a/editor/libeditor/EditorEventListener.cpp b/editor/libeditor/EditorEventListener.cpp new file mode 100644 index 000000000..f90458d3e --- /dev/null +++ b/editor/libeditor/EditorEventListener.cpp @@ -0,0 +1,1215 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 sw=2 et tw=78: */ +/* 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 "EditorEventListener.h" + +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc. +#include "mozilla/EditorBase.h" // for EditorBase, etc. +#include "mozilla/EventListenerManager.h" // for EventListenerManager +#include "mozilla/IMEStateManager.h" // for IMEStateManager +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/TextEvents.h" // for WidgetCompositionEvent +#include "mozilla/dom/Element.h" // for Element +#include "mozilla/dom/Event.h" // for Event +#include "mozilla/dom/EventTarget.h" // for EventTarget +#include "mozilla/dom/Selection.h" +#include "nsAString.h" +#include "nsCaret.h" // for nsCaret +#include "nsDebug.h" // for NS_ENSURE_TRUE, etc. +#include "nsFocusManager.h" // for nsFocusManager +#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::input +#include "nsIClipboard.h" // for nsIClipboard, etc. +#include "nsIContent.h" // for nsIContent +#include "nsIController.h" // for nsIController +#include "nsID.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/DataTransfer.h" +#include "nsIDOMDocument.h" // for nsIDOMDocument +#include "nsIDOMDragEvent.h" // for nsIDOMDragEvent +#include "nsIDOMElement.h" // for nsIDOMElement +#include "nsIDOMEvent.h" // for nsIDOMEvent +#include "nsIDOMEventTarget.h" // for nsIDOMEventTarget +#include "nsIDOMKeyEvent.h" // for nsIDOMKeyEvent +#include "nsIDOMMouseEvent.h" // for nsIDOMMouseEvent +#include "nsIDOMNode.h" // for nsIDOMNode +#include "nsIDocument.h" // for nsIDocument +#include "nsIEditor.h" // for EditorBase::GetSelection, etc. +#include "nsIEditorIMESupport.h" +#include "nsIEditorMailSupport.h" // for nsIEditorMailSupport +#include "nsIFocusManager.h" // for nsIFocusManager +#include "nsIFormControl.h" // for nsIFormControl, etc. +#include "nsIHTMLEditor.h" // for nsIHTMLEditor +#include "nsINode.h" // for nsINode, ::NODE_IS_EDITABLE, etc. +#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc. +#include "nsIPresShell.h" // for nsIPresShell +#include "nsISelectionController.h" // for nsISelectionController, etc. +#include "nsITransferable.h" // for kFileMime, kHTMLMime, etc. +#include "nsIWidget.h" // for nsIWidget +#include "nsLiteralString.h" // for NS_LITERAL_STRING +#include "nsPIWindowRoot.h" // for nsPIWindowRoot +#include "nsPrintfCString.h" // for nsPrintfCString +#include "nsRange.h" +#include "nsServiceManagerUtils.h" // for do_GetService +#include "nsString.h" // for nsAutoString +#include "nsQueryObject.h" // for do_QueryObject +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH +#include "nsContentUtils.h" // for nsContentUtils, etc. +#include "nsIBidiKeyboard.h" // for nsIBidiKeyboard +#endif + +#include "mozilla/dom/TabParent.h" + +class nsPresContext; + +namespace mozilla { + +using namespace dom; + +static void +DoCommandCallback(Command aCommand, void* aData) +{ + nsIDocument* doc = static_cast<nsIDocument*>(aData); + nsPIDOMWindowOuter* win = doc->GetWindow(); + if (!win) { + return; + } + nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot(); + if (!root) { + return; + } + + const char* commandStr = WidgetKeyboardEvent::GetCommandStr(aCommand); + + nsCOMPtr<nsIController> controller; + root->GetControllerForCommand(commandStr, getter_AddRefs(controller)); + if (!controller) { + return; + } + + bool commandEnabled; + nsresult rv = controller->IsCommandEnabled(commandStr, &commandEnabled); + NS_ENSURE_SUCCESS_VOID(rv); + if (commandEnabled) { + controller->DoCommand(commandStr); + } +} + +EditorEventListener::EditorEventListener() + : mEditorBase(nullptr) + , mCommitText(false) + , mInTransaction(false) + , mMouseDownOrUpConsumedByIME(false) +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + , mHaveBidiKeyboards(false) + , mShouldSwitchTextDirection(false) + , mSwitchToRTL(false) +#endif +{ +} + +EditorEventListener::~EditorEventListener() +{ + if (mEditorBase) { + NS_WARNING("We're not uninstalled"); + Disconnect(); + } +} + +nsresult +EditorEventListener::Connect(EditorBase* aEditorBase) +{ + NS_ENSURE_ARG(aEditorBase); + +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard(); + if (bidiKeyboard) { + bool haveBidiKeyboards = false; + bidiKeyboard->GetHaveBidiKeyboards(&haveBidiKeyboards); + mHaveBidiKeyboards = haveBidiKeyboards; + } +#endif + + mEditorBase = aEditorBase; + + nsresult rv = InstallToEditor(); + if (NS_FAILED(rv)) { + Disconnect(); + } + return rv; +} + +nsresult +EditorEventListener::InstallToEditor() +{ + NS_PRECONDITION(mEditorBase, "The caller must set mEditorBase"); + + nsCOMPtr<EventTarget> piTarget = mEditorBase->GetDOMEventTarget(); + NS_ENSURE_TRUE(piTarget, NS_ERROR_FAILURE); + + // register the event listeners with the listener manager + EventListenerManager* elmP = piTarget->GetOrCreateListenerManager(); + NS_ENSURE_STATE(elmP); + +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("keydown"), + TrustedEventsAtSystemGroupBubble()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("keyup"), + TrustedEventsAtSystemGroupBubble()); +#endif + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("keypress"), + TrustedEventsAtSystemGroupBubble()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("dragenter"), + TrustedEventsAtSystemGroupBubble()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("dragover"), + TrustedEventsAtSystemGroupBubble()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("dragexit"), + TrustedEventsAtSystemGroupBubble()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("drop"), + TrustedEventsAtSystemGroupBubble()); + // XXX We should add the mouse event listeners as system event group. + // E.g., web applications cannot prevent middle mouse paste by + // preventDefault() of click event at bubble phase. + // However, if we do so, all click handlers in any frames and frontend + // code need to check if it's editable. It makes easier create new bugs. + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("mousedown"), + TrustedEventsAtCapture()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("mouseup"), + TrustedEventsAtCapture()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("click"), + TrustedEventsAtCapture()); +// Focus event doesn't bubble so adding the listener to capturing phase. +// Make sure this works after bug 235441 gets fixed. + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("blur"), + TrustedEventsAtCapture()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("focus"), + TrustedEventsAtCapture()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("text"), + TrustedEventsAtSystemGroupBubble()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("compositionstart"), + TrustedEventsAtSystemGroupBubble()); + elmP->AddEventListenerByType(this, + NS_LITERAL_STRING("compositionend"), + TrustedEventsAtSystemGroupBubble()); + + return NS_OK; +} + +void +EditorEventListener::Disconnect() +{ + if (!mEditorBase) { + return; + } + UninstallFromEditor(); + + nsIFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm) { + nsCOMPtr<nsIDOMElement> domFocus; + fm->GetFocusedElement(getter_AddRefs(domFocus)); + nsCOMPtr<nsINode> focusedElement = do_QueryInterface(domFocus); + mozilla::dom::Element* root = mEditorBase->GetRoot(); + if (focusedElement && root && + nsContentUtils::ContentIsDescendantOf(focusedElement, root)) { + // Reset the Selection ancestor limiter and SelectionController state + // that EditorBase::InitializeSelection set up. + mEditorBase->FinalizeSelection(); + } + } + + mEditorBase = nullptr; +} + +void +EditorEventListener::UninstallFromEditor() +{ + nsCOMPtr<EventTarget> piTarget = mEditorBase->GetDOMEventTarget(); + if (!piTarget) { + return; + } + + EventListenerManager* elmP = piTarget->GetOrCreateListenerManager(); + if (!elmP) { + return; + } + +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("keydown"), + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("keyup"), + TrustedEventsAtSystemGroupBubble()); +#endif + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("keypress"), + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("dragenter"), + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("dragover"), + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("dragexit"), + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("drop"), + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("mousedown"), + TrustedEventsAtCapture()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("mouseup"), + TrustedEventsAtCapture()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("click"), + TrustedEventsAtCapture()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("blur"), + TrustedEventsAtCapture()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("focus"), + TrustedEventsAtCapture()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("text"), + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("compositionstart"), + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, + NS_LITERAL_STRING("compositionend"), + TrustedEventsAtSystemGroupBubble()); +} + +already_AddRefed<nsIPresShell> +EditorEventListener::GetPresShell() +{ + NS_PRECONDITION(mEditorBase, + "The caller must check whether this is connected to an editor"); + return mEditorBase->GetPresShell(); +} + +nsPresContext* +EditorEventListener::GetPresContext() +{ + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + return presShell ? presShell->GetPresContext() : nullptr; +} + +nsIContent* +EditorEventListener::GetFocusedRootContent() +{ + NS_ENSURE_TRUE(mEditorBase, nullptr); + + nsCOMPtr<nsIContent> focusedContent = mEditorBase->GetFocusedContent(); + if (!focusedContent) { + return nullptr; + } + + nsIDocument* composedDoc = focusedContent->GetComposedDoc(); + NS_ENSURE_TRUE(composedDoc, nullptr); + + if (composedDoc->HasFlag(NODE_IS_EDITABLE)) { + return nullptr; + } + + return focusedContent; +} + +bool +EditorEventListener::EditorHasFocus() +{ + NS_PRECONDITION(mEditorBase, + "The caller must check whether this is connected to an editor"); + nsCOMPtr<nsIContent> focusedContent = mEditorBase->GetFocusedContent(); + if (!focusedContent) { + return false; + } + nsIDocument* composedDoc = focusedContent->GetComposedDoc(); + return !!composedDoc; +} + +NS_IMPL_ISUPPORTS(EditorEventListener, nsIDOMEventListener) + +NS_IMETHODIMP +EditorEventListener::HandleEvent(nsIDOMEvent* aEvent) +{ + NS_ENSURE_TRUE(mEditorBase, NS_ERROR_FAILURE); + + nsCOMPtr<nsIEditor> kungFuDeathGrip = mEditorBase; + Unused << kungFuDeathGrip; // mEditorBase is not referred to in this function + + WidgetEvent* internalEvent = aEvent->WidgetEventPtr(); + + // Let's handle each event with the message of the internal event of the + // coming event. If the DOM event was created with improper interface, + // e.g., keydown event is created with |new MouseEvent("keydown", {});|, + // its message is always 0. Therefore, we can ban such strange event easy. + // However, we need to handle strange "focus" and "blur" event. See the + // following code of this switch statement. + // NOTE: Each event handler may require specific event interface. Before + // calling it, this queries the specific interface. If it would fail, + // each event handler would just ignore the event. So, in this method, + // you don't need to check if the QI succeeded before each call. + switch (internalEvent->mMessage) { + // dragenter + case eDragEnter: { + nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent); + return DragEnter(dragEvent); + } + // dragover + case eDragOver: { + nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent); + return DragOver(dragEvent); + } + // dragexit + case eDragExit: { + nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent); + return DragExit(dragEvent); + } + // drop + case eDrop: { + nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent); + return Drop(dragEvent); + } +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + // keydown + case eKeyDown: { + nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent); + return KeyDown(keyEvent); + } + // keyup + case eKeyUp: { + nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent); + return KeyUp(keyEvent); + } +#endif // #ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + // keypress + case eKeyPress: { + nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent); + return KeyPress(keyEvent); + } + // mousedown + case eMouseDown: { + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent); + NS_ENSURE_TRUE(mouseEvent, NS_OK); + // EditorEventListener may receive (1) all mousedown, mouseup and click + // events, (2) only mousedown event or (3) only mouseup event. + // mMouseDownOrUpConsumedByIME is used only for ignoring click event if + // preceding mousedown and/or mouseup event is consumed by IME. + // Therefore, even if case #2 or case #3 occurs, + // mMouseDownOrUpConsumedByIME is true here. Therefore, we should always + // overwrite it here. + mMouseDownOrUpConsumedByIME = NotifyIMEOfMouseButtonEvent(mouseEvent); + return mMouseDownOrUpConsumedByIME ? NS_OK : MouseDown(mouseEvent); + } + // mouseup + case eMouseUp: { + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent); + NS_ENSURE_TRUE(mouseEvent, NS_OK); + // See above comment in the eMouseDown case, first. + // This code assumes that case #1 is occuring. However, if case #3 may + // occurs after case #2 and the mousedown is consumed, + // mMouseDownOrUpConsumedByIME is true even though EditorEventListener + // has not received the preceding mousedown event of this mouseup event. + // So, mMouseDownOrUpConsumedByIME may be invalid here. However, + // this is not a matter because mMouseDownOrUpConsumedByIME is referred + // only by eMouseClick case but click event is fired only in case #1. + // So, before a click event is fired, mMouseDownOrUpConsumedByIME is + // always initialized in the eMouseDown case if it's referred. + if (NotifyIMEOfMouseButtonEvent(mouseEvent)) { + mMouseDownOrUpConsumedByIME = true; + } + return mMouseDownOrUpConsumedByIME ? NS_OK : MouseUp(mouseEvent); + } + // click + case eMouseClick: { + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent); + NS_ENSURE_TRUE(mouseEvent, NS_OK); + // If the preceding mousedown event or mouseup event was consumed, + // editor shouldn't handle this click event. + if (mMouseDownOrUpConsumedByIME) { + mMouseDownOrUpConsumedByIME = false; + mouseEvent->AsEvent()->PreventDefault(); + return NS_OK; + } + return MouseClick(mouseEvent); + } + // focus + case eFocus: + return Focus(aEvent); + // blur + case eBlur: + return Blur(aEvent); + // text + case eCompositionChange: + return HandleText(aEvent); + // compositionstart + case eCompositionStart: + return HandleStartComposition(aEvent); + // compositionend + case eCompositionEnd: + HandleEndComposition(aEvent); + return NS_OK; + default: + break; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + // We should accept "focus" and "blur" event even if it's synthesized with + // wrong interface for compatibility with older Gecko. + if (eventType.EqualsLiteral("focus")) { + return Focus(aEvent); + } + if (eventType.EqualsLiteral("blur")) { + return Blur(aEvent); + } +#ifdef DEBUG + nsPrintfCString assertMessage("Editor doesn't handle \"%s\" event " + "because its internal event doesn't have proper message", + NS_ConvertUTF16toUTF8(eventType).get()); + NS_ASSERTION(false, assertMessage.get()); +#endif + + return NS_OK; +} + +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH +namespace { + +// This function is borrowed from Chromium's ImeInput::IsCtrlShiftPressed +bool IsCtrlShiftPressed(nsIDOMKeyEvent* aEvent, bool& isRTL) +{ + // To check if a user is pressing only a control key and a right-shift key + // (or a left-shift key), we use the steps below: + // 1. Check if a user is pressing a control key and a right-shift key (or + // a left-shift key). + // 2. If the condition 1 is true, we should check if there are any other + // keys pressed at the same time. + // To ignore the keys checked in 1, we set their status to 0 before + // checking the key status. + WidgetKeyboardEvent* keyboardEvent = + aEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(keyboardEvent, + "DOM key event's internal event must be WidgetKeyboardEvent"); + + if (!keyboardEvent->IsControl()) { + return false; + } + + uint32_t location = keyboardEvent->mLocation; + if (location == nsIDOMKeyEvent::DOM_KEY_LOCATION_RIGHT) { + isRTL = true; + } else if (location == nsIDOMKeyEvent::DOM_KEY_LOCATION_LEFT) { + isRTL = false; + } else { + return false; + } + + // Scan the key status to find pressed keys. We should abandon changing the + // text direction when there are other pressed keys. + if (keyboardEvent->IsAlt() || keyboardEvent->IsOS()) { + return false; + } + + return true; +} + +} + +// This logic is mostly borrowed from Chromium's +// RenderWidgetHostViewWin::OnKeyEvent. + +nsresult +EditorEventListener::KeyUp(nsIDOMKeyEvent* aKeyEvent) +{ + NS_ENSURE_TRUE(aKeyEvent, NS_OK); + + if (!mHaveBidiKeyboards) { + return NS_OK; + } + + uint32_t keyCode = 0; + aKeyEvent->GetKeyCode(&keyCode); + if ((keyCode == nsIDOMKeyEvent::DOM_VK_SHIFT || + keyCode == nsIDOMKeyEvent::DOM_VK_CONTROL) && + mShouldSwitchTextDirection && mEditorBase->IsPlaintextEditor()) { + mEditorBase->SwitchTextDirectionTo(mSwitchToRTL ? + nsIPlaintextEditor::eEditorRightToLeft : + nsIPlaintextEditor::eEditorLeftToRight); + mShouldSwitchTextDirection = false; + } + return NS_OK; +} + +nsresult +EditorEventListener::KeyDown(nsIDOMKeyEvent* aKeyEvent) +{ + NS_ENSURE_TRUE(aKeyEvent, NS_OK); + + if (!mHaveBidiKeyboards) { + return NS_OK; + } + + uint32_t keyCode = 0; + aKeyEvent->GetKeyCode(&keyCode); + if (keyCode == nsIDOMKeyEvent::DOM_VK_SHIFT) { + bool switchToRTL; + if (IsCtrlShiftPressed(aKeyEvent, switchToRTL)) { + mShouldSwitchTextDirection = true; + mSwitchToRTL = switchToRTL; + } + } else if (keyCode != nsIDOMKeyEvent::DOM_VK_CONTROL) { + // In case the user presses any other key besides Ctrl and Shift + mShouldSwitchTextDirection = false; + } + return NS_OK; +} +#endif + +nsresult +EditorEventListener::KeyPress(nsIDOMKeyEvent* aKeyEvent) +{ + NS_ENSURE_TRUE(aKeyEvent, NS_OK); + + if (!mEditorBase->IsAcceptableInputEvent(aKeyEvent->AsEvent())) { + return NS_OK; + } + + // DOM event handling happens in two passes, the client pass and the system + // pass. We do all of our processing in the system pass, to allow client + // handlers the opportunity to cancel events and prevent typing in the editor. + // If the client pass cancelled the event, defaultPrevented will be true + // below. + + bool defaultPrevented; + aKeyEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented); + if (defaultPrevented) { + return NS_OK; + } + + nsresult rv = mEditorBase->HandleKeyPressEvent(aKeyEvent); + NS_ENSURE_SUCCESS(rv, rv); + + aKeyEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented); + if (defaultPrevented) { + return NS_OK; + } + + if (!ShouldHandleNativeKeyBindings(aKeyEvent)) { + return NS_OK; + } + + // Now, ask the native key bindings to handle the event. + WidgetKeyboardEvent* keyEvent = + aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(keyEvent, + "DOM key event's internal event must be WidgetKeyboardEvent"); + nsIWidget* widget = keyEvent->mWidget; + // If the event is created by chrome script, the widget is always nullptr. + if (!widget) { + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + nsPresContext* pc = ps ? ps->GetPresContext() : nullptr; + widget = pc ? pc->GetNearestWidget() : nullptr; + NS_ENSURE_TRUE(widget, NS_OK); + } + + nsCOMPtr<nsIDocument> doc = mEditorBase->GetDocument(); + bool handled = widget->ExecuteNativeKeyBinding( + nsIWidget::NativeKeyBindingsForRichTextEditor, + *keyEvent, DoCommandCallback, doc); + if (handled) { + aKeyEvent->AsEvent()->PreventDefault(); + } + return NS_OK; +} + +nsresult +EditorEventListener::MouseClick(nsIDOMMouseEvent* aMouseEvent) +{ + // nothing to do if editor isn't editable or clicked on out of the editor. + if (mEditorBase->IsReadonly() || mEditorBase->IsDisabled() || + !mEditorBase->IsAcceptableInputEvent(aMouseEvent->AsEvent())) { + return NS_OK; + } + + // Notifies clicking on editor to IMEStateManager even when the event was + // consumed. + if (EditorHasFocus()) { + nsPresContext* presContext = GetPresContext(); + if (presContext) { + IMEStateManager::OnClickInEditor(presContext, GetFocusedRootContent(), + aMouseEvent); + } + } + + bool preventDefault; + nsresult rv = aMouseEvent->AsEvent()->GetDefaultPrevented(&preventDefault); + if (NS_FAILED(rv) || preventDefault) { + // We're done if 'preventdefault' is true (see for example bug 70698). + return rv; + } + + // IMEStateManager::OnClickInEditor() may cause anything because it may + // set input context. For example, it may cause opening VKB, changing focus + // or reflow. So, mEditorBase here might have been gone. + if (!mEditorBase) { + return NS_OK; + } + + // If we got a mouse down inside the editing area, we should force the + // IME to commit before we change the cursor position + mEditorBase->ForceCompositionEnd(); + + int16_t button = -1; + aMouseEvent->GetButton(&button); + if (button == 1) { + return HandleMiddleClickPaste(aMouseEvent); + } + return NS_OK; +} + +nsresult +EditorEventListener::HandleMiddleClickPaste(nsIDOMMouseEvent* aMouseEvent) +{ + if (!Preferences::GetBool("middlemouse.paste", false)) { + // Middle click paste isn't enabled. + return NS_OK; + } + + // Set the selection to the point under the mouse cursor: + nsCOMPtr<nsIDOMNode> parent; + if (NS_FAILED(aMouseEvent->GetRangeParent(getter_AddRefs(parent)))) { + return NS_ERROR_NULL_POINTER; + } + int32_t offset = 0; + if (NS_FAILED(aMouseEvent->GetRangeOffset(&offset))) { + return NS_ERROR_NULL_POINTER; + } + + RefPtr<Selection> selection = mEditorBase->GetSelection(); + if (selection) { + selection->Collapse(parent, offset); + } + + // If the ctrl key is pressed, we'll do paste as quotation. + // Would've used the alt key, but the kde wmgr treats alt-middle specially. + bool ctrlKey = false; + aMouseEvent->GetCtrlKey(&ctrlKey); + + nsCOMPtr<nsIEditorMailSupport> mailEditor; + if (ctrlKey) { + mailEditor = do_QueryObject(mEditorBase); + } + + nsresult rv; + int32_t clipboard = nsIClipboard::kGlobalClipboard; + nsCOMPtr<nsIClipboard> clipboardService = + do_GetService("@mozilla.org/widget/clipboard;1", &rv); + if (NS_SUCCEEDED(rv)) { + bool selectionSupported; + rv = clipboardService->SupportsSelectionClipboard(&selectionSupported); + if (NS_SUCCEEDED(rv) && selectionSupported) { + clipboard = nsIClipboard::kSelectionClipboard; + } + } + + if (mailEditor) { + mailEditor->PasteAsQuotation(clipboard); + } else { + mEditorBase->Paste(clipboard); + } + + // Prevent the event from propagating up to be possibly handled + // again by the containing window: + aMouseEvent->AsEvent()->StopPropagation(); + aMouseEvent->AsEvent()->PreventDefault(); + + // We processed the event, whether drop/paste succeeded or not + return NS_OK; +} + +bool +EditorEventListener::NotifyIMEOfMouseButtonEvent( + nsIDOMMouseEvent* aMouseEvent) +{ + if (!EditorHasFocus()) { + return false; + } + + bool defaultPrevented; + nsresult rv = aMouseEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented); + NS_ENSURE_SUCCESS(rv, false); + if (defaultPrevented) { + return false; + } + nsPresContext* presContext = GetPresContext(); + NS_ENSURE_TRUE(presContext, false); + return IMEStateManager::OnMouseButtonEventInEditor(presContext, + GetFocusedRootContent(), + aMouseEvent); +} + +nsresult +EditorEventListener::MouseDown(nsIDOMMouseEvent* aMouseEvent) +{ + // FYI: This may be called by HTMLEditorEventListener::MouseDown() even + // when the event is not acceptable for committing composition. + if (mEditorBase) { + mEditorBase->ForceCompositionEnd(); + } + return NS_OK; +} + +nsresult +EditorEventListener::HandleText(nsIDOMEvent* aTextEvent) +{ + if (!mEditorBase->IsAcceptableInputEvent(aTextEvent)) { + return NS_OK; + } + + // if we are readonly or disabled, then do nothing. + if (mEditorBase->IsReadonly() || mEditorBase->IsDisabled()) { + return NS_OK; + } + + return mEditorBase->UpdateIMEComposition(aTextEvent); +} + +/** + * Drag event implementation + */ + +nsresult +EditorEventListener::DragEnter(nsIDOMDragEvent* aDragEvent) +{ + NS_ENSURE_TRUE(aDragEvent, NS_OK); + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_OK); + + if (!mCaret) { + mCaret = new nsCaret(); + mCaret->Init(presShell); + mCaret->SetCaretReadOnly(true); + // This is to avoid the requirement that the Selection is Collapsed which + // it can't be when dragging a selection in the same shell. + // See nsCaret::IsVisible(). + mCaret->SetVisibilityDuringSelection(true); + } + + presShell->SetCaret(mCaret); + + return DragOver(aDragEvent); +} + +nsresult +EditorEventListener::DragOver(nsIDOMDragEvent* aDragEvent) +{ + NS_ENSURE_TRUE(aDragEvent, NS_OK); + + nsCOMPtr<nsIDOMNode> parent; + bool defaultPrevented; + aDragEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented); + if (defaultPrevented) { + return NS_OK; + } + + aDragEvent->GetRangeParent(getter_AddRefs(parent)); + nsCOMPtr<nsIContent> dropParent = do_QueryInterface(parent); + NS_ENSURE_TRUE(dropParent, NS_ERROR_FAILURE); + + if (dropParent->IsEditable() && CanDrop(aDragEvent)) { + aDragEvent->AsEvent()->PreventDefault(); // consumed + + if (!mCaret) { + return NS_OK; + } + + int32_t offset = 0; + nsresult rv = aDragEvent->GetRangeOffset(&offset); + NS_ENSURE_SUCCESS(rv, rv); + + mCaret->SetVisible(true); + mCaret->SetCaretPosition(parent, offset); + + return NS_OK; + } + + if (!IsFileControlTextBox()) { + // This is needed when dropping on an input, to prevent the editor for + // the editable parent from receiving the event. + aDragEvent->AsEvent()->StopPropagation(); + } + + if (mCaret) { + mCaret->SetVisible(false); + } + return NS_OK; +} + +void +EditorEventListener::CleanupDragDropCaret() +{ + if (!mCaret) { + return; + } + + mCaret->SetVisible(false); // hide it, so that it turns off its timer + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + if (presShell) { + presShell->RestoreCaret(); + } + + mCaret->Terminate(); + mCaret = nullptr; +} + +nsresult +EditorEventListener::DragExit(nsIDOMDragEvent* aDragEvent) +{ + NS_ENSURE_TRUE(aDragEvent, NS_OK); + + CleanupDragDropCaret(); + + return NS_OK; +} + +nsresult +EditorEventListener::Drop(nsIDOMDragEvent* aDragEvent) +{ + NS_ENSURE_TRUE(aDragEvent, NS_OK); + + CleanupDragDropCaret(); + + bool defaultPrevented; + aDragEvent->AsEvent()->GetDefaultPrevented(&defaultPrevented); + if (defaultPrevented) { + return NS_OK; + } + + nsCOMPtr<nsIDOMNode> parent; + aDragEvent->GetRangeParent(getter_AddRefs(parent)); + nsCOMPtr<nsIContent> dropParent = do_QueryInterface(parent); + NS_ENSURE_TRUE(dropParent, NS_ERROR_FAILURE); + + if (!dropParent->IsEditable() || !CanDrop(aDragEvent)) { + // was it because we're read-only? + if ((mEditorBase->IsReadonly() || mEditorBase->IsDisabled()) && + !IsFileControlTextBox()) { + // it was decided to "eat" the event as this is the "least surprise" + // since someone else handling it might be unintentional and the + // user could probably re-drag to be not over the disabled/readonly + // editfields if that is what is desired. + return aDragEvent->AsEvent()->StopPropagation(); + } + return NS_OK; + } + + aDragEvent->AsEvent()->StopPropagation(); + aDragEvent->AsEvent()->PreventDefault(); + return mEditorBase->InsertFromDrop(aDragEvent->AsEvent()); +} + +bool +EditorEventListener::CanDrop(nsIDOMDragEvent* aEvent) +{ + // if the target doc is read-only, we can't drop + if (mEditorBase->IsReadonly() || mEditorBase->IsDisabled()) { + return false; + } + + nsCOMPtr<nsIDOMDataTransfer> domDataTransfer; + aEvent->GetDataTransfer(getter_AddRefs(domDataTransfer)); + nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(domDataTransfer); + NS_ENSURE_TRUE(dataTransfer, false); + + nsTArray<nsString> types; + dataTransfer->GetTypes(types, *nsContentUtils::GetSystemPrincipal()); + + // Plaintext editors only support dropping text. Otherwise, HTML and files + // can be dropped as well. + if (!types.Contains(NS_LITERAL_STRING(kTextMime)) && + !types.Contains(NS_LITERAL_STRING(kMozTextInternal)) && + (mEditorBase->IsPlaintextEditor() || + (!types.Contains(NS_LITERAL_STRING(kHTMLMime)) && + !types.Contains(NS_LITERAL_STRING(kFileMime))))) { + return false; + } + + // If there is no source node, this is probably an external drag and the + // drop is allowed. The later checks rely on checking if the drag target + // is the same as the drag source. + nsCOMPtr<nsIDOMNode> sourceNode; + dataTransfer->GetMozSourceNode(getter_AddRefs(sourceNode)); + if (!sourceNode) { + return true; + } + + // There is a source node, so compare the source documents and this document. + // Disallow drops on the same document. + + nsCOMPtr<nsIDOMDocument> domdoc = mEditorBase->GetDOMDocument(); + NS_ENSURE_TRUE(domdoc, false); + + nsCOMPtr<nsIDOMDocument> sourceDoc; + nsresult rv = sourceNode->GetOwnerDocument(getter_AddRefs(sourceDoc)); + NS_ENSURE_SUCCESS(rv, false); + + // If the source and the dest are not same document, allow to drop it always. + if (domdoc != sourceDoc) { + return true; + } + + // If the source node is a remote browser, treat this as coming from a + // different document and allow the drop. + nsCOMPtr<nsIContent> sourceContent = do_QueryInterface(sourceNode); + TabParent* tp = TabParent::GetFrom(sourceContent); + if (tp) { + return true; + } + + RefPtr<Selection> selection = mEditorBase->GetSelection(); + if (!selection) { + return false; + } + + // If selection is collapsed, allow to drop it always. + if (selection->Collapsed()) { + return true; + } + + nsCOMPtr<nsIDOMNode> parent; + rv = aEvent->GetRangeParent(getter_AddRefs(parent)); + if (NS_FAILED(rv) || !parent) { + return false; + } + + int32_t offset = 0; + rv = aEvent->GetRangeOffset(&offset); + NS_ENSURE_SUCCESS(rv, false); + + int32_t rangeCount; + rv = selection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, false); + + for (int32_t i = 0; i < rangeCount; i++) { + RefPtr<nsRange> range = selection->GetRangeAt(i); + if (!range) { + // Don't bail yet, iterate through them all + continue; + } + + bool inRange = true; + range->IsPointInRange(parent, offset, &inRange); + if (inRange) { + // Okay, now you can bail, we are over the orginal selection + return false; + } + } + return true; +} + +nsresult +EditorEventListener::HandleStartComposition(nsIDOMEvent* aCompositionEvent) +{ + if (!mEditorBase->IsAcceptableInputEvent(aCompositionEvent)) { + return NS_OK; + } + WidgetCompositionEvent* compositionStart = + aCompositionEvent->WidgetEventPtr()->AsCompositionEvent(); + return mEditorBase->BeginIMEComposition(compositionStart); +} + +void +EditorEventListener::HandleEndComposition(nsIDOMEvent* aCompositionEvent) +{ + if (!mEditorBase->IsAcceptableInputEvent(aCompositionEvent)) { + return; + } + + mEditorBase->EndIMEComposition(); +} + +nsresult +EditorEventListener::Focus(nsIDOMEvent* aEvent) +{ + NS_ENSURE_TRUE(aEvent, NS_OK); + + // Don't turn on selection and caret when the editor is disabled. + if (mEditorBase->IsDisabled()) { + return NS_OK; + } + + // Spell check a textarea the first time that it is focused. + SpellCheckIfNeeded(); + if (!mEditorBase) { + // In e10s, this can cause us to flush notifications, which can destroy + // the node we're about to focus. + return NS_OK; + } + + nsCOMPtr<nsIDOMEventTarget> target; + aEvent->GetTarget(getter_AddRefs(target)); + nsCOMPtr<nsINode> node = do_QueryInterface(target); + NS_ENSURE_TRUE(node, NS_ERROR_UNEXPECTED); + + // If the target is a document node but it's not editable, we should ignore + // it because actual focused element's event is going to come. + if (node->IsNodeOfType(nsINode::eDOCUMENT) && + !node->HasFlag(NODE_IS_EDITABLE)) { + return NS_OK; + } + + if (node->IsNodeOfType(nsINode::eCONTENT)) { + // XXX If the focus event target is a form control in contenteditable + // element, perhaps, the parent HTML editor should do nothing by this + // handler. However, FindSelectionRoot() returns the root element of the + // contenteditable editor. So, the editableRoot value is invalid for + // the plain text editor, and it will be set to the wrong limiter of + // the selection. However, fortunately, actual bugs are not found yet. + nsCOMPtr<nsIContent> editableRoot = mEditorBase->FindSelectionRoot(node); + + // make sure that the element is really focused in case an earlier + // listener in the chain changed the focus. + if (editableRoot) { + nsIFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, NS_OK); + + nsCOMPtr<nsIDOMElement> element; + fm->GetFocusedElement(getter_AddRefs(element)); + if (!element) { + return NS_OK; + } + + nsCOMPtr<nsIDOMEventTarget> originalTarget; + aEvent->GetOriginalTarget(getter_AddRefs(originalTarget)); + + nsCOMPtr<nsIContent> originalTargetAsContent = + do_QueryInterface(originalTarget); + nsCOMPtr<nsIContent> focusedElementAsContent = + do_QueryInterface(element); + + if (!SameCOMIdentity( + focusedElementAsContent->FindFirstNonChromeOnlyAccessContent(), + originalTargetAsContent->FindFirstNonChromeOnlyAccessContent())) { + return NS_OK; + } + } + } + + mEditorBase->OnFocus(target); + + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_OK); + nsCOMPtr<nsIContent> focusedContent = mEditorBase->GetFocusedContentForIME(); + IMEStateManager::OnFocusInEditor(ps->GetPresContext(), focusedContent, + mEditorBase); + + return NS_OK; +} + +nsresult +EditorEventListener::Blur(nsIDOMEvent* aEvent) +{ + NS_ENSURE_TRUE(aEvent, NS_OK); + + // check if something else is focused. If another element is focused, then + // we should not change the selection. + nsIFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, NS_OK); + + nsCOMPtr<nsIDOMElement> element; + fm->GetFocusedElement(getter_AddRefs(element)); + if (!element) { + mEditorBase->FinalizeSelection(); + } + return NS_OK; +} + +void +EditorEventListener::SpellCheckIfNeeded() +{ + // If the spell check skip flag is still enabled from creation time, + // disable it because focused editors are allowed to spell check. + uint32_t currentFlags = 0; + mEditorBase->GetFlags(¤tFlags); + if(currentFlags & nsIPlaintextEditor::eEditorSkipSpellCheck) { + currentFlags ^= nsIPlaintextEditor::eEditorSkipSpellCheck; + mEditorBase->SetFlags(currentFlags); + } +} + +bool +EditorEventListener::IsFileControlTextBox() +{ + Element* root = mEditorBase->GetRoot(); + if (!root || !root->ChromeOnlyAccess()) { + return false; + } + nsIContent* parent = root->FindFirstNonChromeOnlyAccessContent(); + if (!parent || !parent->IsHTMLElement(nsGkAtoms::input)) { + return false; + } + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(parent); + return formControl->GetType() == NS_FORM_INPUT_FILE; +} + +bool +EditorEventListener::ShouldHandleNativeKeyBindings(nsIDOMKeyEvent* aKeyEvent) +{ + // Only return true if the target of the event is a desendant of the active + // editing host in order to match the similar decision made in + // nsXBLWindowKeyHandler. + // Note that IsAcceptableInputEvent doesn't check for the active editing + // host for keyboard events, otherwise this check would have been + // unnecessary. IsAcceptableInputEvent currently makes a similar check for + // mouse events. + + nsCOMPtr<nsIDOMEventTarget> target; + aKeyEvent->AsEvent()->GetTarget(getter_AddRefs(target)); + nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target); + if (!targetContent) { + return false; + } + + nsCOMPtr<nsIHTMLEditor> htmlEditor = + do_QueryInterface(static_cast<nsIEditor*>(mEditorBase)); + if (!htmlEditor) { + return false; + } + + nsCOMPtr<nsIDocument> doc = mEditorBase->GetDocument(); + if (doc->HasFlag(NODE_IS_EDITABLE)) { + // Don't need to perform any checks in designMode documents. + return true; + } + + nsIContent* editingHost = htmlEditor->GetActiveEditingHost(); + if (!editingHost) { + return false; + } + + return nsContentUtils::ContentIsDescendantOf(targetContent, editingHost); +} + +} // namespace mozilla diff --git a/editor/libeditor/EditorEventListener.h b/editor/libeditor/EditorEventListener.h new file mode 100644 index 000000000..505b711c7 --- /dev/null +++ b/editor/libeditor/EditorEventListener.h @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 4; 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/. */ + +#ifndef EditorEventListener_h +#define EditorEventListener_h + +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsIDOMEventListener.h" +#include "nsISupportsImpl.h" +#include "nscore.h" + +class nsCaret; +class nsIContent; +class nsIDOMDragEvent; +class nsIDOMEvent; +class nsIDOMKeyEvent; +class nsIDOMMouseEvent; +class nsIPresShell; +class nsPresContext; + +// X.h defines KeyPress +#ifdef KeyPress +#undef KeyPress +#endif + +#ifdef XP_WIN +// On Windows, we support switching the text direction by pressing Ctrl+Shift +#define HANDLE_NATIVE_TEXT_DIRECTION_SWITCH +#endif + +namespace mozilla { + +class EditorBase; + +class EditorEventListener : public nsIDOMEventListener +{ +public: + EditorEventListener(); + + virtual nsresult Connect(EditorBase* aEditorBase); + + void Disconnect(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + void SpellCheckIfNeeded(); + +protected: + virtual ~EditorEventListener(); + + nsresult InstallToEditor(); + void UninstallFromEditor(); + +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + nsresult KeyDown(nsIDOMKeyEvent* aKeyEvent); + nsresult KeyUp(nsIDOMKeyEvent* aKeyEvent); +#endif + nsresult KeyPress(nsIDOMKeyEvent* aKeyEvent); + nsresult HandleText(nsIDOMEvent* aTextEvent); + nsresult HandleStartComposition(nsIDOMEvent* aCompositionEvent); + void HandleEndComposition(nsIDOMEvent* aCompositionEvent); + virtual nsresult MouseDown(nsIDOMMouseEvent* aMouseEvent); + virtual nsresult MouseUp(nsIDOMMouseEvent* aMouseEvent) { return NS_OK; } + virtual nsresult MouseClick(nsIDOMMouseEvent* aMouseEvent); + nsresult Focus(nsIDOMEvent* aEvent); + nsresult Blur(nsIDOMEvent* aEvent); + nsresult DragEnter(nsIDOMDragEvent* aDragEvent); + nsresult DragOver(nsIDOMDragEvent* aDragEvent); + nsresult DragExit(nsIDOMDragEvent* aDragEvent); + nsresult Drop(nsIDOMDragEvent* aDragEvent); + + bool CanDrop(nsIDOMDragEvent* aEvent); + void CleanupDragDropCaret(); + already_AddRefed<nsIPresShell> GetPresShell(); + nsPresContext* GetPresContext(); + nsIContent* GetFocusedRootContent(); + // Returns true if IME consumes the mouse event. + bool NotifyIMEOfMouseButtonEvent(nsIDOMMouseEvent* aMouseEvent); + bool EditorHasFocus(); + bool IsFileControlTextBox(); + bool ShouldHandleNativeKeyBindings(nsIDOMKeyEvent* aKeyEvent); + nsresult HandleMiddleClickPaste(nsIDOMMouseEvent* aMouseEvent); + + EditorBase* mEditorBase; // weak + RefPtr<nsCaret> mCaret; + bool mCommitText; + bool mInTransaction; + bool mMouseDownOrUpConsumedByIME; +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + bool mHaveBidiKeyboards; + bool mShouldSwitchTextDirection; + bool mSwitchToRTL; +#endif +}; + +} // namespace mozilla + +#endif // #ifndef EditorEventListener_h diff --git a/editor/libeditor/EditorUtils.cpp b/editor/libeditor/EditorUtils.cpp new file mode 100644 index 000000000..4ec60c830 --- /dev/null +++ b/editor/libeditor/EditorUtils.cpp @@ -0,0 +1,227 @@ +/* -*- 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/EditorUtils.h" + +#include "mozilla/OwningNonNull.h" +#include "mozilla/dom/Selection.h" +#include "nsComponentManagerUtils.h" +#include "nsError.h" +#include "nsIClipboardDragDropHookList.h" +// hooks +#include "nsIClipboardDragDropHooks.h" +#include "nsIContent.h" +#include "nsIContentIterator.h" +#include "nsIDOMDocument.h" +#include "nsIDocShell.h" +#include "nsIDocument.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsINode.h" +#include "nsISimpleEnumerator.h" + +class nsISupports; +class nsRange; + +namespace mozilla { + +using namespace dom; + +/****************************************************************************** + * AutoSelectionRestorer + *****************************************************************************/ + +AutoSelectionRestorer::AutoSelectionRestorer( + Selection* aSelection, + EditorBase* aEditorBase + MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL) + : mEditorBase(nullptr) +{ + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + if (NS_WARN_IF(!aSelection) || NS_WARN_IF(!aEditorBase)) { + return; + } + if (aEditorBase->ArePreservingSelection()) { + // We already have initialized mSavedSel, so this must be nested call. + return; + } + mSelection = aSelection; + mEditorBase = aEditorBase; + mEditorBase->PreserveSelectionAcrossActions(mSelection); +} + +AutoSelectionRestorer::~AutoSelectionRestorer() +{ + NS_ASSERTION(!mSelection || mEditorBase, + "mEditorBase should be non-null when mSelection is"); + // mSelection will be null if this was nested call. + if (mSelection && mEditorBase->ArePreservingSelection()) { + mEditorBase->RestorePreservedSelection(mSelection); + } +} + +void +AutoSelectionRestorer::Abort() +{ + NS_ASSERTION(!mSelection || mEditorBase, + "mEditorBase should be non-null when mSelection is"); + if (mSelection) { + mEditorBase->StopPreservingSelection(); + } +} + +/****************************************************************************** + * some helper classes for iterating the dom tree + *****************************************************************************/ + +DOMIterator::DOMIterator(nsINode& aNode MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL) +{ + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + mIter = NS_NewContentIterator(); + DebugOnly<nsresult> rv = mIter->Init(&aNode); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +nsresult +DOMIterator::Init(nsRange& aRange) +{ + mIter = NS_NewContentIterator(); + return mIter->Init(&aRange); +} + +DOMIterator::DOMIterator(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_IN_IMPL) +{ + MOZ_GUARD_OBJECT_NOTIFIER_INIT; +} + +DOMIterator::~DOMIterator() +{ +} + +void +DOMIterator::AppendList(const BoolDomIterFunctor& functor, + nsTArray<OwningNonNull<nsINode>>& arrayOfNodes) const +{ + // Iterate through dom and build list + for (; !mIter->IsDone(); mIter->Next()) { + nsCOMPtr<nsINode> node = mIter->GetCurrentNode(); + + if (functor(node)) { + arrayOfNodes.AppendElement(*node); + } + } +} + +DOMSubtreeIterator::DOMSubtreeIterator( + MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_IN_IMPL) + : DOMIterator(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_TO_PARENT) +{ +} + +nsresult +DOMSubtreeIterator::Init(nsRange& aRange) +{ + mIter = NS_NewContentSubtreeIterator(); + return mIter->Init(&aRange); +} + +DOMSubtreeIterator::~DOMSubtreeIterator() +{ +} + +/****************************************************************************** + * some general purpose editor utils + *****************************************************************************/ + +bool +EditorUtils::IsDescendantOf(nsINode* aNode, + nsINode* aParent, + int32_t* aOffset) +{ + MOZ_ASSERT(aNode && aParent); + if (aNode == aParent) { + return false; + } + + for (nsCOMPtr<nsINode> node = aNode; node; node = node->GetParentNode()) { + if (node->GetParentNode() == aParent) { + if (aOffset) { + *aOffset = aParent->IndexOf(node); + } + return true; + } + } + + return false; +} + +bool +EditorUtils::IsDescendantOf(nsIDOMNode* aNode, + nsIDOMNode* aParent, + int32_t* aOffset) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + NS_ENSURE_TRUE(node && parent, false); + return IsDescendantOf(node, parent, aOffset); +} + +bool +EditorUtils::IsLeafNode(nsIDOMNode* aNode) +{ + bool hasChildren = false; + if (aNode) + aNode->HasChildNodes(&hasChildren); + return !hasChildren; +} + +/****************************************************************************** + * utility methods for drag/drop/copy/paste hooks + *****************************************************************************/ + +nsresult +EditorHookUtils::GetHookEnumeratorFromDocument(nsIDOMDocument* aDoc, + nsISimpleEnumerator** aResult) +{ + nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDocShell> docShell = doc->GetDocShell(); + nsCOMPtr<nsIClipboardDragDropHookList> hookObj = do_GetInterface(docShell); + NS_ENSURE_TRUE(hookObj, NS_ERROR_FAILURE); + + return hookObj->GetHookEnumerator(aResult); +} + +bool +EditorHookUtils::DoInsertionHook(nsIDOMDocument* aDoc, + nsIDOMEvent* aDropEvent, + nsITransferable *aTrans) +{ + nsCOMPtr<nsISimpleEnumerator> enumerator; + GetHookEnumeratorFromDocument(aDoc, getter_AddRefs(enumerator)); + NS_ENSURE_TRUE(enumerator, true); + + bool hasMoreHooks = false; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMoreHooks)) && + hasMoreHooks) { + nsCOMPtr<nsISupports> isupp; + if (NS_FAILED(enumerator->GetNext(getter_AddRefs(isupp)))) { + break; + } + + nsCOMPtr<nsIClipboardDragDropHooks> override = do_QueryInterface(isupp); + if (override) { + bool doInsert = true; + DebugOnly<nsresult> hookResult = + override->OnPasteOrDrop(aDropEvent, aTrans, &doInsert); + NS_ASSERTION(NS_SUCCEEDED(hookResult), "hook failure in OnPasteOrDrop"); + NS_ENSURE_TRUE(doInsert, false); + } + } + + return true; +} + +} // namespace mozilla diff --git a/editor/libeditor/EditorUtils.h b/editor/libeditor/EditorUtils.h new file mode 100644 index 000000000..34286da8a --- /dev/null +++ b/editor/libeditor/EditorUtils.h @@ -0,0 +1,315 @@ +/* -*- 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/. */ + + +#ifndef mozilla_EditorUtils_h +#define mozilla_EditorUtils_h + +#include "mozilla/EditorBase.h" +#include "mozilla/GuardObjects.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsIDOMNode.h" +#include "nsIEditor.h" +#include "nscore.h" + +class nsIAtom; +class nsIContentIterator; +class nsIDOMDocument; +class nsIDOMEvent; +class nsISimpleEnumerator; +class nsITransferable; +class nsRange; + +namespace mozilla { +template <class T> class OwningNonNull; + +namespace dom { +class Selection; +} // namespace dom + +/*************************************************************************** + * stack based helper class for batching a collection of txns inside a + * placeholder txn. + */ +class MOZ_RAII AutoPlaceHolderBatch +{ +private: + nsCOMPtr<nsIEditor> mEditor; + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER + +public: + AutoPlaceHolderBatch(nsIEditor* aEditor, + nsIAtom* aAtom + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : mEditor(aEditor) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + if (mEditor) { + mEditor->BeginPlaceHolderTransaction(aAtom); + } + } + ~AutoPlaceHolderBatch() + { + if (mEditor) { + mEditor->EndPlaceHolderTransaction(); + } + } +}; + +/*************************************************************************** + * stack based helper class for batching a collection of txns. + * Note: I changed this to use placeholder batching so that we get + * proper selection save/restore across undo/redo. + */ +class MOZ_RAII AutoEditBatch final : public AutoPlaceHolderBatch +{ +private: + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER + +public: + explicit AutoEditBatch(nsIEditor* aEditor + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : AutoPlaceHolderBatch(aEditor, nullptr) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + } + ~AutoEditBatch() {} +}; + +/*************************************************************************** + * stack based helper class for saving/restoring selection. Note that this + * assumes that the nodes involved are still around afterwards! + */ +class MOZ_RAII AutoSelectionRestorer final +{ +private: + // Ref-counted reference to the selection that we are supposed to restore. + RefPtr<dom::Selection> mSelection; + EditorBase* mEditorBase; // Non-owning ref to EditorBase. + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER + +public: + /** + * Constructor responsible for remembering all state needed to restore + * aSelection. + */ + AutoSelectionRestorer(dom::Selection* aSelection, + EditorBase* aEditorBase + MOZ_GUARD_OBJECT_NOTIFIER_PARAM); + + /** + * Destructor restores mSelection to its former state + */ + ~AutoSelectionRestorer(); + + /** + * Abort() cancels to restore the selection. + */ + void Abort(); +}; + +/*************************************************************************** + * stack based helper class for StartOperation()/EndOperation() sandwich + */ +class MOZ_RAII AutoRules final +{ +public: + AutoRules(EditorBase* aEditorBase, EditAction aAction, + nsIEditor::EDirection aDirection + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : mEditorBase(aEditorBase) + , mDoNothing(false) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + // mAction will already be set if this is nested call + if (mEditorBase && !mEditorBase->mAction) { + mEditorBase->StartOperation(aAction, aDirection); + } else { + mDoNothing = true; // nested calls will end up here + } + } + + ~AutoRules() + { + if (mEditorBase && !mDoNothing) { + mEditorBase->EndOperation(); + } + } + +protected: + EditorBase* mEditorBase; + bool mDoNothing; + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +/*************************************************************************** + * stack based helper class for turning off active selection adjustment + * by low level transactions + */ +class MOZ_RAII AutoTransactionsConserveSelection final +{ +public: + explicit AutoTransactionsConserveSelection(EditorBase* aEditorBase + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : mEditorBase(aEditorBase) + , mOldState(true) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + if (mEditorBase) { + mOldState = mEditorBase->GetShouldTxnSetSelection(); + mEditorBase->SetShouldTxnSetSelection(false); + } + } + + ~AutoTransactionsConserveSelection() + { + if (mEditorBase) { + mEditorBase->SetShouldTxnSetSelection(mOldState); + } + } + +protected: + EditorBase* mEditorBase; + bool mOldState; + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +/*************************************************************************** + * stack based helper class for batching reflow and paint requests. + */ +class MOZ_RAII AutoUpdateViewBatch final +{ +public: + explicit AutoUpdateViewBatch(EditorBase* aEditorBase + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : mEditorBase(aEditorBase) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + NS_ASSERTION(mEditorBase, "null mEditorBase pointer!"); + + if (mEditorBase) { + mEditorBase->BeginUpdateViewBatch(); + } + } + + ~AutoUpdateViewBatch() + { + if (mEditorBase) { + mEditorBase->EndUpdateViewBatch(); + } + } + +protected: + EditorBase* mEditorBase; + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +/****************************************************************************** + * some helper classes for iterating the dom tree + *****************************************************************************/ + +class BoolDomIterFunctor +{ +public: + virtual bool operator()(nsINode* aNode) const = 0; +}; + +class MOZ_RAII DOMIterator +{ +public: + explicit DOMIterator(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM); + + explicit DOMIterator(nsINode& aNode MOZ_GUARD_OBJECT_NOTIFIER_PARAM); + virtual ~DOMIterator(); + + nsresult Init(nsRange& aRange); + + void AppendList( + const BoolDomIterFunctor& functor, + nsTArray<mozilla::OwningNonNull<nsINode>>& arrayOfNodes) const; + +protected: + nsCOMPtr<nsIContentIterator> mIter; + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +class MOZ_RAII DOMSubtreeIterator final : public DOMIterator +{ +public: + explicit DOMSubtreeIterator(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM); + virtual ~DOMSubtreeIterator(); + + nsresult Init(nsRange& aRange); +}; + +class TrivialFunctor final : public BoolDomIterFunctor +{ +public: + // Used to build list of all nodes iterator covers + virtual bool operator()(nsINode* aNode) const + { + return true; + } +}; + +/****************************************************************************** + * general dom point utility struct + *****************************************************************************/ +struct MOZ_STACK_CLASS EditorDOMPoint final +{ + nsCOMPtr<nsINode> node; + int32_t offset; + + EditorDOMPoint() + : node(nullptr) + , offset(-1) + {} + EditorDOMPoint(nsINode* aNode, int32_t aOffset) + : node(aNode) + , offset(aOffset) + {} + EditorDOMPoint(nsIDOMNode* aNode, int32_t aOffset) + : node(do_QueryInterface(aNode)) + , offset(aOffset) + {} + + void SetPoint(nsINode* aNode, int32_t aOffset) + { + node = aNode; + offset = aOffset; + } + void SetPoint(nsIDOMNode* aNode, int32_t aOffset) + { + node = do_QueryInterface(aNode); + offset = aOffset; + } +}; + +class EditorUtils final +{ +public: + static bool IsDescendantOf(nsINode* aNode, nsINode* aParent, + int32_t* aOffset = 0); + static bool IsDescendantOf(nsIDOMNode* aNode, nsIDOMNode* aParent, + int32_t* aOffset = 0); + static bool IsLeafNode(nsIDOMNode* aNode); +}; + +class EditorHookUtils final +{ +public: + static bool DoInsertionHook(nsIDOMDocument* aDoc, nsIDOMEvent* aEvent, + nsITransferable* aTrans); + +private: + static nsresult GetHookEnumeratorFromDocument( + nsIDOMDocument*aDoc, + nsISimpleEnumerator** aEnumerator); +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_EditorUtils_h diff --git a/editor/libeditor/EditorUtils.js b/editor/libeditor/EditorUtils.js new file mode 100644 index 000000000..2959f67ab --- /dev/null +++ b/editor/libeditor/EditorUtils.js @@ -0,0 +1,34 @@ +/* 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/. */ + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +"use strict"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const EDITORUTILS_CID = Components.ID('{12e63991-86ac-4dff-bb1a-703495d67d17}'); + +function EditorUtils() { +} + +EditorUtils.prototype = { + classID: EDITORUTILS_CID, + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIEditorUtils ]), + + slurpBlob(aBlob, aScope, aListener) { + let reader = new aScope.FileReader(); + reader.addEventListener("load", (event) => { + aListener.onResult(event.target.result); + }); + reader.addEventListener("error", (event) => { + aListener.onError(event.target.error.message); + }); + + reader.readAsBinaryString(aBlob); + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([EditorUtils]); diff --git a/editor/libeditor/EditorUtils.manifest b/editor/libeditor/EditorUtils.manifest new file mode 100644 index 000000000..5c479e0b2 --- /dev/null +++ b/editor/libeditor/EditorUtils.manifest @@ -0,0 +1,2 @@ +component {12e63991-86ac-4dff-bb1a-703495d67d17} EditorUtils.js +contract @mozilla.org/editor-utils;1 {12e63991-86ac-4dff-bb1a-703495d67d17} diff --git a/editor/libeditor/HTMLAbsPositionEditor.cpp b/editor/libeditor/HTMLAbsPositionEditor.cpp new file mode 100644 index 000000000..670da78ae --- /dev/null +++ b/editor/libeditor/HTMLAbsPositionEditor.cpp @@ -0,0 +1,682 @@ +/* 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/HTMLEditor.h" + +#include <math.h> + +#include "HTMLEditorObjectResizerUtils.h" +#include "HTMLEditRules.h" +#include "HTMLEditUtils.h" +#include "TextEditUtils.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Element.h" +#include "mozilla/mozalloc.h" +#include "nsAString.h" +#include "nsAlgorithm.h" +#include "nsCOMPtr.h" +#include "nsComputedDOMStyle.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsROCSSPrimitiveValue.h" +#include "nsIDOMCSSStyleDeclaration.h" +#include "nsIDOMElement.h" +#include "nsIDOMEventListener.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMNode.h" +#include "nsDOMCSSRGBColor.h" +#include "nsIDOMWindow.h" +#include "nsIEditor.h" +#include "nsIEditRules.h" +#include "nsIHTMLEditor.h" +#include "nsIHTMLObjectResizer.h" +#include "nsINode.h" +#include "nsIPresShell.h" +#include "nsISupportsImpl.h" +#include "nsISupportsUtils.h" +#include "nsLiteralString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nscore.h" +#include <algorithm> + +namespace mozilla { + +using namespace dom; + +#define BLACK_BG_RGB_TRIGGER 0xd0 + +NS_IMETHODIMP +HTMLEditor::AbsolutePositionSelection(bool aEnabled) +{ + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, + aEnabled ? EditAction::setAbsolutePosition : + EditAction::removeAbsolutePosition, + nsIEditor::eNext); + + // the line below does not match the code; should it be removed? + // Find out if the selection is collapsed: + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + TextRulesInfo ruleInfo(aEnabled ? EditAction::setAbsolutePosition : + EditAction::removeAbsolutePosition); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (NS_FAILED(rv) || cancel) { + return rv; + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +HTMLEditor::GetAbsolutelyPositionedSelectionContainer(nsIDOMElement** _retval) +{ + nsAutoString positionStr; + nsCOMPtr<nsINode> node = GetSelectionContainer(); + nsCOMPtr<nsIDOMNode> resultNode; + + while (!resultNode && node && !node->IsHTMLElement(nsGkAtoms::html)) { + nsresult rv = + mCSSEditUtils->GetComputedProperty(*node, *nsGkAtoms::position, + positionStr); + NS_ENSURE_SUCCESS(rv, rv); + if (positionStr.EqualsLiteral("absolute")) + resultNode = GetAsDOMNode(node); + else { + node = node->GetParentNode(); + } + } + + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(resultNode); + element.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetSelectionContainerAbsolutelyPositioned( + bool* aIsSelectionContainerAbsolutelyPositioned) +{ + *aIsSelectionContainerAbsolutelyPositioned = (mAbsolutelyPositionedObject != nullptr); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetAbsolutePositioningEnabled(bool* aIsEnabled) +{ + *aIsEnabled = mIsAbsolutelyPositioningEnabled; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::SetAbsolutePositioningEnabled(bool aIsEnabled) +{ + mIsAbsolutelyPositioningEnabled = aIsEnabled; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RelativeChangeElementZIndex(nsIDOMElement* aElement, + int32_t aChange, + int32_t* aReturn) +{ + NS_ENSURE_ARG_POINTER(aElement); + NS_ENSURE_ARG_POINTER(aReturn); + if (!aChange) // early way out, no change + return NS_OK; + + int32_t zIndex; + nsresult rv = GetElementZIndex(aElement, &zIndex); + NS_ENSURE_SUCCESS(rv, rv); + + zIndex = std::max(zIndex + aChange, 0); + SetElementZIndex(aElement, zIndex); + *aReturn = zIndex; + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::SetElementZIndex(nsIDOMElement* aElement, + int32_t aZindex) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_ARG_POINTER(element); + + nsAutoString zIndexStr; + zIndexStr.AppendInt(aZindex); + + mCSSEditUtils->SetCSSProperty(*element, *nsGkAtoms::z_index, zIndexStr); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RelativeChangeZIndex(int32_t aChange) +{ + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, + (aChange < 0) ? EditAction::decreaseZIndex : + EditAction::increaseZIndex, + nsIEditor::eNext); + + // brade: can we get rid of this comment? + // Find out if the selection is collapsed: + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + TextRulesInfo ruleInfo(aChange < 0 ? EditAction::decreaseZIndex : + EditAction::increaseZIndex); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +HTMLEditor::GetElementZIndex(nsIDOMElement* aElement, + int32_t* aZindex) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_STATE(element || !aElement); + nsAutoString zIndexStr; + *aZindex = 0; + + nsresult rv = + mCSSEditUtils->GetSpecifiedProperty(*element, *nsGkAtoms::z_index, + zIndexStr); + NS_ENSURE_SUCCESS(rv, rv); + if (zIndexStr.EqualsLiteral("auto")) { + // we have to look at the positioned ancestors + // cf. CSS 2 spec section 9.9.1 + nsCOMPtr<nsIDOMNode> parentNode; + rv = aElement->GetParentNode(getter_AddRefs(parentNode)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsINode> node = do_QueryInterface(parentNode); + nsAutoString positionStr; + while (node && zIndexStr.EqualsLiteral("auto") && + !node->IsHTMLElement(nsGkAtoms::body)) { + rv = mCSSEditUtils->GetComputedProperty(*node, *nsGkAtoms::position, + positionStr); + NS_ENSURE_SUCCESS(rv, rv); + if (positionStr.EqualsLiteral("absolute")) { + // ah, we found one, what's its z-index ? If its z-index is auto, + // we have to continue climbing the document's tree + rv = mCSSEditUtils->GetComputedProperty(*node, *nsGkAtoms::z_index, + zIndexStr); + NS_ENSURE_SUCCESS(rv, rv); + } + node = node->GetParentNode(); + } + } + + if (!zIndexStr.EqualsLiteral("auto")) { + nsresult errorCode; + *aZindex = zIndexStr.ToInteger(&errorCode); + } + + return NS_OK; +} + +already_AddRefed<Element> +HTMLEditor::CreateGrabber(nsINode* aParentNode) +{ + // let's create a grabber through the element factory + nsCOMPtr<nsIDOMElement> retDOM; + CreateAnonymousElement(NS_LITERAL_STRING("span"), GetAsDOMNode(aParentNode), + NS_LITERAL_STRING("mozGrabber"), false, + getter_AddRefs(retDOM)); + + NS_ENSURE_TRUE(retDOM, nullptr); + + // add the mouse listener so we can detect a click on a resizer + nsCOMPtr<nsIDOMEventTarget> evtTarget(do_QueryInterface(retDOM)); + evtTarget->AddEventListener(NS_LITERAL_STRING("mousedown"), + mEventListener, false); + + nsCOMPtr<Element> ret = do_QueryInterface(retDOM); + return ret.forget(); +} + +NS_IMETHODIMP +HTMLEditor::RefreshGrabber() +{ + NS_ENSURE_TRUE(mAbsolutelyPositionedObject, NS_ERROR_NULL_POINTER); + + nsresult rv = + GetPositionAndDimensions( + static_cast<nsIDOMElement*>(GetAsDOMNode(mAbsolutelyPositionedObject)), + mPositionedObjectX, + mPositionedObjectY, + mPositionedObjectWidth, + mPositionedObjectHeight, + mPositionedObjectBorderLeft, + mPositionedObjectBorderTop, + mPositionedObjectMarginLeft, + mPositionedObjectMarginTop); + NS_ENSURE_SUCCESS(rv, rv); + + SetAnonymousElementPosition(mPositionedObjectX+12, + mPositionedObjectY-14, + static_cast<nsIDOMElement*>(GetAsDOMNode(mGrabber))); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::HideGrabber() +{ + nsresult rv = mAbsolutelyPositionedObject->UnsetAttr(kNameSpaceID_None, + nsGkAtoms::_moz_abspos, + true); + NS_ENSURE_SUCCESS(rv, rv); + + mAbsolutelyPositionedObject = nullptr; + NS_ENSURE_TRUE(mGrabber, NS_ERROR_NULL_POINTER); + + // get the presshell's document observer interface. + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + // We allow the pres shell to be null; when it is, we presume there + // are no document observers to notify, but we still want to + // UnbindFromTree. + + nsCOMPtr<nsIContent> parentContent = mGrabber->GetParent(); + NS_ENSURE_TRUE(parentContent, NS_ERROR_NULL_POINTER); + + DeleteRefToAnonymousNode(static_cast<nsIDOMElement*>(GetAsDOMNode(mGrabber)), parentContent, ps); + mGrabber = nullptr; + DeleteRefToAnonymousNode(static_cast<nsIDOMElement*>(GetAsDOMNode(mPositioningShadow)), parentContent, ps); + mPositioningShadow = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::ShowGrabberOnElement(nsIDOMElement* aElement) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_ARG_POINTER(element); + + if (NS_WARN_IF(!IsDescendantOfEditorRoot(element))) { + return NS_ERROR_UNEXPECTED; + } + + if (mGrabber) { + NS_ERROR("call HideGrabber first"); + return NS_ERROR_UNEXPECTED; + } + + nsAutoString classValue; + nsresult rv = CheckPositionedElementBGandFG(aElement, classValue); + NS_ENSURE_SUCCESS(rv, rv); + + rv = element->SetAttr(kNameSpaceID_None, nsGkAtoms::_moz_abspos, + classValue, true); + NS_ENSURE_SUCCESS(rv, rv); + + // first, let's keep track of that element... + mAbsolutelyPositionedObject = element; + + mGrabber = CreateGrabber(element->GetParentNode()); + NS_ENSURE_TRUE(mGrabber, NS_ERROR_FAILURE); + + // and set its position + return RefreshGrabber(); +} + +nsresult +HTMLEditor::StartMoving(nsIDOMElement* aHandle) +{ + nsCOMPtr<nsINode> parentNode = mGrabber->GetParentNode(); + + // now, let's create the resizing shadow + mPositioningShadow = CreateShadow(GetAsDOMNode(parentNode), + static_cast<nsIDOMElement*>(GetAsDOMNode(mAbsolutelyPositionedObject))); + NS_ENSURE_TRUE(mPositioningShadow, NS_ERROR_FAILURE); + nsresult rv = SetShadowPosition(mPositioningShadow, + mAbsolutelyPositionedObject, + mPositionedObjectX, mPositionedObjectY); + NS_ENSURE_SUCCESS(rv, rv); + + // make the shadow appear + mPositioningShadow->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true); + + // position it + mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::width, + mPositionedObjectWidth); + mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::height, + mPositionedObjectHeight); + + mIsMoving = true; + return NS_OK; // XXX Looks like nobody refers this result +} + +void +HTMLEditor::SnapToGrid(int32_t& newX, int32_t& newY) +{ + if (mSnapToGridEnabled && mGridSize) { + newX = (int32_t) floor( ((float)newX / (float)mGridSize) + 0.5f ) * mGridSize; + newY = (int32_t) floor( ((float)newY / (float)mGridSize) + 0.5f ) * mGridSize; + } +} + +nsresult +HTMLEditor::GrabberClicked() +{ + // add a mouse move listener to the editor + nsresult rv = NS_OK; + if (!mMouseMotionListenerP) { + mMouseMotionListenerP = new ResizerMouseMotionListener(this); + if (!mMouseMotionListenerP) {return NS_ERROR_NULL_POINTER;} + + nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget(); + NS_ENSURE_TRUE(piTarget, NS_ERROR_FAILURE); + + rv = piTarget->AddEventListener(NS_LITERAL_STRING("mousemove"), + mMouseMotionListenerP, + false, false); + NS_ASSERTION(NS_SUCCEEDED(rv), + "failed to register mouse motion listener"); + } + mGrabberClicked = true; + return rv; +} + +nsresult +HTMLEditor::EndMoving() +{ + if (mPositioningShadow) { + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsIContent> parentContent = mGrabber->GetParent(); + NS_ENSURE_TRUE(parentContent, NS_ERROR_FAILURE); + + DeleteRefToAnonymousNode(static_cast<nsIDOMElement*>(GetAsDOMNode(mPositioningShadow)), + parentContent, ps); + + mPositioningShadow = nullptr; + } + nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget(); + + if (piTarget && mMouseMotionListenerP) { + DebugOnly<nsresult> rv = + piTarget->RemoveEventListener(NS_LITERAL_STRING("mousemove"), + mMouseMotionListenerP, + false); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to remove mouse motion listener"); + } + mMouseMotionListenerP = nullptr; + + mGrabberClicked = false; + mIsMoving = false; + RefPtr<Selection> selection = GetSelection(); + if (!selection) { + return NS_ERROR_NOT_INITIALIZED; + } + return CheckSelectionStateForAnonymousButtons(selection); +} +nsresult +HTMLEditor::SetFinalPosition(int32_t aX, + int32_t aY) +{ + nsresult rv = EndMoving(); + NS_ENSURE_SUCCESS(rv, rv); + + // we have now to set the new width and height of the resized object + // we don't set the x and y position because we don't control that in + // a normal HTML layout + int32_t newX = mPositionedObjectX + aX - mOriginalX - (mPositionedObjectBorderLeft+mPositionedObjectMarginLeft); + int32_t newY = mPositionedObjectY + aY - mOriginalY - (mPositionedObjectBorderTop+mPositionedObjectMarginTop); + + SnapToGrid(newX, newY); + + nsAutoString x, y; + x.AppendInt(newX); + y.AppendInt(newY); + + // we want one transaction only from a user's point of view + AutoEditBatch batchIt(this); + + nsCOMPtr<Element> absolutelyPositionedObject = + do_QueryInterface(mAbsolutelyPositionedObject); + NS_ENSURE_STATE(absolutelyPositionedObject); + mCSSEditUtils->SetCSSPropertyPixels(*absolutelyPositionedObject, + *nsGkAtoms::top, newY); + mCSSEditUtils->SetCSSPropertyPixels(*absolutelyPositionedObject, + *nsGkAtoms::left, newX); + // keep track of that size + mPositionedObjectX = newX; + mPositionedObjectY = newY; + + return RefreshResizers(); +} + +void +HTMLEditor::AddPositioningOffset(int32_t& aX, + int32_t& aY) +{ + // Get the positioning offset + int32_t positioningOffset = + Preferences::GetInt("editor.positioning.offset", 0); + + aX += positioningOffset; + aY += positioningOffset; +} + +NS_IMETHODIMP +HTMLEditor::AbsolutelyPositionElement(nsIDOMElement* aElement, + bool aEnabled) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_ARG_POINTER(element); + + nsAutoString positionStr; + mCSSEditUtils->GetComputedProperty(*element, *nsGkAtoms::position, + positionStr); + bool isPositioned = (positionStr.EqualsLiteral("absolute")); + + // nothing to do if the element is already in the state we want + if (isPositioned == aEnabled) + return NS_OK; + + AutoEditBatch batchIt(this); + + if (aEnabled) { + int32_t x, y; + GetElementOrigin(aElement, x, y); + + mCSSEditUtils->SetCSSProperty(*element, *nsGkAtoms::position, + NS_LITERAL_STRING("absolute")); + + AddPositioningOffset(x, y); + SnapToGrid(x, y); + SetElementPosition(*element, x, y); + + // we may need to create a br if the positioned element is alone in its + // container + nsCOMPtr<nsINode> element = do_QueryInterface(aElement); + NS_ENSURE_STATE(element); + + nsINode* parentNode = element->GetParentNode(); + if (parentNode->GetChildCount() == 1) { + nsCOMPtr<nsIDOMNode> brNode; + nsresult rv = CreateBR(parentNode->AsDOMNode(), 0, address_of(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + else { + mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::position, + EmptyString()); + mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::top, + EmptyString()); + mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::left, + EmptyString()); + mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::z_index, + EmptyString()); + + if (!HTMLEditUtils::IsImage(aElement)) { + mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::width, + EmptyString()); + mCSSEditUtils->RemoveCSSProperty(*element, *nsGkAtoms::height, + EmptyString()); + } + + nsCOMPtr<dom::Element> element = do_QueryInterface(aElement); + if (element && element->IsHTMLElement(nsGkAtoms::div) && + !HasStyleOrIdOrClass(element)) { + RefPtr<HTMLEditRules> htmlRules = + static_cast<HTMLEditRules*>(mRules.get()); + NS_ENSURE_TRUE(htmlRules, NS_ERROR_FAILURE); + nsresult rv = htmlRules->MakeSureElemStartsOrEndsOnCR(aElement); + NS_ENSURE_SUCCESS(rv, rv); + rv = RemoveContainer(element); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::SetSnapToGridEnabled(bool aEnabled) +{ + mSnapToGridEnabled = aEnabled; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetSnapToGridEnabled(bool* aIsEnabled) +{ + *aIsEnabled = mSnapToGridEnabled; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::SetGridSize(uint32_t aSize) +{ + mGridSize = aSize; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetGridSize(uint32_t* aSize) +{ + *aSize = mGridSize; + return NS_OK; +} + +// self-explanatory +NS_IMETHODIMP +HTMLEditor::SetElementPosition(nsIDOMElement* aElement, + int32_t aX, + int32_t aY) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_STATE(element); + + SetElementPosition(*element, aX, aY); + return NS_OK; +} + +void +HTMLEditor::SetElementPosition(Element& aElement, + int32_t aX, + int32_t aY) +{ + AutoEditBatch batchIt(this); + mCSSEditUtils->SetCSSPropertyPixels(aElement, *nsGkAtoms::left, aX); + mCSSEditUtils->SetCSSPropertyPixels(aElement, *nsGkAtoms::top, aY); +} + +// self-explanatory +NS_IMETHODIMP +HTMLEditor::GetPositionedElement(nsIDOMElement** aReturn) +{ + nsCOMPtr<nsIDOMElement> ret = + static_cast<nsIDOMElement*>(GetAsDOMNode(mAbsolutelyPositionedObject)); + ret.forget(aReturn); + return NS_OK; +} + +nsresult +HTMLEditor::CheckPositionedElementBGandFG(nsIDOMElement* aElement, + nsAString& aReturn) +{ + // we are going to outline the positioned element and bring it to the + // front to overlap any other element intersecting with it. But + // first, let's see what's the background and foreground colors of the + // positioned element. + // if background-image computed value is 'none, + // If the background color is 'auto' and R G B values of the foreground are + // each above #d0, use a black background + // If the background color is 'auto' and at least one of R G B values of + // the foreground is below #d0, use a white background + // Otherwise don't change background/foreground + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_STATE(element || !aElement); + + aReturn.Truncate(); + + nsAutoString bgImageStr; + nsresult rv = + mCSSEditUtils->GetComputedProperty(*element, *nsGkAtoms::background_image, + bgImageStr); + NS_ENSURE_SUCCESS(rv, rv); + if (bgImageStr.EqualsLiteral("none")) { + nsAutoString bgColorStr; + rv = + mCSSEditUtils->GetComputedProperty(*element, *nsGkAtoms::backgroundColor, + bgColorStr); + NS_ENSURE_SUCCESS(rv, rv); + if (bgColorStr.EqualsLiteral("transparent")) { + RefPtr<nsComputedDOMStyle> cssDecl = + mCSSEditUtils->GetComputedStyle(element); + NS_ENSURE_STATE(cssDecl); + + // from these declarations, get the one we want and that one only + ErrorResult error; + RefPtr<dom::CSSValue> cssVal = cssDecl->GetPropertyCSSValue(NS_LITERAL_STRING("color"), error); + NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult()); + + nsROCSSPrimitiveValue* val = cssVal->AsPrimitiveValue(); + NS_ENSURE_TRUE(val, NS_ERROR_FAILURE); + + if (nsIDOMCSSPrimitiveValue::CSS_RGBCOLOR == val->PrimitiveType()) { + nsDOMCSSRGBColor* rgbVal = val->GetRGBColorValue(error); + NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult()); + float r = rgbVal->Red()-> + GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_NUMBER, error); + NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult()); + float g = rgbVal->Green()-> + GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_NUMBER, error); + NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult()); + float b = rgbVal->Blue()-> + GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_NUMBER, error); + NS_ENSURE_TRUE(!error.Failed(), error.StealNSResult()); + if (r >= BLACK_BG_RGB_TRIGGER && + g >= BLACK_BG_RGB_TRIGGER && + b >= BLACK_BG_RGB_TRIGGER) + aReturn.AssignLiteral("black"); + else + aReturn.AssignLiteral("white"); + return NS_OK; + } + } + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLAnonymousNodeEditor.cpp b/editor/libeditor/HTMLAnonymousNodeEditor.cpp new file mode 100644 index 000000000..48f20fd04 --- /dev/null +++ b/editor/libeditor/HTMLAnonymousNodeEditor.cpp @@ -0,0 +1,548 @@ +/* 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/HTMLEditor.h" + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Element.h" +#include "mozilla/mozalloc.h" +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsComputedDOMStyle.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsID.h" +#include "nsIDOMCSSPrimitiveValue.h" +#include "nsIDOMCSSStyleDeclaration.h" +#include "nsIDOMCSSValue.h" +#include "nsIDOMElement.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMHTMLElement.h" +#include "nsIDOMNode.h" +#include "nsIDOMWindow.h" +#include "nsIDocument.h" +#include "nsIDocumentObserver.h" +#include "nsIHTMLAbsPosEditor.h" +#include "nsIHTMLInlineTableEditor.h" +#include "nsIHTMLObjectResizer.h" +#include "nsStubMutationObserver.h" +#include "nsINode.h" +#include "nsIPresShell.h" +#include "nsISupportsImpl.h" +#include "nsISupportsUtils.h" +#include "nsLiteralString.h" +#include "nsPresContext.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsUnicharUtils.h" +#include "nscore.h" +#include "nsContentUtils.h" // for nsAutoScriptBlocker + +class nsIDOMEventListener; +class nsISelection; + +namespace mozilla { + +using namespace dom; + +// retrieve an integer stored into a CSS computed float value +static int32_t GetCSSFloatValue(nsIDOMCSSStyleDeclaration * aDecl, + const nsAString & aProperty) +{ + MOZ_ASSERT(aDecl); + + nsCOMPtr<nsIDOMCSSValue> value; + // get the computed CSSValue of the property + nsresult rv = aDecl->GetPropertyCSSValue(aProperty, getter_AddRefs(value)); + if (NS_FAILED(rv) || !value) { + return 0; + } + + // check the type of the returned CSSValue; we handle here only + // pixel and enum types + nsCOMPtr<nsIDOMCSSPrimitiveValue> val = do_QueryInterface(value); + uint16_t type; + val->GetPrimitiveType(&type); + + float f = 0; + switch (type) { + case nsIDOMCSSPrimitiveValue::CSS_PX: + // the value is in pixels, just get it + rv = val->GetFloatValue(nsIDOMCSSPrimitiveValue::CSS_PX, &f); + NS_ENSURE_SUCCESS(rv, 0); + break; + case nsIDOMCSSPrimitiveValue::CSS_IDENT: { + // the value is keyword, we have to map these keywords into + // numeric values + nsAutoString str; + val->GetStringValue(str); + if (str.EqualsLiteral("thin")) { + f = 1; + } else if (str.EqualsLiteral("medium")) { + f = 3; + } else if (str.EqualsLiteral("thick")) { + f = 5; + } + break; + } + } + + return (int32_t) f; +} + +class ElementDeletionObserver final : public nsStubMutationObserver +{ +public: + ElementDeletionObserver(nsIContent* aNativeAnonNode, + nsIContent* aObservedNode) + : mNativeAnonNode(aNativeAnonNode) + , mObservedNode(aObservedNode) + {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED + NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED + +protected: + ~ElementDeletionObserver() {} + nsIContent* mNativeAnonNode; + nsIContent* mObservedNode; +}; + +NS_IMPL_ISUPPORTS(ElementDeletionObserver, nsIMutationObserver) + +void +ElementDeletionObserver::ParentChainChanged(nsIContent* aContent) +{ + // If the native anonymous content has been unbound already in + // DeleteRefToAnonymousNode, mNativeAnonNode's parentNode is null. + if (aContent == mObservedNode && mNativeAnonNode && + mNativeAnonNode->GetParentNode() == aContent) { + // If the observed node has been moved to another document, there isn't much + // we can do easily. But at least be safe and unbind the native anonymous + // content and stop observing changes. + if (mNativeAnonNode->OwnerDoc() != mObservedNode->OwnerDoc()) { + mObservedNode->RemoveMutationObserver(this); + mObservedNode = nullptr; + mNativeAnonNode->RemoveMutationObserver(this); + mNativeAnonNode->UnbindFromTree(); + mNativeAnonNode = nullptr; + NS_RELEASE_THIS(); + return; + } + + // We're staying in the same document, just rebind the native anonymous + // node so that the subtree root points to the right object etc. + mNativeAnonNode->UnbindFromTree(); + mNativeAnonNode->BindToTree(mObservedNode->GetUncomposedDoc(), mObservedNode, + mObservedNode, true); + } +} + +void +ElementDeletionObserver::NodeWillBeDestroyed(const nsINode* aNode) +{ + NS_ASSERTION(aNode == mNativeAnonNode || aNode == mObservedNode, + "Wrong aNode!"); + if (aNode == mNativeAnonNode) { + mObservedNode->RemoveMutationObserver(this); + mObservedNode = nullptr; + } else { + mNativeAnonNode->RemoveMutationObserver(this); + mNativeAnonNode->UnbindFromTree(); + mNativeAnonNode = nullptr; + } + + NS_RELEASE_THIS(); +} + +// Returns in *aReturn an anonymous nsDOMElement of type aTag, +// child of aParentNode. If aIsCreatedHidden is true, the class +// "hidden" is added to the created element. If aAnonClass is not +// the empty string, it becomes the value of the attribute "_moz_anonclass" +nsresult +HTMLEditor::CreateAnonymousElement(const nsAString& aTag, + nsIDOMNode* aParentNode, + const nsAString& aAnonClass, + bool aIsCreatedHidden, + nsIDOMElement** aReturn) +{ + NS_ENSURE_ARG_POINTER(aParentNode); + NS_ENSURE_ARG_POINTER(aReturn); + *aReturn = nullptr; + + nsCOMPtr<nsIContent> parentContent( do_QueryInterface(aParentNode) ); + NS_ENSURE_TRUE(parentContent, NS_OK); + + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER); + + // Get the pres shell + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + // Create a new node through the element factory + nsCOMPtr<nsIAtom> tagAtom = NS_Atomize(aTag); + nsCOMPtr<Element> newContent = CreateHTMLContent(tagAtom); + NS_ENSURE_STATE(newContent); + + nsCOMPtr<nsIDOMElement> newElement = do_QueryInterface(newContent); + NS_ENSURE_TRUE(newElement, NS_ERROR_FAILURE); + + // add the "hidden" class if needed + if (aIsCreatedHidden) { + nsresult rv = newElement->SetAttribute(NS_LITERAL_STRING("class"), + NS_LITERAL_STRING("hidden")); + NS_ENSURE_SUCCESS(rv, rv); + } + + // add an _moz_anonclass attribute if needed + if (!aAnonClass.IsEmpty()) { + nsresult rv = newElement->SetAttribute(NS_LITERAL_STRING("_moz_anonclass"), + aAnonClass); + NS_ENSURE_SUCCESS(rv, rv); + } + + { + nsAutoScriptBlocker scriptBlocker; + + // establish parenthood of the element + newContent->SetIsNativeAnonymousRoot(); + nsresult rv = + newContent->BindToTree(doc, parentContent, parentContent, true); + if (NS_FAILED(rv)) { + newContent->UnbindFromTree(); + return rv; + } + } + + ElementDeletionObserver* observer = + new ElementDeletionObserver(newContent, parentContent); + NS_ADDREF(observer); // NodeWillBeDestroyed releases. + parentContent->AddMutationObserver(observer); + newContent->AddMutationObserver(observer); + +#ifdef DEBUG + // Editor anonymous content gets passed to RecreateFramesFor... which can't + // _really_ deal with anonymous content (because it can't get the frame tree + // ordering right). But for us the ordering doesn't matter so this is sort of + // ok. + newContent->SetProperty(nsGkAtoms::restylableAnonymousNode, + reinterpret_cast<void*>(true)); +#endif // DEBUG + + // display the element + ps->RecreateFramesFor(newContent); + + newElement.forget(aReturn); + return NS_OK; +} + +// Removes event listener and calls DeleteRefToAnonymousNode. +void +HTMLEditor::RemoveListenerAndDeleteRef(const nsAString& aEvent, + nsIDOMEventListener* aListener, + bool aUseCapture, + Element* aElement, + nsIContent* aParentContent, + nsIPresShell* aShell) +{ + nsCOMPtr<nsIDOMEventTarget> evtTarget(do_QueryInterface(aElement)); + if (evtTarget) { + evtTarget->RemoveEventListener(aEvent, aListener, aUseCapture); + } + DeleteRefToAnonymousNode(static_cast<nsIDOMElement*>(GetAsDOMNode(aElement)), aParentContent, aShell); +} + +// Deletes all references to an anonymous element +void +HTMLEditor::DeleteRefToAnonymousNode(nsIDOMElement* aElement, + nsIContent* aParentContent, + nsIPresShell* aShell) +{ + // call ContentRemoved() for the anonymous content + // node so its references get removed from the frame manager's + // undisplay map, and its layout frames get destroyed! + + if (aElement) { + nsCOMPtr<nsIContent> content = do_QueryInterface(aElement); + if (content) { + nsAutoScriptBlocker scriptBlocker; + // Need to check whether aShell has been destroyed (but not yet deleted). + // In that case presContext->GetPresShell() returns nullptr. + // See bug 338129. + if (content->IsInComposedDoc() && aShell && aShell->GetPresContext() && + aShell->GetPresContext()->GetPresShell() == aShell) { + nsCOMPtr<nsIDocumentObserver> docObserver = do_QueryInterface(aShell); + if (docObserver) { + // Call BeginUpdate() so that the nsCSSFrameConstructor/PresShell + // knows we're messing with the frame tree. + nsCOMPtr<nsIDocument> document = GetDocument(); + if (document) { + docObserver->BeginUpdate(document, UPDATE_CONTENT_MODEL); + } + + // XXX This is wrong (bug 439258). Once it's fixed, the NS_WARNING + // in RestyleManager::RestyleForRemove should be changed back + // to an assertion. + docObserver->ContentRemoved(content->GetComposedDoc(), + aParentContent, content, -1, + content->GetPreviousSibling()); + if (document) { + docObserver->EndUpdate(document, UPDATE_CONTENT_MODEL); + } + } + } + content->UnbindFromTree(); + } + } +} + +// The following method is mostly called by a selection listener. When a +// selection change is notified, the method is called to check if resizing +// handles, a grabber and/or inline table editing UI need to be displayed +// or refreshed +NS_IMETHODIMP +HTMLEditor::CheckSelectionStateForAnonymousButtons(nsISelection* aSelection) +{ + NS_ENSURE_ARG_POINTER(aSelection); + + // early way out if all contextual UI extensions are disabled + NS_ENSURE_TRUE(mIsObjectResizingEnabled || + mIsAbsolutelyPositioningEnabled || + mIsInlineTableEditingEnabled, NS_OK); + + // Don't change selection state if we're moving. + if (mIsMoving) { + return NS_OK; + } + + nsCOMPtr<nsIDOMElement> focusElement; + // let's get the containing element of the selection + nsresult rv = GetSelectionContainer(getter_AddRefs(focusElement)); + NS_ENSURE_TRUE(focusElement, NS_OK); + NS_ENSURE_SUCCESS(rv, rv); + + // If we're not in a document, don't try to add resizers + nsCOMPtr<dom::Element> focusElementNode = do_QueryInterface(focusElement); + NS_ENSURE_STATE(focusElementNode); + if (!focusElementNode->IsInUncomposedDoc()) { + return NS_OK; + } + + // what's its tag? + nsAutoString focusTagName; + rv = focusElement->GetTagName(focusTagName); + NS_ENSURE_SUCCESS(rv, rv); + ToLowerCase(focusTagName); + nsCOMPtr<nsIAtom> focusTagAtom = NS_Atomize(focusTagName); + + nsCOMPtr<nsIDOMElement> absPosElement; + if (mIsAbsolutelyPositioningEnabled) { + // Absolute Positioning support is enabled, is the selection contained + // in an absolutely positioned element ? + rv = + GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(absPosElement)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIDOMElement> cellElement; + if (mIsObjectResizingEnabled || mIsInlineTableEditingEnabled) { + // Resizing or Inline Table Editing is enabled, we need to check if the + // selection is contained in a table cell + rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), + nullptr, + getter_AddRefs(cellElement)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mIsObjectResizingEnabled && cellElement) { + // we are here because Resizing is enabled AND selection is contained in + // a cell + + // get the enclosing table + if (nsGkAtoms::img != focusTagAtom) { + // the element container of the selection is not an image, so we'll show + // the resizers around the table + nsCOMPtr<nsIDOMNode> tableNode = GetEnclosingTable(cellElement); + focusElement = do_QueryInterface(tableNode); + focusTagAtom = nsGkAtoms::table; + } + } + + // we allow resizers only around images, tables, and absolutely positioned + // elements. If we don't have image/table, let's look at the latter case. + if (nsGkAtoms::img != focusTagAtom && nsGkAtoms::table != focusTagAtom) { + focusElement = absPosElement; + } + + // at this point, focusElement contains the element for Resizing, + // cellElement contains the element for InlineTableEditing + // absPosElement contains the element for Positioning + + // Note: All the Hide/Show methods below may change attributes on real + // content which means a DOMAttrModified handler may cause arbitrary + // side effects while this code runs (bug 420439). + + if (mIsAbsolutelyPositioningEnabled && mAbsolutelyPositionedObject && + absPosElement != GetAsDOMNode(mAbsolutelyPositionedObject)) { + rv = HideGrabber(); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(!mAbsolutelyPositionedObject, "HideGrabber failed"); + } + + if (mIsObjectResizingEnabled && mResizedObject && + GetAsDOMNode(mResizedObject) != focusElement) { + rv = HideResizers(); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(!mResizedObject, "HideResizers failed"); + } + + if (mIsInlineTableEditingEnabled && mInlineEditedCell && + mInlineEditedCell != cellElement) { + rv = HideInlineTableEditingUI(); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(!mInlineEditedCell, "HideInlineTableEditingUI failed"); + } + + // now, let's display all contextual UI for good + nsIContent* hostContent = GetActiveEditingHost(); + nsCOMPtr<nsIDOMNode> hostNode = do_QueryInterface(hostContent); + + if (mIsObjectResizingEnabled && focusElement && + IsModifiableNode(focusElement) && focusElement != hostNode) { + if (nsGkAtoms::img == focusTagAtom) { + mResizedObjectIsAnImage = true; + } + if (mResizedObject) { + nsresult rv = RefreshResizers(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsresult rv = ShowResizers(focusElement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + if (mIsAbsolutelyPositioningEnabled && absPosElement && + IsModifiableNode(absPosElement) && absPosElement != hostNode) { + if (mAbsolutelyPositionedObject) { + nsresult rv = RefreshGrabber(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsresult rv = ShowGrabberOnElement(absPosElement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + if (mIsInlineTableEditingEnabled && cellElement && + IsModifiableNode(cellElement) && cellElement != hostNode) { + if (mInlineEditedCell) { + nsresult rv = RefreshInlineTableEditingUI(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsresult rv = ShowInlineTableEditingUI(cellElement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + return NS_OK; +} + +// Resizing and Absolute Positioning need to know everything about the +// containing box of the element: position, size, margins, borders +nsresult +HTMLEditor::GetPositionAndDimensions(nsIDOMElement* aElement, + int32_t& aX, + int32_t& aY, + int32_t& aW, + int32_t& aH, + int32_t& aBorderLeft, + int32_t& aBorderTop, + int32_t& aMarginLeft, + int32_t& aMarginTop) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_ARG_POINTER(element); + + // Is the element positioned ? let's check the cheap way first... + bool isPositioned = false; + nsresult rv = + aElement->HasAttribute(NS_LITERAL_STRING("_moz_abspos"), &isPositioned); + NS_ENSURE_SUCCESS(rv, rv); + if (!isPositioned) { + // hmmm... the expensive way now... + nsAutoString positionStr; + mCSSEditUtils->GetComputedProperty(*element, *nsGkAtoms::position, + positionStr); + isPositioned = positionStr.EqualsLiteral("absolute"); + } + + if (isPositioned) { + // Yes, it is absolutely positioned + mResizedObjectIsAbsolutelyPositioned = true; + + // Get the all the computed css styles attached to the element node + RefPtr<nsComputedDOMStyle> cssDecl = + mCSSEditUtils->GetComputedStyle(element); + NS_ENSURE_STATE(cssDecl); + + aBorderLeft = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("border-left-width")); + aBorderTop = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("border-top-width")); + aMarginLeft = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("margin-left")); + aMarginTop = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("margin-top")); + + aX = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("left")) + + aMarginLeft + aBorderLeft; + aY = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("top")) + + aMarginTop + aBorderTop; + aW = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("width")); + aH = GetCSSFloatValue(cssDecl, NS_LITERAL_STRING("height")); + } else { + mResizedObjectIsAbsolutelyPositioned = false; + nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(aElement); + if (!htmlElement) { + return NS_ERROR_NULL_POINTER; + } + GetElementOrigin(aElement, aX, aY); + + if (NS_WARN_IF(NS_FAILED(htmlElement->GetOffsetWidth(&aW))) || + NS_WARN_IF(NS_FAILED(htmlElement->GetOffsetHeight(&aH)))) { + return rv; + } + + aBorderLeft = 0; + aBorderTop = 0; + aMarginLeft = 0; + aMarginTop = 0; + } + return NS_OK; +} + +// self-explanatory +void +HTMLEditor::SetAnonymousElementPosition(int32_t aX, + int32_t aY, + nsIDOMElement* aElement) +{ + mCSSEditUtils->SetCSSPropertyPixels(aElement, NS_LITERAL_STRING("left"), aX); + mCSSEditUtils->SetCSSPropertyPixels(aElement, NS_LITERAL_STRING("top"), aY); +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLEditRules.cpp b/editor/libeditor/HTMLEditRules.cpp new file mode 100644 index 000000000..c2d61f767 --- /dev/null +++ b/editor/libeditor/HTMLEditRules.cpp @@ -0,0 +1,8785 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 et tw=79: */ +/* 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 "HTMLEditRules.h" + +#include <stdlib.h> + +#include "HTMLEditUtils.h" +#include "TextEditUtils.h" +#include "WSRunObject.h" +#include "mozilla/Assertions.h" +#include "mozilla/CSSEditUtils.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Element.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/mozalloc.h" +#include "nsAutoPtr.h" +#include "nsAString.h" +#include "nsAlgorithm.h" +#include "nsCRT.h" +#include "nsCRTGlue.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsIContentIterator.h" +#include "nsID.h" +#include "nsIDOMCharacterData.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIDOMNode.h" +#include "nsIDOMText.h" +#include "nsIFrame.h" +#include "nsIHTMLAbsPosEditor.h" +#include "nsIHTMLDocument.h" +#include "nsINode.h" +#include "nsLiteralString.h" +#include "nsRange.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsTextNode.h" +#include "nsThreadUtils.h" +#include "nsUnicharUtils.h" +#include <algorithm> + +// Workaround for windows headers +#ifdef SetProp +#undef SetProp +#endif + +class nsISupports; + +namespace mozilla { + +class RulesInfo; + +using namespace dom; + +//const static char* kMOZEditorBogusNodeAttr="MOZ_EDITOR_BOGUS_NODE"; +//const static char* kMOZEditorBogusNodeValue="TRUE"; + +enum +{ + kLonely = 0, + kPrevSib = 1, + kNextSib = 2, + kBothSibs = 3 +}; + +/******************************************************** + * first some helpful functors we will use + ********************************************************/ + +static bool IsBlockNode(const nsINode& node) +{ + return HTMLEditor::NodeIsBlockStatic(&node); +} + +static bool IsInlineNode(const nsINode& node) +{ + return !IsBlockNode(node); +} + +static bool +IsStyleCachePreservingAction(EditAction action) +{ + return action == EditAction::deleteSelection || + action == EditAction::insertBreak || + action == EditAction::makeList || + action == EditAction::indent || + action == EditAction::outdent || + action == EditAction::align || + action == EditAction::makeBasicBlock || + action == EditAction::removeList || + action == EditAction::makeDefListItem || + action == EditAction::insertElement || + action == EditAction::insertQuotation; +} + +class TableCellAndListItemFunctor final : public BoolDomIterFunctor +{ +public: + // Used to build list of all li's, td's & th's iterator covers + virtual bool operator()(nsINode* aNode) const + { + return HTMLEditUtils::IsTableCell(aNode) || + HTMLEditUtils::IsListItem(aNode); + } +}; + +class BRNodeFunctor final : public BoolDomIterFunctor +{ +public: + virtual bool operator()(nsINode* aNode) const + { + return aNode->IsHTMLElement(nsGkAtoms::br); + } +}; + +class EmptyEditableFunctor final : public BoolDomIterFunctor +{ +public: + explicit EmptyEditableFunctor(HTMLEditor* aHTMLEditor) + : mHTMLEditor(aHTMLEditor) + {} + + virtual bool operator()(nsINode* aNode) const + { + if (mHTMLEditor->IsEditable(aNode) && + (HTMLEditUtils::IsListItem(aNode) || + HTMLEditUtils::IsTableCellOrCaption(*aNode))) { + bool bIsEmptyNode; + nsresult rv = + mHTMLEditor->IsEmptyNode(aNode, &bIsEmptyNode, false, false); + NS_ENSURE_SUCCESS(rv, false); + if (bIsEmptyNode) { + return true; + } + } + return false; + } + +protected: + HTMLEditor* mHTMLEditor; +}; + +/******************************************************** + * mozilla::HTMLEditRules + ********************************************************/ + +HTMLEditRules::HTMLEditRules() + : mHTMLEditor(nullptr) + , mListenerEnabled(false) + , mReturnInEmptyLIKillsList(false) + , mDidDeleteSelection(false) + , mDidRangedDelete(false) + , mRestoreContentEditableCount(false) + , mJoinOffset(0) +{ + InitFields(); +} + +void +HTMLEditRules::InitFields() +{ + mHTMLEditor = nullptr; + mDocChangeRange = nullptr; + mListenerEnabled = true; + mReturnInEmptyLIKillsList = true; + mDidDeleteSelection = false; + mDidRangedDelete = false; + mRestoreContentEditableCount = false; + mUtilRange = nullptr; + mJoinOffset = 0; + mNewBlock = nullptr; + mRangeItem = new RangeItem(); + // populate mCachedStyles + mCachedStyles[0] = StyleCache(nsGkAtoms::b, EmptyString(), EmptyString()); + mCachedStyles[1] = StyleCache(nsGkAtoms::i, EmptyString(), EmptyString()); + mCachedStyles[2] = StyleCache(nsGkAtoms::u, EmptyString(), EmptyString()); + mCachedStyles[3] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("face"), EmptyString()); + mCachedStyles[4] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("size"), EmptyString()); + mCachedStyles[5] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("color"), EmptyString()); + mCachedStyles[6] = StyleCache(nsGkAtoms::tt, EmptyString(), EmptyString()); + mCachedStyles[7] = StyleCache(nsGkAtoms::em, EmptyString(), EmptyString()); + mCachedStyles[8] = StyleCache(nsGkAtoms::strong, EmptyString(), EmptyString()); + mCachedStyles[9] = StyleCache(nsGkAtoms::dfn, EmptyString(), EmptyString()); + mCachedStyles[10] = StyleCache(nsGkAtoms::code, EmptyString(), EmptyString()); + mCachedStyles[11] = StyleCache(nsGkAtoms::samp, EmptyString(), EmptyString()); + mCachedStyles[12] = StyleCache(nsGkAtoms::var, EmptyString(), EmptyString()); + mCachedStyles[13] = StyleCache(nsGkAtoms::cite, EmptyString(), EmptyString()); + mCachedStyles[14] = StyleCache(nsGkAtoms::abbr, EmptyString(), EmptyString()); + mCachedStyles[15] = StyleCache(nsGkAtoms::acronym, EmptyString(), EmptyString()); + mCachedStyles[16] = StyleCache(nsGkAtoms::backgroundColor, EmptyString(), EmptyString()); + mCachedStyles[17] = StyleCache(nsGkAtoms::sub, EmptyString(), EmptyString()); + mCachedStyles[18] = StyleCache(nsGkAtoms::sup, EmptyString(), EmptyString()); +} + +HTMLEditRules::~HTMLEditRules() +{ + // remove ourselves as a listener to edit actions + // In some cases, we have already been removed by + // ~HTMLEditor, in which case we will get a null pointer here + // which we ignore. But this allows us to add the ability to + // switch rule sets on the fly if we want. + if (mHTMLEditor) { + mHTMLEditor->RemoveEditActionListener(this); + } +} + +NS_IMPL_ADDREF_INHERITED(HTMLEditRules, TextEditRules) +NS_IMPL_RELEASE_INHERITED(HTMLEditRules, TextEditRules) +NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLEditRules) + NS_INTERFACE_TABLE_INHERITED(HTMLEditRules, nsIEditActionListener) +NS_INTERFACE_TABLE_TAIL_INHERITING(TextEditRules) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLEditRules, TextEditRules, + mDocChangeRange, mUtilRange, mNewBlock, + mRangeItem) + +NS_IMETHODIMP +HTMLEditRules::Init(TextEditor* aTextEditor) +{ + InitFields(); + + mHTMLEditor = static_cast<HTMLEditor*>(aTextEditor); + + // call through to base class Init + nsresult rv = TextEditRules::Init(aTextEditor); + NS_ENSURE_SUCCESS(rv, rv); + + // cache any prefs we care about + static const char kPrefName[] = + "editor.html.typing.returnInEmptyListItemClosesList"; + nsAdoptingCString returnInEmptyLIKillsList = + Preferences::GetCString(kPrefName); + + // only when "false", becomes FALSE. Otherwise (including empty), TRUE. + // XXX Why was this pref designed as a string and not bool? + mReturnInEmptyLIKillsList = !returnInEmptyLIKillsList.EqualsLiteral("false"); + + // make a utility range for use by the listenter + nsCOMPtr<nsINode> node = mHTMLEditor->GetRoot(); + if (!node) { + node = mHTMLEditor->GetDocument(); + } + + NS_ENSURE_STATE(node); + + mUtilRange = new nsRange(node); + + // set up mDocChangeRange to be whole doc + // temporarily turn off rules sniffing + AutoLockRulesSniffing lockIt(this); + if (!mDocChangeRange) { + mDocChangeRange = new nsRange(node); + } + + if (node->IsElement()) { + ErrorResult rv; + mDocChangeRange->SelectNode(*node, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + AdjustSpecialBreaks(); + } + + // add ourselves as a listener to edit actions + return mHTMLEditor->AddEditActionListener(this); +} + +NS_IMETHODIMP +HTMLEditRules::DetachEditor() +{ + if (mHTMLEditor) { + mHTMLEditor->RemoveEditActionListener(this); + } + mHTMLEditor = nullptr; + return TextEditRules::DetachEditor(); +} + +NS_IMETHODIMP +HTMLEditRules::BeforeEdit(EditAction action, + nsIEditor::EDirection aDirection) +{ + if (mLockRulesSniffing) { + return NS_OK; + } + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + AutoLockRulesSniffing lockIt(this); + mDidExplicitlySetInterline = false; + + if (!mActionNesting) { + mActionNesting++; + + // Clear our flag about if just deleted a range + mDidRangedDelete = false; + + // Remember where our selection was before edit action took place: + + // Get selection + RefPtr<Selection> selection = htmlEditor->GetSelection(); + + // Get the selection location + if (NS_WARN_IF(!selection) || !selection->RangeCount()) { + return NS_ERROR_UNEXPECTED; + } + mRangeItem->startNode = selection->GetRangeAt(0)->GetStartParent(); + mRangeItem->startOffset = selection->GetRangeAt(0)->StartOffset(); + mRangeItem->endNode = selection->GetRangeAt(0)->GetEndParent(); + mRangeItem->endOffset = selection->GetRangeAt(0)->EndOffset(); + nsCOMPtr<nsINode> selStartNode = mRangeItem->startNode; + nsCOMPtr<nsINode> selEndNode = mRangeItem->endNode; + + // Register with range updater to track this as we perturb the doc + htmlEditor->mRangeUpdater.RegisterRangeItem(mRangeItem); + + // Clear deletion state bool + mDidDeleteSelection = false; + + // Clear out mDocChangeRange and mUtilRange + if (mDocChangeRange) { + // Clear out our accounting of what changed + mDocChangeRange->Reset(); + } + if (mUtilRange) { + // Ditto for mUtilRange. + mUtilRange->Reset(); + } + + // Remember current inline styles for deletion and normal insertion ops + if (action == EditAction::insertText || + action == EditAction::insertIMEText || + action == EditAction::deleteSelection || + IsStyleCachePreservingAction(action)) { + nsCOMPtr<nsINode> selNode = + aDirection == nsIEditor::eNext ? selEndNode : selStartNode; + nsresult rv = CacheInlineStyles(GetAsDOMNode(selNode)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Stabilize the document against contenteditable count changes + nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc); + NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE); + if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) { + htmlDoc->ChangeContentEditableCount(nullptr, +1); + mRestoreContentEditableCount = true; + } + + // Check that selection is in subtree defined by body node + ConfirmSelectionInBody(); + // Let rules remember the top level action + mTheAction = action; + } + return NS_OK; +} + + +NS_IMETHODIMP +HTMLEditRules::AfterEdit(EditAction action, + nsIEditor::EDirection aDirection) +{ + if (mLockRulesSniffing) { + return NS_OK; + } + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + AutoLockRulesSniffing lockIt(this); + + MOZ_ASSERT(mActionNesting > 0); + nsresult rv = NS_OK; + mActionNesting--; + if (!mActionNesting) { + // Do all the tricky stuff + rv = AfterEditInner(action, aDirection); + + // Free up selectionState range item + htmlEditor->mRangeUpdater.DropRangeItem(mRangeItem); + + // Reset the contenteditable count to its previous value + if (mRestoreContentEditableCount) { + nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc); + NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE); + if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) { + htmlDoc->ChangeContentEditableCount(nullptr, -1); + } + mRestoreContentEditableCount = false; + } + } + + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +HTMLEditRules::AfterEditInner(EditAction action, + nsIEditor::EDirection aDirection) +{ + ConfirmSelectionInBody(); + if (action == EditAction::ignore) { + return NS_OK; + } + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<Selection> selection = mHTMLEditor->GetSelection(); + NS_ENSURE_STATE(selection); + + nsCOMPtr<nsIDOMNode> rangeStartParent, rangeEndParent; + int32_t rangeStartOffset = 0, rangeEndOffset = 0; + // do we have a real range to act on? + bool bDamagedRange = false; + if (mDocChangeRange) { + mDocChangeRange->GetStartContainer(getter_AddRefs(rangeStartParent)); + mDocChangeRange->GetEndContainer(getter_AddRefs(rangeEndParent)); + mDocChangeRange->GetStartOffset(&rangeStartOffset); + mDocChangeRange->GetEndOffset(&rangeEndOffset); + if (rangeStartParent && rangeEndParent) + bDamagedRange = true; + } + + if (bDamagedRange && !((action == EditAction::undo) || + (action == EditAction::redo))) { + // 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. + NS_ENSURE_STATE(mHTMLEditor); + AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); + + // expand the "changed doc range" as needed + PromoteRange(*mDocChangeRange, action); + + // if we did a ranged deletion or handling backspace key, make sure we have + // a place to put caret. + // Note we only want to do this if the overall operation was deletion, + // not if deletion was done along the way for EditAction::loadHTML, EditAction::insertText, etc. + // That's why this is here rather than DidDeleteSelection(). + if (action == EditAction::deleteSelection && mDidRangedDelete) { + nsresult rv = InsertBRIfNeeded(selection); + NS_ENSURE_SUCCESS(rv, rv); + } + + // add in any needed <br>s, and remove any unneeded ones. + AdjustSpecialBreaks(); + + // merge any adjacent text nodes + if (action != EditAction::insertText && + action != EditAction::insertIMEText) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->CollapseAdjacentTextNodes(mDocChangeRange); + NS_ENSURE_SUCCESS(rv, rv); + } + + // clean up any empty nodes in the selection + nsresult rv = RemoveEmptyNodes(); + NS_ENSURE_SUCCESS(rv, rv); + + // attempt to transform any unneeded nbsp's into spaces after doing various operations + if (action == EditAction::insertText || + action == EditAction::insertIMEText || + action == EditAction::deleteSelection || + action == EditAction::insertBreak || + action == EditAction::htmlPaste || + action == EditAction::loadHTML) { + rv = AdjustWhitespace(selection); + NS_ENSURE_SUCCESS(rv, rv); + + // also do this for original selection endpoints. + NS_ENSURE_STATE(mHTMLEditor); + NS_ENSURE_STATE(mRangeItem->startNode); + NS_ENSURE_STATE(mRangeItem->endNode); + WSRunObject(mHTMLEditor, mRangeItem->startNode, + mRangeItem->startOffset).AdjustWhitespace(); + // we only need to handle old selection endpoint if it was different from start + if (mRangeItem->startNode != mRangeItem->endNode || + mRangeItem->startOffset != mRangeItem->endOffset) { + NS_ENSURE_STATE(mHTMLEditor); + WSRunObject(mHTMLEditor, mRangeItem->endNode, + mRangeItem->endOffset).AdjustWhitespace(); + } + } + + // if we created a new block, make sure selection lands in it + if (mNewBlock) { + rv = PinSelectionToNewBlock(selection); + mNewBlock = nullptr; + } + + // adjust selection for insert text, html paste, and delete actions + if (action == EditAction::insertText || + action == EditAction::insertIMEText || + action == EditAction::deleteSelection || + action == EditAction::insertBreak || + action == EditAction::htmlPaste || + action == EditAction::loadHTML) { + rv = AdjustSelection(selection, aDirection); + NS_ENSURE_SUCCESS(rv, rv); + } + + // check for any styles which were removed inappropriately + if (action == EditAction::insertText || + action == EditAction::insertIMEText || + action == EditAction::deleteSelection || + IsStyleCachePreservingAction(action)) { + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->mTypeInState->UpdateSelState(selection); + rv = ReapplyCachedStyles(); + NS_ENSURE_SUCCESS(rv, rv); + ClearCachedStyles(); + } + } + + NS_ENSURE_STATE(mHTMLEditor); + + nsresult rv = + mHTMLEditor->HandleInlineSpellCheck(action, selection, + GetAsDOMNode(mRangeItem->startNode), + mRangeItem->startOffset, + rangeStartParent, rangeStartOffset, + rangeEndParent, rangeEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // detect empty doc + rv = CreateBogusNodeIfNeeded(selection); + NS_ENSURE_SUCCESS(rv, rv); + + // adjust selection HINT if needed + if (!mDidExplicitlySetInterline) { + CheckInterlinePosition(*selection); + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditRules::WillDoAction(Selection* aSelection, + RulesInfo* aInfo, + bool* aCancel, + bool* aHandled) +{ + MOZ_ASSERT(aInfo && aCancel && aHandled); + + *aCancel = false; + *aHandled = false; + + // my kingdom for dynamic cast + TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo); + + // Deal with actions for which we don't need to check whether the selection is + // editable. + if (info->action == EditAction::outputText || + info->action == EditAction::undo || + info->action == EditAction::redo) { + return TextEditRules::WillDoAction(aSelection, aInfo, aCancel, aHandled); + } + + // Nothing to do if there's no selection to act on + if (!aSelection) { + return NS_OK; + } + NS_ENSURE_TRUE(aSelection->RangeCount(), NS_OK); + + RefPtr<nsRange> range = aSelection->GetRangeAt(0); + nsCOMPtr<nsINode> selStartNode = range->GetStartParent(); + + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsModifiableNode(selStartNode)) { + *aCancel = true; + return NS_OK; + } + + nsCOMPtr<nsINode> selEndNode = range->GetEndParent(); + + if (selStartNode != selEndNode) { + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsModifiableNode(selEndNode)) { + *aCancel = true; + return NS_OK; + } + + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsModifiableNode(range->GetCommonAncestor())) { + *aCancel = true; + return NS_OK; + } + } + + switch (info->action) { + case EditAction::insertText: + case EditAction::insertIMEText: + UndefineCaretBidiLevel(aSelection); + return WillInsertText(info->action, aSelection, aCancel, aHandled, + info->inString, info->outString, info->maxLength); + case EditAction::loadHTML: + return WillLoadHTML(aSelection, aCancel); + case EditAction::insertBreak: + UndefineCaretBidiLevel(aSelection); + return WillInsertBreak(*aSelection, aCancel, aHandled); + case EditAction::deleteSelection: + return WillDeleteSelection(aSelection, info->collapsedAction, + info->stripWrappers, aCancel, aHandled); + case EditAction::makeList: + return WillMakeList(aSelection, info->blockType, info->entireList, + info->bulletType, aCancel, aHandled); + case EditAction::indent: + return WillIndent(aSelection, aCancel, aHandled); + case EditAction::outdent: + return WillOutdent(*aSelection, aCancel, aHandled); + case EditAction::setAbsolutePosition: + return WillAbsolutePosition(*aSelection, aCancel, aHandled); + case EditAction::removeAbsolutePosition: + return WillRemoveAbsolutePosition(aSelection, aCancel, aHandled); + case EditAction::align: + return WillAlign(*aSelection, *info->alignType, aCancel, aHandled); + case EditAction::makeBasicBlock: + return WillMakeBasicBlock(*aSelection, *info->blockType, aCancel, + aHandled); + case EditAction::removeList: + return WillRemoveList(aSelection, info->bOrdered, aCancel, aHandled); + case EditAction::makeDefListItem: + return WillMakeDefListItem(aSelection, info->blockType, info->entireList, + aCancel, aHandled); + case EditAction::insertElement: + WillInsert(*aSelection, aCancel); + return NS_OK; + case EditAction::decreaseZIndex: + return WillRelativeChangeZIndex(aSelection, -1, aCancel, aHandled); + case EditAction::increaseZIndex: + return WillRelativeChangeZIndex(aSelection, 1, aCancel, aHandled); + default: + return TextEditRules::WillDoAction(aSelection, aInfo, + aCancel, aHandled); + } +} + +NS_IMETHODIMP +HTMLEditRules::DidDoAction(Selection* aSelection, + RulesInfo* aInfo, + nsresult aResult) +{ + TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo); + switch (info->action) { + case EditAction::insertBreak: + return DidInsertBreak(aSelection, aResult); + case EditAction::deleteSelection: + return DidDeleteSelection(aSelection, info->collapsedAction, aResult); + case EditAction::makeBasicBlock: + case EditAction::indent: + case EditAction::outdent: + case EditAction::align: + return DidMakeBasicBlock(aSelection, aInfo, aResult); + case EditAction::setAbsolutePosition: { + nsresult rv = DidMakeBasicBlock(aSelection, aInfo, aResult); + NS_ENSURE_SUCCESS(rv, rv); + return DidAbsolutePosition(); + } + default: + // pass through to TextEditRules + return TextEditRules::DidDoAction(aSelection, aInfo, aResult); + } +} + +nsresult +HTMLEditRules::GetListState(bool* aMixed, + bool* aOL, + bool* aUL, + bool* aDL) +{ + NS_ENSURE_TRUE(aMixed && aOL && aUL && aDL, NS_ERROR_NULL_POINTER); + *aMixed = false; + *aOL = false; + *aUL = false; + *aDL = false; + bool bNonList = false; + + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no, + TouchContent::no); + NS_ENSURE_SUCCESS(rv, rv); + + // Examine list type for nodes in selection. + for (const auto& curNode : arrayOfNodes) { + if (!curNode->IsElement()) { + bNonList = true; + } else if (curNode->IsHTMLElement(nsGkAtoms::ul)) { + *aUL = true; + } else if (curNode->IsHTMLElement(nsGkAtoms::ol)) { + *aOL = true; + } else if (curNode->IsHTMLElement(nsGkAtoms::li)) { + if (dom::Element* parent = curNode->GetParentElement()) { + if (parent->IsHTMLElement(nsGkAtoms::ul)) { + *aUL = true; + } else if (parent->IsHTMLElement(nsGkAtoms::ol)) { + *aOL = true; + } + } + } else if (curNode->IsAnyOfHTMLElements(nsGkAtoms::dl, + nsGkAtoms::dt, + nsGkAtoms::dd)) { + *aDL = true; + } else { + bNonList = true; + } + } + + // hokey arithmetic with booleans + if ((*aUL + *aOL + *aDL + bNonList) > 1) { + *aMixed = true; + } + + return NS_OK; +} + +nsresult +HTMLEditRules::GetListItemState(bool* aMixed, + bool* aLI, + bool* aDT, + bool* aDD) +{ + NS_ENSURE_TRUE(aMixed && aLI && aDT && aDD, NS_ERROR_NULL_POINTER); + *aMixed = false; + *aLI = false; + *aDT = false; + *aDD = false; + bool bNonList = false; + + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no, + TouchContent::no); + NS_ENSURE_SUCCESS(rv, rv); + + // examine list type for nodes in selection + for (const auto& node : arrayOfNodes) { + if (!node->IsElement()) { + bNonList = true; + } else if (node->IsAnyOfHTMLElements(nsGkAtoms::ul, + nsGkAtoms::ol, + nsGkAtoms::li)) { + *aLI = true; + } else if (node->IsHTMLElement(nsGkAtoms::dt)) { + *aDT = true; + } else if (node->IsHTMLElement(nsGkAtoms::dd)) { + *aDD = true; + } else if (node->IsHTMLElement(nsGkAtoms::dl)) { + // need to look inside dl and see which types of items it has + bool bDT, bDD; + GetDefinitionListItemTypes(node->AsElement(), &bDT, &bDD); + *aDT |= bDT; + *aDD |= bDD; + } else { + bNonList = true; + } + } + + // hokey arithmetic with booleans + if (*aDT + *aDD + bNonList > 1) { + *aMixed = true; + } + + return NS_OK; +} + +nsresult +HTMLEditRules::GetAlignment(bool* aMixed, + nsIHTMLEditor::EAlignment* aAlign) +{ + MOZ_ASSERT(aMixed && aAlign); + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // For now, just return first alignment. We'll lie about if it's mixed. + // This is for efficiency given that our current ui doesn't care if it's + // mixed. + // cmanske: NOT TRUE! We would like to pay attention to mixed state in Format + // | Align submenu! + + // This routine assumes that alignment is done ONLY via divs + + // Default alignment is left + *aMixed = false; + *aAlign = nsIHTMLEditor::eLeft; + + // Get selection + NS_ENSURE_STATE(htmlEditor->GetSelection()); + OwningNonNull<Selection> selection = *htmlEditor->GetSelection(); + + // Get selection location + NS_ENSURE_TRUE(htmlEditor->GetRoot(), NS_ERROR_FAILURE); + OwningNonNull<Element> root = *htmlEditor->GetRoot(); + + int32_t rootOffset = root->GetParentNode() ? + root->GetParentNode()->IndexOf(root) : -1; + + NS_ENSURE_STATE(selection->GetRangeAt(0) && + selection->GetRangeAt(0)->GetStartParent()); + OwningNonNull<nsINode> parent = *selection->GetRangeAt(0)->GetStartParent(); + int32_t offset = selection->GetRangeAt(0)->StartOffset(); + + // Is the selection collapsed? + nsCOMPtr<nsINode> nodeToExamine; + if (selection->Collapsed() || parent->GetAsText()) { + // If selection is collapsed, we want to look at 'parent' and its ancestors + // for divs with alignment on them. If we are in a text node, then that is + // the node of interest. + nodeToExamine = parent; + } else if (parent->IsHTMLElement(nsGkAtoms::html) && offset == rootOffset) { + // If we have selected the body, let's look at the first editable node + nodeToExamine = htmlEditor->GetNextNode(parent, offset, true); + } else { + nsTArray<RefPtr<nsRange>> arrayOfRanges; + GetPromotedRanges(selection, arrayOfRanges, EditAction::align); + + // Use these ranges to construct a list of nodes to act on. + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + nsresult rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, + EditAction::align, TouchContent::no); + NS_ENSURE_SUCCESS(rv, rv); + nodeToExamine = arrayOfNodes.SafeElementAt(0); + } + + NS_ENSURE_TRUE(nodeToExamine, NS_ERROR_NULL_POINTER); + + NS_NAMED_LITERAL_STRING(typeAttrName, "align"); + nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(*nodeToExamine); + + NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE); + + if (htmlEditor->IsCSSEnabled() && + htmlEditor->mCSSEditUtils->IsCSSEditableProperty(blockParent, nullptr, + &typeAttrName)) { + // We are in CSS mode and we know how to align this element with CSS + nsAutoString value; + // Let's get the value(s) of text-align or margin-left/margin-right + htmlEditor->mCSSEditUtils->GetCSSEquivalentToHTMLInlineStyleSet( + blockParent, nullptr, &typeAttrName, value, CSSEditUtils::eComputed); + if (value.EqualsLiteral("center") || + value.EqualsLiteral("-moz-center") || + value.EqualsLiteral("auto auto")) { + *aAlign = nsIHTMLEditor::eCenter; + return NS_OK; + } + if (value.EqualsLiteral("right") || + value.EqualsLiteral("-moz-right") || + value.EqualsLiteral("auto 0px")) { + *aAlign = nsIHTMLEditor::eRight; + return NS_OK; + } + if (value.EqualsLiteral("justify")) { + *aAlign = nsIHTMLEditor::eJustify; + return NS_OK; + } + *aAlign = nsIHTMLEditor::eLeft; + return NS_OK; + } + + // Check up the ladder for divs with alignment + bool isFirstNodeToExamine = true; + for (; nodeToExamine; nodeToExamine = nodeToExamine->GetParentNode()) { + if (!isFirstNodeToExamine && + nodeToExamine->IsHTMLElement(nsGkAtoms::table)) { + // The node to examine is a table and this is not the first node we + // examine; let's break here to materialize the 'inline-block' behaviour + // of html tables regarding to text alignment + return NS_OK; + } + if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(nodeToExamine))) { + // Check for alignment + nsAutoString typeAttrVal; + nodeToExamine->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::align, + typeAttrVal); + ToLowerCase(typeAttrVal); + if (!typeAttrVal.IsEmpty()) { + if (typeAttrVal.EqualsLiteral("center")) { + *aAlign = nsIHTMLEditor::eCenter; + } else if (typeAttrVal.EqualsLiteral("right")) { + *aAlign = nsIHTMLEditor::eRight; + } else if (typeAttrVal.EqualsLiteral("justify")) { + *aAlign = nsIHTMLEditor::eJustify; + } else { + *aAlign = nsIHTMLEditor::eLeft; + } + return NS_OK; + } + } + isFirstNodeToExamine = false; + } + return NS_OK; +} + +static nsIAtom& MarginPropertyAtomForIndent(CSSEditUtils& aHTMLCSSUtils, + nsINode& aNode) +{ + nsAutoString direction; + aHTMLCSSUtils.GetComputedProperty(aNode, *nsGkAtoms::direction, direction); + return direction.EqualsLiteral("rtl") ? + *nsGkAtoms::marginRight : *nsGkAtoms::marginLeft; +} + +nsresult +HTMLEditRules::GetIndentState(bool* aCanIndent, + bool* aCanOutdent) +{ + // XXX Looks like that this is implementation of + // nsIHTMLEditor::getIndentState() however nobody calls this method + // even with the interface method. + NS_ENSURE_TRUE(aCanIndent && aCanOutdent, NS_ERROR_FAILURE); + *aCanIndent = true; + *aCanOutdent = false; + + // get selection + NS_ENSURE_STATE(mHTMLEditor && mHTMLEditor->GetSelection()); + OwningNonNull<Selection> selection = *mHTMLEditor->GetSelection(); + + // contruct a list of nodes to act on. + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + nsresult rv = GetNodesFromSelection(*selection, EditAction::indent, + arrayOfNodes, TouchContent::no); + NS_ENSURE_SUCCESS(rv, rv); + + // examine nodes in selection for blockquotes or list elements; + // these we can outdent. Note that we return true for canOutdent + // if *any* of the selection is outdentable, rather than all of it. + NS_ENSURE_STATE(mHTMLEditor); + bool useCSS = mHTMLEditor->IsCSSEnabled(); + for (auto& curNode : Reversed(arrayOfNodes)) { + if (HTMLEditUtils::IsNodeThatCanOutdent(GetAsDOMNode(curNode))) { + *aCanOutdent = true; + break; + } else if (useCSS) { + // we are in CSS mode, indentation is done using the margin-left (or margin-right) property + NS_ENSURE_STATE(mHTMLEditor); + nsIAtom& marginProperty = + MarginPropertyAtomForIndent(*mHTMLEditor->mCSSEditUtils, curNode); + nsAutoString value; + // retrieve its specified value + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->mCSSEditUtils->GetSpecifiedProperty(*curNode, + marginProperty, value); + float f; + nsCOMPtr<nsIAtom> unit; + // get its number part and its unit + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit)); + // if the number part is strictly positive, outdent is possible + if (0 < f) { + *aCanOutdent = true; + break; + } + } + } + + if (!*aCanOutdent) { + // if we haven't found something to outdent yet, also check the parents + // of selection endpoints. We might have a blockquote or list item + // in the parent hierarchy. + + // gather up info we need for test + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIDOMNode> parent, tmp, root = do_QueryInterface(mHTMLEditor->GetRoot()); + NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER); + int32_t selOffset; + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<Selection> selection = mHTMLEditor->GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // test start parent hierarchy + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->GetStartNodeAndOffset(selection, getter_AddRefs(parent), + &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + while (parent && parent != root) { + if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) { + *aCanOutdent = true; + break; + } + tmp = parent; + tmp->GetParentNode(getter_AddRefs(parent)); + } + + // test end parent hierarchy + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->GetEndNodeAndOffset(selection, getter_AddRefs(parent), + &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + while (parent && parent != root) { + if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) { + *aCanOutdent = true; + break; + } + tmp = parent; + tmp->GetParentNode(getter_AddRefs(parent)); + } + } + return NS_OK; +} + + +nsresult +HTMLEditRules::GetParagraphState(bool* aMixed, + nsAString& outFormat) +{ + // This routine is *heavily* tied to our ui choices in the paragraph + // style popup. I can't see a way around that. + NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER); + *aMixed = true; + outFormat.Truncate(0); + + bool bMixed = false; + // using "x" as an uninitialized value, since "" is meaningful + nsAutoString formatStr(NS_LITERAL_STRING("x")); + + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + nsresult rv = GetParagraphFormatNodes(arrayOfNodes, TouchContent::no); + NS_ENSURE_SUCCESS(rv, rv); + + // post process list. We need to replace any block nodes that are not format + // nodes with their content. This is so we only have to look "up" the hierarchy + // to find format nodes, instead of both up and down. + for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) { + auto& curNode = arrayOfNodes[i]; + nsAutoString format; + // if it is a known format node we have it easy + if (IsBlockNode(curNode) && !HTMLEditUtils::IsFormatNode(curNode)) { + // arrayOfNodes.RemoveObject(curNode); + rv = AppendInnerFormatNodes(arrayOfNodes, curNode); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // we might have an empty node list. if so, find selection parent + // and put that on the list + if (arrayOfNodes.IsEmpty()) { + nsCOMPtr<nsINode> selNode; + int32_t selOffset; + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<Selection> selection = mHTMLEditor->GetSelection(); + NS_ENSURE_STATE(selection); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->GetStartNodeAndOffset(selection, getter_AddRefs(selNode), + &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(selNode, NS_ERROR_NULL_POINTER); + arrayOfNodes.AppendElement(*selNode); + } + + // remember root node + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIDOMElement> rootElem = do_QueryInterface(mHTMLEditor->GetRoot()); + NS_ENSURE_TRUE(rootElem, NS_ERROR_NULL_POINTER); + + // loop through the nodes in selection and examine their paragraph format + for (auto& curNode : Reversed(arrayOfNodes)) { + nsAutoString format; + // if it is a known format node we have it easy + if (HTMLEditUtils::IsFormatNode(curNode)) { + GetFormatString(GetAsDOMNode(curNode), format); + } else if (IsBlockNode(curNode)) { + // this is a div or some other non-format block. + // we should ignore it. Its children were appended to this list + // by AppendInnerFormatNodes() call above. We will get needed + // info when we examine them instead. + continue; + } else { + nsCOMPtr<nsIDOMNode> node, tmp = GetAsDOMNode(curNode); + tmp->GetParentNode(getter_AddRefs(node)); + while (node) { + if (node == rootElem) { + format.Truncate(0); + break; + } else if (HTMLEditUtils::IsFormatNode(node)) { + GetFormatString(node, format); + break; + } + // else keep looking up + tmp = node; + tmp->GetParentNode(getter_AddRefs(node)); + } + } + + // if this is the first node, we've found, remember it as the format + if (formatStr.EqualsLiteral("x")) { + formatStr = format; + } + // else make sure it matches previously found format + else if (format != formatStr) { + bMixed = true; + break; + } + } + + *aMixed = bMixed; + outFormat = formatStr; + return NS_OK; +} + +nsresult +HTMLEditRules::AppendInnerFormatNodes(nsTArray<OwningNonNull<nsINode>>& aArray, + nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + // we only need to place any one inline inside this node onto + // the list. They are all the same for purposes of determining + // paragraph style. We use foundInline to track this as we are + // going through the children in the loop below. + bool foundInline = false; + for (nsIContent* child = aNode->GetFirstChild(); + child; + child = child->GetNextSibling()) { + bool isBlock = IsBlockNode(*child); + bool isFormat = HTMLEditUtils::IsFormatNode(child); + if (isBlock && !isFormat) { + // if it's a div, etc., recurse + AppendInnerFormatNodes(aArray, child); + } else if (isFormat) { + aArray.AppendElement(*child); + } else if (!foundInline) { + // if this is the first inline we've found, use it + foundInline = true; + aArray.AppendElement(*child); + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::GetFormatString(nsIDOMNode* aNode, + nsAString& outFormat) +{ + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + + if (HTMLEditUtils::IsFormatNode(aNode)) { + nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(aNode); + atom->ToString(outFormat); + } else { + outFormat.Truncate(); + } + return NS_OK; +} + +void +HTMLEditRules::WillInsert(Selection& aSelection, + bool* aCancel) +{ + MOZ_ASSERT(aCancel); + + TextEditRules::WillInsert(aSelection, aCancel); + + NS_ENSURE_TRUE_VOID(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Adjust selection to prevent insertion after a moz-BR. This next only + // works for collapsed selections right now, because selection is a pain to + // work with when not collapsed. (no good way to extend start or end of + // selection), so we ignore those types of selections. + if (!aSelection.Collapsed()) { + return; + } + + // If we are after a mozBR in the same block, then move selection to be + // before it + NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) && + aSelection.GetRangeAt(0)->GetStartParent()); + OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent(); + int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset(); + + // Get prior node + nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorHTMLNode(selNode, + selOffset); + if (priorNode && TextEditUtils::IsMozBR(priorNode)) { + nsCOMPtr<Element> block1 = htmlEditor->GetBlock(selNode); + nsCOMPtr<Element> block2 = htmlEditor->GetBlockNodeParent(priorNode); + + if (block1 && block1 == block2) { + // If we are here then the selection is right after a mozBR that is in + // the same block as the selection. We need to move the selection start + // to be before the mozBR. + selNode = priorNode->GetParentNode(); + selOffset = selNode->IndexOf(priorNode); + nsresult rv = aSelection.Collapse(selNode, selOffset); + NS_ENSURE_SUCCESS_VOID(rv); + } + } + + if (mDidDeleteSelection && + (mTheAction == EditAction::insertText || + mTheAction == EditAction::insertIMEText || + mTheAction == EditAction::deleteSelection)) { + nsresult rv = ReapplyCachedStyles(); + NS_ENSURE_SUCCESS_VOID(rv); + } + // For most actions we want to clear the cached styles, but there are + // exceptions + if (!IsStyleCachePreservingAction(mTheAction)) { + ClearCachedStyles(); + } +} + +nsresult +HTMLEditRules::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; + // If the selection isn't collapsed, delete it. Don't delete existing inline + // tags, because we're hopefully going to insert text (bug 787432). + if (!aSelection->Collapsed()) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eNoStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + WillInsert(*aSelection, aCancel); + // initialize out param + // we want to ignore result of WillInsert() + *aCancel = false; + + // we need to get the doc + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIDocument> doc = mHTMLEditor->GetDocument(); + NS_ENSURE_STATE(doc); + + // for every property that is set, insert a new inline style node + nsresult rv = CreateStyleForInsertText(*aSelection, *doc); + NS_ENSURE_SUCCESS(rv, rv); + + // get the (collapsed) selection location + NS_ENSURE_STATE(mHTMLEditor); + NS_ENSURE_STATE(aSelection->GetRangeAt(0)); + nsCOMPtr<nsINode> selNode = aSelection->GetRangeAt(0)->GetStartParent(); + int32_t selOffset = aSelection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_STATE(selNode); + + // dont put text in places that can't have it + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsTextNode(selNode) && + (!mHTMLEditor || !mHTMLEditor->CanContainTag(*selNode, + *nsGkAtoms::textTagName))) { + return NS_ERROR_FAILURE; + } + + if (aAction == EditAction::insertIMEText) { + // Right now the WSRunObject code bails on empty strings, but IME needs + // the InsertTextImpl() call to still happen since empty strings are meaningful there. + NS_ENSURE_STATE(mHTMLEditor); + // If there is one or more IME selections, its minimum offset should be + // the insertion point. + int32_t IMESelectionOffset = + mHTMLEditor->GetIMESelectionStartOffsetIn(selNode); + if (IMESelectionOffset >= 0) { + selOffset = IMESelectionOffset; + } + if (inString->IsEmpty()) { + rv = mHTMLEditor->InsertTextImpl(*inString, address_of(selNode), + &selOffset, doc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + WSRunObject wsObj(mHTMLEditor, selNode, selOffset); + rv = wsObj.InsertText(*inString, address_of(selNode), &selOffset, doc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + // aAction == kInsertText + else { + // find where we are + nsCOMPtr<nsINode> curNode = selNode; + int32_t curOffset = selOffset; + + // is our text going to be PREformatted? + // We remember this so that we know how to handle tabs. + bool isPRE; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->IsPreformatted(GetAsDOMNode(selNode), &isPRE); + NS_ENSURE_SUCCESS(rv, rv); + + // turn off the edit listener: we know how to + // build the "doc changed range" ourselves, and it's + // must faster to do it once here than to track all + // the changes one at a time. + AutoLockListener lockit(&mListenerEnabled); + + // don't spaz my selection in subtransactions + NS_ENSURE_STATE(mHTMLEditor); + AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); + nsAutoString tString(*inString); + const char16_t *unicodeBuf = tString.get(); + int32_t pos = 0; + NS_NAMED_LITERAL_STRING(newlineStr, LFSTR); + + // for efficiency, break out the pre case separately. This is because + // its a lot cheaper to search the input string for only newlines than + // it is to search for both tabs and newlines. + if (isPRE || IsPlaintextEditor()) { + while (unicodeBuf && pos != -1 && + pos < static_cast<int32_t>(inString->Length())) { + int32_t oldPos = pos; + int32_t subStrLen; + pos = tString.FindChar(nsCRT::LF, oldPos); + + if (pos != -1) { + subStrLen = pos - oldPos; + // if first char is newline, then use just it + if (!subStrLen) { + subStrLen = 1; + } + } else { + subStrLen = tString.Length() - oldPos; + pos = tString.Length(); + } + + nsDependentSubstring subStr(tString, oldPos, subStrLen); + + // is it a return? + if (subStr.Equals(newlineStr)) { + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> br = + mHTMLEditor->CreateBRImpl(address_of(curNode), &curOffset, + nsIEditor::eNone); + NS_ENSURE_STATE(br); + pos++; + } else { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->InsertTextImpl(subStr, address_of(curNode), + &curOffset, doc); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } else { + NS_NAMED_LITERAL_STRING(tabStr, "\t"); + NS_NAMED_LITERAL_STRING(spacesStr, " "); + char specialChars[] = {TAB, nsCRT::LF, 0}; + while (unicodeBuf && pos != -1 && + pos < static_cast<int32_t>(inString->Length())) { + int32_t oldPos = pos; + int32_t subStrLen; + pos = tString.FindCharInSet(specialChars, oldPos); + + if (pos != -1) { + subStrLen = pos - oldPos; + // if first char is newline, then use just it + if (!subStrLen) { + subStrLen = 1; + } + } else { + subStrLen = tString.Length() - oldPos; + pos = tString.Length(); + } + + nsDependentSubstring subStr(tString, oldPos, subStrLen); + NS_ENSURE_STATE(mHTMLEditor); + WSRunObject wsObj(mHTMLEditor, curNode, curOffset); + + // is it a tab? + if (subStr.Equals(tabStr)) { + rv = + wsObj.InsertText(spacesStr, address_of(curNode), &curOffset, doc); + NS_ENSURE_SUCCESS(rv, rv); + pos++; + } + // is it a return? + else if (subStr.Equals(newlineStr)) { + nsCOMPtr<Element> br = wsObj.InsertBreak(address_of(curNode), + &curOffset, + nsIEditor::eNone); + NS_ENSURE_TRUE(br, NS_ERROR_FAILURE); + pos++; + } else { + rv = wsObj.InsertText(subStr, address_of(curNode), &curOffset, doc); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + aSelection->SetInterlinePosition(false); + if (curNode) aSelection->Collapse(curNode, curOffset); + // manually update the doc changed range so that AfterEdit will clean up + // the correct portion of the document. + if (!mDocChangeRange) { + mDocChangeRange = new nsRange(selNode); + } + rv = mDocChangeRange->SetStart(selNode, selOffset); + NS_ENSURE_SUCCESS(rv, rv); + + if (curNode) { + rv = mDocChangeRange->SetEnd(curNode, curOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = mDocChangeRange->SetEnd(selNode, selOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::WillLoadHTML(Selection* aSelection, + bool* aCancel) +{ + NS_ENSURE_TRUE(aSelection && aCancel, NS_ERROR_NULL_POINTER); + + *aCancel = false; + + // Delete mBogusNode if it exists. If we really need one, + // it will be added during post-processing in AfterEditInner(). + + if (mBogusNode) { + mTextEditor->DeleteNode(mBogusNode); + mBogusNode = nullptr; + } + + return NS_OK; +} + +nsresult +HTMLEditRules::WillInsertBreak(Selection& aSelection, + bool* aCancel, + bool* aHandled) +{ + MOZ_ASSERT(aCancel && aHandled); + *aCancel = false; + *aHandled = false; + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // If the selection isn't collapsed, delete it. + if (!aSelection.Collapsed()) { + nsresult rv = + htmlEditor->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; + + // Split any mailcites in the way. Should we abort this if we encounter + // table cell boundaries? + if (IsMailEditor()) { + nsresult rv = SplitMailCites(&aSelection, aHandled); + NS_ENSURE_SUCCESS(rv, rv); + if (*aHandled) { + return NS_OK; + } + } + + // Smart splitting rules + NS_ENSURE_TRUE(aSelection.GetRangeAt(0) && + aSelection.GetRangeAt(0)->GetStartParent(), + NS_ERROR_FAILURE); + OwningNonNull<nsINode> node = *aSelection.GetRangeAt(0)->GetStartParent(); + int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); + + // Do nothing if the node is read-only + if (!htmlEditor->IsModifiableNode(node)) { + *aCancel = true; + return NS_OK; + } + + // Identify the block + nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(node); + NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE); + + // If the active editing host is an inline element, or if the active editing + // host is the block parent itself, just append a br. + nsCOMPtr<Element> host = htmlEditor->GetActiveEditingHost(); + if (!EditorUtils::IsDescendantOf(blockParent, host)) { + nsresult rv = StandardBreakImpl(node, offset, aSelection); + NS_ENSURE_SUCCESS(rv, rv); + *aHandled = true; + return NS_OK; + } + + // If block is empty, populate with br. (For example, imagine a div that + // contains the word "text". The user selects "text" and types return. + // "Text" is deleted leaving an empty block. We want to put in one br to + // make block have a line. Then code further below will put in a second br.) + bool isEmpty; + IsEmptyBlock(*blockParent, &isEmpty); + if (isEmpty) { + nsCOMPtr<Element> br = htmlEditor->CreateBR(blockParent, + blockParent->Length()); + NS_ENSURE_STATE(br); + } + + nsCOMPtr<Element> listItem = IsInListItem(blockParent); + if (listItem && listItem != host) { + ReturnInListItem(aSelection, *listItem, node, offset); + *aHandled = true; + return NS_OK; + } else if (HTMLEditUtils::IsHeader(*blockParent)) { + // Headers: close (or split) header + ReturnInHeader(aSelection, *blockParent, node, offset); + *aHandled = true; + return NS_OK; + } else if (blockParent->IsHTMLElement(nsGkAtoms::p)) { + // Paragraphs: special rules to look for <br>s + nsresult rv = + ReturnInParagraph(&aSelection, GetAsDOMNode(blockParent), + GetAsDOMNode(node), offset, aCancel, aHandled); + NS_ENSURE_SUCCESS(rv, rv); + // Fall through, we may not have handled it in ReturnInParagraph() + } + + // If not already handled then do the standard thing + if (!(*aHandled)) { + *aHandled = true; + return StandardBreakImpl(node, offset, aSelection); + } + return NS_OK; +} + +nsresult +HTMLEditRules::StandardBreakImpl(nsINode& aNode, + int32_t aOffset, + Selection& aSelection) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + nsCOMPtr<Element> brNode; + bool bAfterBlock = false; + bool bBeforeBlock = false; + nsCOMPtr<nsINode> node = &aNode; + + if (IsPlaintextEditor()) { + brNode = htmlEditor->CreateBR(node, aOffset); + NS_ENSURE_STATE(brNode); + } else { + WSRunObject wsObj(htmlEditor, node, aOffset); + int32_t visOffset = 0; + WSType wsType; + nsCOMPtr<nsINode> visNode; + wsObj.PriorVisibleNode(node, aOffset, address_of(visNode), + &visOffset, &wsType); + if (wsType & WSType::block) { + bAfterBlock = true; + } + wsObj.NextVisibleNode(node, aOffset, address_of(visNode), + &visOffset, &wsType); + if (wsType & WSType::block) { + bBeforeBlock = true; + } + nsCOMPtr<nsIDOMNode> linkDOMNode; + if (htmlEditor->IsInLink(GetAsDOMNode(node), address_of(linkDOMNode))) { + // Split the link + nsCOMPtr<Element> linkNode = do_QueryInterface(linkDOMNode); + NS_ENSURE_STATE(linkNode || !linkDOMNode); + nsCOMPtr<nsINode> linkParent = linkNode->GetParentNode(); + aOffset = htmlEditor->SplitNodeDeep(*linkNode, *node->AsContent(), + aOffset, + HTMLEditor::EmptyContainers::no); + NS_ENSURE_STATE(aOffset != -1); + node = linkParent; + } + brNode = wsObj.InsertBreak(address_of(node), &aOffset, nsIEditor::eNone); + NS_ENSURE_TRUE(brNode, NS_ERROR_FAILURE); + } + node = brNode->GetParentNode(); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + int32_t offset = node->IndexOf(brNode); + if (bAfterBlock && bBeforeBlock) { + // We just placed a br between block boundaries. This is the one case + // where we want the selection to be before the br we just placed, as the + // br will be on a new line, rather than at end of prior line. + aSelection.SetInterlinePosition(true); + nsresult rv = aSelection.Collapse(node, offset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + WSRunObject wsObj(htmlEditor, node, offset + 1); + nsCOMPtr<nsINode> secondBR; + int32_t visOffset = 0; + WSType wsType; + wsObj.NextVisibleNode(node, offset + 1, address_of(secondBR), + &visOffset, &wsType); + if (wsType == WSType::br) { + // The next thing after the break we inserted is another break. Move the + // second break to be the first break's sibling. This will prevent them + // from being in different inline nodes, which would break + // SetInterlinePosition(). It will also assure that if the user clicks + // away and then clicks back on their new blank line, they will still get + // the style from the line above. + nsCOMPtr<nsINode> brParent = secondBR->GetParentNode(); + int32_t brOffset = brParent ? brParent->IndexOf(secondBR) : -1; + if (brParent != node || brOffset != offset + 1) { + nsresult rv = + htmlEditor->MoveNode(secondBR->AsContent(), node, offset + 1); + NS_ENSURE_SUCCESS(rv, rv); + } + } + // SetInterlinePosition(true) means we want the caret to stick to the + // content on the "right". We want the caret to stick to whatever is past + // the break. This is because the break is on the same line we were on, + // but the next content will be on the following line. + + // An exception to this is if the break has a next sibling that is a block + // node. Then we stick to the left to avoid an uber caret. + nsCOMPtr<nsIContent> siblingNode = brNode->GetNextSibling(); + if (siblingNode && IsBlockNode(*siblingNode)) { + aSelection.SetInterlinePosition(false); + } else { + aSelection.SetInterlinePosition(true); + } + nsresult rv = aSelection.Collapse(node, offset + 1); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult +HTMLEditRules::DidInsertBreak(Selection* aSelection, + nsresult aResult) +{ + return NS_OK; +} + +nsresult +HTMLEditRules::SplitMailCites(Selection* aSelection, + bool* aHandled) +{ + NS_ENSURE_TRUE(aSelection && aHandled, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIContent> leftCite, rightCite; + nsCOMPtr<nsINode> selNode; + nsCOMPtr<Element> citeNode; + int32_t selOffset; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(selNode), + &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + citeNode = GetTopEnclosingMailCite(*selNode); + if (citeNode) { + // If our selection is just before a break, nudge it to be + // just after it. This does two things for us. It saves us the trouble of having to add + // a break here ourselves to preserve the "blockness" of the inline span mailquote + // (in the inline case), and : + // it means the break won't end up making an empty line that happens to be inside a + // mailquote (in either inline or block case). + // The latter can confuse a user if they click there and start typing, + // because being in the mailquote may affect wrapping behavior, or font color, etc. + NS_ENSURE_STATE(mHTMLEditor); + WSRunObject wsObj(mHTMLEditor, selNode, selOffset); + nsCOMPtr<nsINode> visNode; + int32_t visOffset=0; + WSType wsType; + wsObj.NextVisibleNode(selNode, selOffset, address_of(visNode), + &visOffset, &wsType); + if (wsType == WSType::br) { + // ok, we are just before a break. is it inside the mailquote? + if (visNode != citeNode && citeNode->Contains(visNode)) { + // it is. so lets reset our selection to be just after it. + NS_ENSURE_STATE(mHTMLEditor); + selNode = mHTMLEditor->GetNodeLocation(visNode, &selOffset); + ++selOffset; + } + } + + NS_ENSURE_STATE(mHTMLEditor); + NS_ENSURE_STATE(selNode->IsContent()); + int32_t newOffset = mHTMLEditor->SplitNodeDeep(*citeNode, + *selNode->AsContent(), selOffset, HTMLEditor::EmptyContainers::no, + getter_AddRefs(leftCite), getter_AddRefs(rightCite)); + NS_ENSURE_STATE(newOffset != -1); + + // Add an invisible <br> to the end of the left part if it was a <span> of + // style="display: block". This is important, since when serialising the + // cite to plain text, the span which caused the visual break is discarded. + // So the added <br> will guarantee that the serialiser will insert a + // break where the user saw one. + if (leftCite && + leftCite->IsHTMLElement(nsGkAtoms::span) && + leftCite->GetPrimaryFrame()->IsFrameOfType(nsIFrame::eBlockFrame)) { + nsCOMPtr<nsINode> lastChild = leftCite->GetLastChild(); + if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) { + // We ignore the result here. + nsCOMPtr<Element> invisBR = + mHTMLEditor->CreateBR(leftCite, leftCite->Length()); + } + } + + selNode = citeNode->GetParentNode(); + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(selNode, newOffset); + NS_ENSURE_STATE(brNode); + + // want selection before the break, and on same line + aSelection->SetInterlinePosition(true); + rv = aSelection->Collapse(selNode, newOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // if citeNode wasn't a block, we might also want another break before it. + // We need to examine the content both before the br we just added and also + // just after it. If we don't have another br or block boundary adjacent, + // then we will need a 2nd br added to achieve blank line that user expects. + if (IsInlineNode(*citeNode)) { + NS_ENSURE_STATE(mHTMLEditor); + WSRunObject wsObj(mHTMLEditor, selNode, newOffset); + nsCOMPtr<nsINode> visNode; + int32_t visOffset=0; + WSType wsType; + wsObj.PriorVisibleNode(selNode, newOffset, address_of(visNode), + &visOffset, &wsType); + if (wsType == WSType::normalWS || wsType == WSType::text || + wsType == WSType::special) { + NS_ENSURE_STATE(mHTMLEditor); + WSRunObject wsObjAfterBR(mHTMLEditor, selNode, newOffset+1); + wsObjAfterBR.NextVisibleNode(selNode, newOffset + 1, + address_of(visNode), &visOffset, &wsType); + if (wsType == WSType::normalWS || wsType == WSType::text || + wsType == WSType::special || + // In case we're at the very end. + wsType == WSType::thisBlock) { + NS_ENSURE_STATE(mHTMLEditor); + brNode = mHTMLEditor->CreateBR(selNode, newOffset); + NS_ENSURE_STATE(brNode); + } + } + } + + // delete any empty cites + bool bEmptyCite = false; + if (leftCite) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->IsEmptyNode(leftCite, &bEmptyCite, true, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (bEmptyCite) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(leftCite); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + if (rightCite) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->IsEmptyNode(rightCite, &bEmptyCite, true, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (bEmptyCite) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(rightCite); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + *aHandled = true; + } + return NS_OK; +} + + +nsresult +HTMLEditRules::WillDeleteSelection(Selection* aSelection, + nsIEditor::EDirection aAction, + nsIEditor::EStripWrappers aStripWrappers, + bool* aCancel, + bool* aHandled) +{ + MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip || + aStripWrappers == nsIEditor::eNoStrip); + + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + // Initialize out params + *aCancel = false; + *aHandled = false; + + // Remember that we did a selection deletion. Used by CreateStyleForInsertText() + mDidDeleteSelection = true; + + // If there is only bogus content, cancel the operation + if (mBogusNode) { + *aCancel = true; + return NS_OK; + } + + // First check for table selection mode. If so, hand off to table editor. + nsCOMPtr<nsIDOMElement> cell; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetFirstSelectedCell(nullptr, getter_AddRefs(cell)); + if (NS_SUCCEEDED(rv) && cell) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteTableCellContents(); + *aHandled = true; + return rv; + } + cell = nullptr; + + // origCollapsed is used later to determine whether we should join blocks. We + // don't really care about bCollapsed because it will be modified by + // ExtendSelectionForDelete later. JoinBlocks should happen if the original + // selection is collapsed and the cursor is at the end of a block element, in + // which case ExtendSelectionForDelete would always make the selection not + // collapsed. + bool bCollapsed = aSelection->Collapsed(); + bool join = false; + bool origCollapsed = bCollapsed; + + nsCOMPtr<nsINode> selNode; + int32_t selOffset; + + NS_ENSURE_STATE(aSelection->GetRangeAt(0)); + nsCOMPtr<nsINode> startNode = aSelection->GetRangeAt(0)->GetStartParent(); + int32_t startOffset = aSelection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); + + if (bCollapsed) { + // If we are inside an empty block, delete it. + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> host = mHTMLEditor->GetActiveEditingHost(); + NS_ENSURE_TRUE(host, NS_ERROR_FAILURE); + rv = CheckForEmptyBlock(startNode, host, aSelection, aAction, aHandled); + NS_ENSURE_SUCCESS(rv, rv); + if (*aHandled) { + return NS_OK; + } + + // Test for distance between caret and text that will be deleted + rv = CheckBidiLevelForDeletion(aSelection, GetAsDOMNode(startNode), + startOffset, aAction, aCancel); + NS_ENSURE_SUCCESS(rv, rv); + if (*aCancel) { + return NS_OK; + } + + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->ExtendSelectionForDelete(aSelection, &aAction); + NS_ENSURE_SUCCESS(rv, rv); + + // We should delete nothing. + if (aAction == nsIEditor::eNone) { + return NS_OK; + } + + // ExtendSelectionForDelete() may have changed the selection, update it + NS_ENSURE_STATE(aSelection->GetRangeAt(0)); + startNode = aSelection->GetRangeAt(0)->GetStartParent(); + startOffset = aSelection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); + + bCollapsed = aSelection->Collapsed(); + } + + if (bCollapsed) { + // What's in the direction we are deleting? + NS_ENSURE_STATE(mHTMLEditor); + WSRunObject wsObj(mHTMLEditor, startNode, startOffset); + nsCOMPtr<nsINode> visNode; + int32_t visOffset; + WSType wsType; + + // Find next visible node + if (aAction == nsIEditor::eNext) { + wsObj.NextVisibleNode(startNode, startOffset, address_of(visNode), + &visOffset, &wsType); + } else { + wsObj.PriorVisibleNode(startNode, startOffset, address_of(visNode), + &visOffset, &wsType); + } + + if (!visNode) { + // Can't find anything to delete! + *aCancel = true; + // XXX This is the result of mHTMLEditor->GetFirstSelectedCell(). + // The value could be both an error and NS_OK. + return rv; + } + + if (wsType == WSType::normalWS) { + // We found some visible ws to delete. Let ws code handle it. + *aHandled = true; + if (aAction == nsIEditor::eNext) { + rv = wsObj.DeleteWSForward(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = wsObj.DeleteWSBackward(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + return InsertBRIfNeeded(aSelection); + } + + if (wsType == WSType::text) { + // Found normal text to delete. + OwningNonNull<Text> nodeAsText = *visNode->GetAsText(); + int32_t so = visOffset; + int32_t eo = visOffset + 1; + if (aAction == nsIEditor::ePrevious) { + if (!so) { + return NS_ERROR_UNEXPECTED; + } + so--; + eo--; + // Bug 1068979: delete both codepoints if surrogate pair + if (so > 0) { + const nsTextFragment *text = nodeAsText->GetText(); + if (NS_IS_LOW_SURROGATE(text->CharAt(so)) && + NS_IS_HIGH_SURROGATE(text->CharAt(so - 1))) { + so--; + } + } + } else { + RefPtr<nsRange> range = aSelection->GetRangeAt(0); + NS_ENSURE_STATE(range); + + NS_ASSERTION(range->GetStartParent() == visNode, + "selection start not in visNode"); + NS_ASSERTION(range->GetEndParent() == visNode, + "selection end not in visNode"); + + so = range->StartOffset(); + eo = range->EndOffset(); + } + NS_ENSURE_STATE(mHTMLEditor); + rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(visNode), + &so, address_of(visNode), &eo); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + *aHandled = true; + rv = mHTMLEditor->DeleteText(nodeAsText, std::min(so, eo), + DeprecatedAbs(eo - so)); + NS_ENSURE_SUCCESS(rv, rv); + + // XXX When Backspace key is pressed, Chromium removes following empty + // text nodes when removing the last character of the non-empty text + // node. However, Edge never removes empty text nodes even if + // selection is in the following empty text node(s). For now, we + // should keep our traditional behavior same as Edge for backward + // compatibility. + // XXX When Delete key is pressed, Edge removes all preceding empty + // text nodes when removing the first character of the non-empty + // text node. Chromium removes only selected empty text node and + // following empty text nodes and the first character of the + // non-empty text node. For now, we should keep our traditional + // behavior same as Chromium for backward compatibility. + + DeleteNodeIfCollapsedText(nodeAsText); + + rv = InsertBRIfNeeded(aSelection); + NS_ENSURE_SUCCESS(rv, rv); + + // Remember that we did a ranged delete for the benefit of + // AfterEditInner(). + mDidRangedDelete = true; + + return NS_OK; + } + + if (wsType == WSType::special || wsType == WSType::br || + visNode->IsHTMLElement(nsGkAtoms::hr)) { + // Short circuit for invisible breaks. delete them and recurse. + if (visNode->IsHTMLElement(nsGkAtoms::br) && + (!mHTMLEditor || !mHTMLEditor->IsVisBreak(visNode))) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(visNode); + NS_ENSURE_SUCCESS(rv, rv); + return WillDeleteSelection(aSelection, aAction, aStripWrappers, + aCancel, aHandled); + } + + // Special handling for backspace when positioned after <hr> + if (aAction == nsIEditor::ePrevious && + visNode->IsHTMLElement(nsGkAtoms::hr)) { + // Only if the caret is positioned at the end-of-hr-line position, we + // want to delete the <hr>. + // + // In other words, we only want to delete, if our selection position + // (indicated by startNode and startOffset) is the position directly + // after the <hr>, on the same line as the <hr>. + // + // To detect this case we check: + // startNode == parentOfVisNode + // and + // startOffset -1 == visNodeOffsetToVisNodeParent + // and + // interline position is false (left) + // + // In any other case we set the position to startnode -1 and + // interlineposition to false, only moving the caret to the + // end-of-hr-line position. + bool moveOnly = true; + + selNode = visNode->GetParentNode(); + selOffset = selNode ? selNode->IndexOf(visNode) : -1; + + bool interLineIsRight; + rv = aSelection->GetInterlinePosition(&interLineIsRight); + NS_ENSURE_SUCCESS(rv, rv); + + if (startNode == selNode && startOffset - 1 == selOffset && + !interLineIsRight) { + moveOnly = false; + } + + if (moveOnly) { + // Go to the position after the <hr>, but to the end of the <hr> line + // by setting the interline position to left. + ++selOffset; + aSelection->Collapse(selNode, selOffset); + aSelection->SetInterlinePosition(false); + mDidExplicitlySetInterline = true; + *aHandled = true; + + // There is one exception to the move only case. If the <hr> is + // followed by a <br> we want to delete the <br>. + + WSType otherWSType; + nsCOMPtr<nsINode> otherNode; + int32_t otherOffset; + + wsObj.NextVisibleNode(startNode, startOffset, address_of(otherNode), + &otherOffset, &otherWSType); + + if (otherWSType == WSType::br) { + // Delete the <br> + + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIContent> otherContent(do_QueryInterface(otherNode)); + rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, otherContent); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(otherNode); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + // Else continue with normal delete code + } + + // Found break or image, or hr. + NS_ENSURE_STATE(mHTMLEditor); + NS_ENSURE_STATE(visNode->IsContent()); + rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, visNode->AsContent()); + NS_ENSURE_SUCCESS(rv, rv); + // Remember sibling to visnode, if any + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIContent> sibling = mHTMLEditor->GetPriorHTMLSibling(visNode); + // Delete the node, and join like nodes if appropriate + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(visNode); + NS_ENSURE_SUCCESS(rv, rv); + // We did something, so let's say so. + *aHandled = true; + // Is there a prior node and are they siblings? + nsCOMPtr<nsINode> stepbrother; + if (sibling) { + NS_ENSURE_STATE(mHTMLEditor); + stepbrother = mHTMLEditor->GetNextHTMLSibling(sibling); + } + // Are they both text nodes? If so, join them! + if (startNode == stepbrother && startNode->GetAsText() && + sibling->GetAsText()) { + EditorDOMPoint pt = JoinNodesSmart(*sibling, *startNode->AsContent()); + NS_ENSURE_STATE(pt.node); + // Fix up selection + rv = aSelection->Collapse(pt.node, pt.offset); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = InsertBRIfNeeded(aSelection); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + if (wsType == WSType::otherBlock) { + // Make sure it's not a table element. If so, cancel the operation + // (translation: users cannot backspace or delete across table cells) + if (HTMLEditUtils::IsTableElement(visNode)) { + *aCancel = true; + return NS_OK; + } + + // Next to a block. See if we are between a block and a br. If so, we + // really want to delete the br. Else join content at selection to the + // block. + bool bDeletedBR = false; + WSType otherWSType; + nsCOMPtr<nsINode> otherNode; + int32_t otherOffset; + + // Find node in other direction + if (aAction == nsIEditor::eNext) { + wsObj.PriorVisibleNode(startNode, startOffset, address_of(otherNode), + &otherOffset, &otherWSType); + } else { + wsObj.NextVisibleNode(startNode, startOffset, address_of(otherNode), + &otherOffset, &otherWSType); + } + + // First find the adjacent node in the block + nsCOMPtr<nsIContent> leafNode; + nsCOMPtr<nsINode> leftNode, rightNode; + if (aAction == nsIEditor::ePrevious) { + NS_ENSURE_STATE(mHTMLEditor); + leafNode = mHTMLEditor->GetLastEditableLeaf(*visNode); + leftNode = leafNode; + rightNode = startNode; + } else { + NS_ENSURE_STATE(mHTMLEditor); + leafNode = mHTMLEditor->GetFirstEditableLeaf(*visNode); + leftNode = startNode; + rightNode = leafNode; + } + + if (otherNode->IsHTMLElement(nsGkAtoms::br)) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(otherNode); + NS_ENSURE_SUCCESS(rv, rv); + // XXX Only in this case, setting "handled" to true only when it + // succeeds? + *aHandled = true; + bDeletedBR = true; + } + + // Don't cross table boundaries + if (leftNode && rightNode && + InDifferentTableElements(leftNode, rightNode)) { + return NS_OK; + } + + if (bDeletedBR) { + // Put selection at edge of block and we are done. + NS_ENSURE_STATE(leafNode); + EditorDOMPoint newSel = GetGoodSelPointForNode(*leafNode, aAction); + NS_ENSURE_STATE(newSel.node); + aSelection->Collapse(newSel.node, newSel.offset); + return NS_OK; + } + + // Else we are joining content to block + + nsCOMPtr<nsINode> selPointNode = startNode; + int32_t selPointOffset = startOffset; + { + NS_ENSURE_STATE(mHTMLEditor); + AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, + address_of(selPointNode), &selPointOffset); + NS_ENSURE_STATE(leftNode && leftNode->IsContent() && + rightNode && rightNode->IsContent()); + *aHandled = true; + rv = JoinBlocks(*leftNode->AsContent(), *rightNode->AsContent(), + aCancel); + NS_ENSURE_SUCCESS(rv, rv); + } + aSelection->Collapse(selPointNode, selPointOffset); + return NS_OK; + } + + if (wsType == WSType::thisBlock) { + // At edge of our block. Look beside it and see if we can join to an + // adjacent block + + // Make sure it's not a table element. If so, cancel the operation + // (translation: users cannot backspace or delete across table cells) + if (HTMLEditUtils::IsTableElement(visNode)) { + *aCancel = true; + return NS_OK; + } + + // First find the relevant nodes + nsCOMPtr<nsINode> leftNode, rightNode; + if (aAction == nsIEditor::ePrevious) { + NS_ENSURE_STATE(mHTMLEditor); + leftNode = mHTMLEditor->GetPriorHTMLNode(visNode); + rightNode = startNode; + } else { + NS_ENSURE_STATE(mHTMLEditor); + rightNode = mHTMLEditor->GetNextHTMLNode(visNode); + leftNode = startNode; + } + + // Nothing to join + if (!leftNode || !rightNode) { + *aCancel = true; + return NS_OK; + } + + // Don't cross table boundaries -- cancel it + if (InDifferentTableElements(leftNode, rightNode)) { + *aCancel = true; + return NS_OK; + } + + nsCOMPtr<nsINode> selPointNode = startNode; + int32_t selPointOffset = startOffset; + { + NS_ENSURE_STATE(mHTMLEditor); + AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, + address_of(selPointNode), &selPointOffset); + NS_ENSURE_STATE(leftNode->IsContent() && rightNode->IsContent()); + *aHandled = true; + rv = JoinBlocks(*leftNode->AsContent(), *rightNode->AsContent(), + aCancel); + NS_ENSURE_SUCCESS(rv, rv); + } + aSelection->Collapse(selPointNode, selPointOffset); + return NS_OK; + } + } + + + // Else we have a non-collapsed selection. First adjust the selection. + rv = ExpandSelectionForDeletion(*aSelection); + NS_ENSURE_SUCCESS(rv, rv); + + // Remember that we did a ranged delete for the benefit of AfterEditInner(). + mDidRangedDelete = true; + + // Refresh start and end points + NS_ENSURE_STATE(aSelection->GetRangeAt(0)); + startNode = aSelection->GetRangeAt(0)->GetStartParent(); + startOffset = aSelection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); + nsCOMPtr<nsINode> endNode = aSelection->GetRangeAt(0)->GetEndParent(); + int32_t endOffset = aSelection->GetRangeAt(0)->EndOffset(); + NS_ENSURE_TRUE(endNode, NS_ERROR_FAILURE); + + // Figure out if the endpoints are in nodes that can be merged. Adjust + // surrounding whitespace in preparation to delete selection. + if (!IsPlaintextEditor()) { + NS_ENSURE_STATE(mHTMLEditor); + AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); + rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, + address_of(startNode), &startOffset, + address_of(endNode), &endOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + { + // Track location of where we are deleting + NS_ENSURE_STATE(mHTMLEditor); + AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater, + address_of(startNode), &startOffset); + AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater, + address_of(endNode), &endOffset); + // We are handling all ranged deletions directly now. + *aHandled = true; + + if (endNode == startNode) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Figure out mailcite ancestors + nsCOMPtr<Element> startCiteNode = GetTopEnclosingMailCite(*startNode); + nsCOMPtr<Element> endCiteNode = GetTopEnclosingMailCite(*endNode); + + // If we only have a mailcite at one of the two endpoints, set the + // directionality of the deletion so that the selection will end up + // outside the mailcite. + if (startCiteNode && !endCiteNode) { + aAction = nsIEditor::eNext; + } else if (!startCiteNode && endCiteNode) { + aAction = nsIEditor::ePrevious; + } + + // Figure out block parents + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> leftParent = mHTMLEditor->GetBlock(*startNode); + nsCOMPtr<Element> rightParent = mHTMLEditor->GetBlock(*endNode); + + // Are endpoint block parents the same? Use default deletion + if (leftParent && leftParent == rightParent) { + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers); + } else { + // Deleting across blocks. Are the blocks of same type? + NS_ENSURE_STATE(leftParent && rightParent); + + // Are the blocks siblings? + nsCOMPtr<nsINode> leftBlockParent = leftParent->GetParentNode(); + nsCOMPtr<nsINode> rightBlockParent = rightParent->GetParentNode(); + + // MOOSE: this could conceivably screw up a table.. fix me. + NS_ENSURE_STATE(mHTMLEditor); + if (leftBlockParent == rightBlockParent && + mHTMLEditor->NodesSameType(GetAsDOMNode(leftParent), + GetAsDOMNode(rightParent)) && + // XXX What's special about these three types of block? + (leftParent->IsHTMLElement(nsGkAtoms::p) || + HTMLEditUtils::IsListItem(leftParent) || + HTMLEditUtils::IsHeader(*leftParent))) { + // First delete the selection + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers); + NS_ENSURE_SUCCESS(rv, rv); + // Join blocks + NS_ENSURE_STATE(mHTMLEditor); + EditorDOMPoint pt = + mHTMLEditor->JoinNodeDeep(*leftParent, *rightParent); + NS_ENSURE_STATE(pt.node); + // Fix up selection + rv = aSelection->Collapse(pt.node, pt.offset); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + // Else blocks not same type, or not siblings. Delete everything + // except table elements. + join = true; + + uint32_t rangeCount = aSelection->RangeCount(); + for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) { + OwningNonNull<nsRange> range = *aSelection->GetRangeAt(rangeIdx); + + // Build a list of nodes in the range + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + TrivialFunctor functor; + DOMSubtreeIterator iter; + nsresult rv = iter.Init(*range); + NS_ENSURE_SUCCESS(rv, rv); + iter.AppendList(functor, arrayOfNodes); + + // Now that we have the list, delete non-table elements + int32_t listCount = arrayOfNodes.Length(); + for (int32_t j = 0; j < listCount; j++) { + nsCOMPtr<nsINode> somenode = do_QueryInterface(arrayOfNodes[0]); + NS_ENSURE_STATE(somenode); + DeleteNonTableElements(somenode); + arrayOfNodes.RemoveElementAt(0); + // If something visible is deleted, no need to join. Visible means + // all nodes except non-visible textnodes and breaks. + if (join && origCollapsed) { + if (!somenode->IsContent()) { + join = false; + continue; + } + nsCOMPtr<nsIContent> content = somenode->AsContent(); + if (content->NodeType() == nsIDOMNode::TEXT_NODE) { + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->IsVisTextNode(content, &join, true); + } else { + NS_ENSURE_STATE(mHTMLEditor); + join = content->IsHTMLElement(nsGkAtoms::br) && + !mHTMLEditor->IsVisBreak(somenode); + } + } + } + } + + // Check endpoints for possible text deletion. We can assume that if + // text node is found, we can delete to end or to begining as + // appropriate, since the case where both sel endpoints in same text + // node was already handled (we wouldn't be here) + if (startNode->GetAsText() && + startNode->Length() > static_cast<uint32_t>(startOffset)) { + // Delete to last character + OwningNonNull<nsGenericDOMDataNode> dataNode = + *static_cast<nsGenericDOMDataNode*>(startNode.get()); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteText(dataNode, startOffset, + startNode->Length() - startOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + if (endNode->GetAsText() && endOffset) { + // Delete to first character + NS_ENSURE_STATE(mHTMLEditor); + OwningNonNull<nsGenericDOMDataNode> dataNode = + *static_cast<nsGenericDOMDataNode*>(endNode.get()); + rv = mHTMLEditor->DeleteText(dataNode, 0, endOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (join) { + rv = JoinBlocks(*leftParent, *rightParent, aCancel); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + } + + // We might have left only collapsed whitespace in the start/end nodes + { + AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater, + address_of(startNode), &startOffset); + AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater, + address_of(endNode), &endOffset); + + DeleteNodeIfCollapsedText(*startNode); + DeleteNodeIfCollapsedText(*endNode); + } + + // If we're joining blocks: if deleting forward the selection should be + // collapsed to the end of the selection, if deleting backward the selection + // should be collapsed to the beginning of the selection. But if we're not + // joining then the selection should collapse to the beginning of the + // selection if we'redeleting forward, because the end of the selection will + // still be in the next block. And same thing for deleting backwards + // (selection should collapse to the end, because the beginning will still be + // in the first block). See Bug 507936 + if (aAction == (join ? nsIEditor::eNext : nsIEditor::ePrevious)) { + rv = aSelection->Collapse(endNode, endOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = aSelection->Collapse(startNode, startOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + return NS_OK; +} + +/** + * If aNode is a text node that contains only collapsed whitespace, delete it. + * It doesn't serve any useful purpose, and we don't want it to confuse code + * that doesn't correctly skip over it. + * + * If deleting the node fails (like if it's not editable), the caller should + * proceed as usual, so don't return any errors. + */ +void +HTMLEditRules::DeleteNodeIfCollapsedText(nsINode& aNode) +{ + if (!aNode.GetAsText()) { + return; + } + bool empty; + nsresult rv = mHTMLEditor->IsVisTextNode(aNode.AsContent(), &empty, false); + NS_ENSURE_SUCCESS_VOID(rv); + if (empty) { + mHTMLEditor->DeleteNode(&aNode); + } +} + + +/** + * InsertBRIfNeeded() determines if a br is needed for current selection to not + * be spastic. If so, it inserts one. Callers responsibility to only call + * with collapsed selection. + * + * @param aSelection The collapsed selection. + */ +nsresult +HTMLEditRules::InsertBRIfNeeded(Selection* aSelection) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + + // get selection + nsCOMPtr<nsINode> node; + int32_t offset; + nsresult rv = + mTextEditor->GetStartNodeAndOffset(aSelection, + getter_AddRefs(node), &offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(node, NS_ERROR_FAILURE); + + // inline elements don't need any br + if (!IsBlockNode(*node)) { + return NS_OK; + } + + // examine selection + NS_ENSURE_STATE(mHTMLEditor); + WSRunObject wsObj(mHTMLEditor, node, offset); + if (((wsObj.mStartReason & WSType::block) || + (wsObj.mStartReason & WSType::br)) && + (wsObj.mEndReason & WSType::block)) { + // if we are tucked between block boundaries then insert a br + // first check that we are allowed to + NS_ENSURE_STATE(mHTMLEditor); + if (mHTMLEditor->CanContainTag(*node, *nsGkAtoms::br)) { + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> br = + mHTMLEditor->CreateBR(node, offset, nsIEditor::ePrevious); + return br ? NS_OK : NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +/** + * GetGoodSelPointForNode() finds where at a node you would want to set the + * selection if you were trying to have a caret next to it. Always returns a + * valid value (unless mHTMLEditor has gone away). + * + * @param aNode The node + * @param aAction Which edge to find: + * eNext/eNextWord/eToEndOfLine indicates beginning, + * ePrevious/PreviousWord/eToBeginningOfLine ending. + */ +EditorDOMPoint +HTMLEditRules::GetGoodSelPointForNode(nsINode& aNode, + nsIEditor::EDirection aAction) +{ + MOZ_ASSERT(aAction == nsIEditor::eNext || + aAction == nsIEditor::eNextWord || + aAction == nsIEditor::ePrevious || + aAction == nsIEditor::ePreviousWord || + aAction == nsIEditor::eToBeginningOfLine || + aAction == nsIEditor::eToEndOfLine); + + bool isPreviousAction = (aAction == nsIEditor::ePrevious || + aAction == nsIEditor::ePreviousWord || + aAction == nsIEditor::eToBeginningOfLine); + + NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); + if (aNode.GetAsText() || mHTMLEditor->IsContainer(&aNode) || + NS_WARN_IF(!aNode.GetParentNode())) { + return EditorDOMPoint(&aNode, isPreviousAction ? aNode.Length() : 0); + } + + EditorDOMPoint ret; + ret.node = aNode.GetParentNode(); + ret.offset = ret.node ? ret.node->IndexOf(&aNode) : -1; + NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); + if ((!aNode.IsHTMLElement(nsGkAtoms::br) || + mHTMLEditor->IsVisBreak(&aNode)) && isPreviousAction) { + ret.offset++; + } + return ret; +} + + +/** + * This method is used to join two block elements. The right element is always + * joined to the left element. If the elements are the same type and not + * nested within each other, JoinNodesSmart is called (example, joining two + * list items together into one). If the elements are not the same type, or + * one is a descendant of the other, we instead destroy the right block placing + * its children into leftblock. DTD containment rules are followed throughout. + */ +nsresult +HTMLEditRules::JoinBlocks(nsIContent& aLeftNode, + nsIContent& aRightNode, + bool* aCanceled) +{ + MOZ_ASSERT(aCanceled); + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + nsCOMPtr<Element> leftBlock = htmlEditor->GetBlock(aLeftNode); + nsCOMPtr<Element> rightBlock = htmlEditor->GetBlock(aRightNode); + + // Sanity checks + NS_ENSURE_TRUE(leftBlock && rightBlock, NS_ERROR_NULL_POINTER); + NS_ENSURE_STATE(leftBlock != rightBlock); + + if (HTMLEditUtils::IsTableElement(leftBlock) || + HTMLEditUtils::IsTableElement(rightBlock)) { + // Do not try to merge table elements + *aCanceled = true; + return NS_OK; + } + + // Make sure we don't try to move things into HR's, which look like blocks + // but aren't containers + if (leftBlock->IsHTMLElement(nsGkAtoms::hr)) { + leftBlock = htmlEditor->GetBlockNodeParent(leftBlock); + } + if (rightBlock->IsHTMLElement(nsGkAtoms::hr)) { + rightBlock = htmlEditor->GetBlockNodeParent(rightBlock); + } + NS_ENSURE_STATE(leftBlock && rightBlock); + + // Bail if both blocks the same + if (leftBlock == rightBlock) { + *aCanceled = true; + return NS_OK; + } + + // Joining a list item to its parent is a NOP. + if (HTMLEditUtils::IsList(leftBlock) && + HTMLEditUtils::IsListItem(rightBlock) && + rightBlock->GetParentNode() == leftBlock) { + return NS_OK; + } + + // Special rule here: if we are trying to join list items, and they are in + // different lists, join the lists instead. + bool mergeLists = false; + nsIAtom* existingList = nsGkAtoms::_empty; + int32_t offset; + nsCOMPtr<Element> leftList, rightList; + if (HTMLEditUtils::IsListItem(leftBlock) && + HTMLEditUtils::IsListItem(rightBlock)) { + leftList = leftBlock->GetParentElement(); + rightList = rightBlock->GetParentElement(); + if (leftList && rightList && leftList != rightList && + !EditorUtils::IsDescendantOf(leftList, rightBlock, &offset) && + !EditorUtils::IsDescendantOf(rightList, leftBlock, &offset)) { + // There are some special complications if the lists are descendants of + // the other lists' items. Note that it is okay for them to be + // descendants of the other lists themselves, which is the usual case for + // sublists in our implementation. + leftBlock = leftList; + rightBlock = rightList; + mergeLists = true; + existingList = leftList->NodeInfo()->NameAtom(); + } + } + + AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor); + + int32_t rightOffset = 0; + int32_t leftOffset = -1; + + // offset below is where you find yourself in rightBlock when you traverse + // upwards from leftBlock + if (EditorUtils::IsDescendantOf(leftBlock, rightBlock, &rightOffset)) { + // Tricky case. Left block is inside right block. Do ws adjustment. This + // just destroys non-visible ws at boundaries we will be joining. + rightOffset++; + nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor, + WSRunObject::kBlockEnd, + leftBlock); + NS_ENSURE_SUCCESS(rv, rv); + + { + // We can't just track rightBlock because it's an Element. + nsCOMPtr<nsINode> trackingRightBlock(rightBlock); + AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater, + address_of(trackingRightBlock), &rightOffset); + rv = WSRunObject::ScrubBlockBoundary(htmlEditor, + WSRunObject::kAfterBlock, + rightBlock, rightOffset); + NS_ENSURE_SUCCESS(rv, rv); + if (trackingRightBlock->IsElement()) { + rightBlock = trackingRightBlock->AsElement(); + } else { + NS_ENSURE_STATE(trackingRightBlock->GetParentElement()); + rightBlock = trackingRightBlock->GetParentElement(); + } + } + // Do br adjustment. + nsCOMPtr<Element> brNode = + CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd); + if (mergeLists) { + // The idea here is to take all children in rightList that are past + // offset, and pull them into leftlist. + for (nsCOMPtr<nsIContent> child = rightList->GetChildAt(offset); + child; child = rightList->GetChildAt(rightOffset)) { + rv = htmlEditor->MoveNode(child, leftList, -1); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset); + } + if (brNode) { + htmlEditor->DeleteNode(brNode); + } + // Offset below is where you find yourself in leftBlock when you traverse + // upwards from rightBlock + } else if (EditorUtils::IsDescendantOf(rightBlock, leftBlock, &leftOffset)) { + // Tricky case. Right block is inside left block. Do ws adjustment. This + // just destroys non-visible ws at boundaries we will be joining. + nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor, + WSRunObject::kBlockStart, + rightBlock); + NS_ENSURE_SUCCESS(rv, rv); + { + // We can't just track leftBlock because it's an Element, so track + // something else. + nsCOMPtr<nsINode> trackingLeftBlock(leftBlock); + AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater, + address_of(trackingLeftBlock), &leftOffset); + rv = WSRunObject::ScrubBlockBoundary(htmlEditor, + WSRunObject::kBeforeBlock, + leftBlock, leftOffset); + NS_ENSURE_SUCCESS(rv, rv); + if (trackingLeftBlock->IsElement()) { + leftBlock = trackingLeftBlock->AsElement(); + } else { + NS_ENSURE_STATE(trackingLeftBlock->GetParentElement()); + leftBlock = trackingLeftBlock->GetParentElement(); + } + } + // Do br adjustment. + nsCOMPtr<Element> brNode = + CheckForInvisibleBR(*leftBlock, BRLocation::beforeBlock, leftOffset); + if (mergeLists) { + MoveContents(*rightList, *leftList, &leftOffset); + } else { + // Left block is a parent of right block, and the parent of the previous + // visible content. Right block is a child and contains the contents we + // want to move. + + int32_t previousContentOffset; + nsCOMPtr<nsINode> previousContentParent; + + if (&aLeftNode == leftBlock) { + // We are working with valid HTML, aLeftNode is a block node, and is + // therefore allowed to contain rightBlock. This is the simple case, + // we will simply move the content in rightBlock out of its block. + previousContentParent = leftBlock; + previousContentOffset = leftOffset; + } else { + // We try to work as well as possible with HTML that's already invalid. + // Although "right block" is a block, and a block must not be contained + // in inline elements, reality is that broken documents do exist. The + // DIRECT parent of "left NODE" might be an inline element. Previous + // versions of this code skipped inline parents until the first block + // parent was found (and used "left block" as the destination). + // However, in some situations this strategy moves the content to an + // unexpected position. (see bug 200416) The new idea is to make the + // moving content a sibling, next to the previous visible content. + + previousContentParent = aLeftNode.GetParentNode(); + previousContentOffset = previousContentParent ? + previousContentParent->IndexOf(&aLeftNode) : -1; + + // We want to move our content just after the previous visible node. + previousContentOffset++; + } + + // Because we don't want the moving content to receive the style of the + // previous content, we split the previous content's style. + + nsCOMPtr<Element> editorRoot = htmlEditor->GetEditorRoot(); + if (!editorRoot || &aLeftNode != editorRoot) { + nsCOMPtr<nsIContent> splittedPreviousContent; + rv = htmlEditor->SplitStyleAbovePoint( + address_of(previousContentParent), + &previousContentOffset, + nullptr, nullptr, nullptr, + getter_AddRefs(splittedPreviousContent)); + NS_ENSURE_SUCCESS(rv, rv); + + if (splittedPreviousContent) { + previousContentParent = splittedPreviousContent->GetParentNode(); + previousContentOffset = previousContentParent ? + previousContentParent->IndexOf(splittedPreviousContent) : -1; + } + } + + NS_ENSURE_TRUE(previousContentParent, NS_ERROR_NULL_POINTER); + + rv = MoveBlock(*previousContentParent->AsElement(), *rightBlock, + previousContentOffset, rightOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + if (brNode) { + htmlEditor->DeleteNode(brNode); + } + } else { + // Normal case. Blocks are siblings, or at least close enough. An example + // of the latter is <p>paragraph</p><ul><li>one<li>two<li>three</ul>. The + // first li and the p are not true siblings, but we still want to join them + // if you backspace from li into p. + + // Adjust whitespace at block boundaries + nsresult rv = + WSRunObject::PrepareToJoinBlocks(htmlEditor, leftBlock, rightBlock); + NS_ENSURE_SUCCESS(rv, rv); + // Do br adjustment. + nsCOMPtr<Element> brNode = + CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd); + if (mergeLists || leftBlock->NodeInfo()->NameAtom() == + rightBlock->NodeInfo()->NameAtom()) { + // Nodes are same type. merge them. + EditorDOMPoint pt = JoinNodesSmart(*leftBlock, *rightBlock); + if (pt.node && mergeLists) { + nsCOMPtr<Element> newBlock; + ConvertListType(rightBlock, getter_AddRefs(newBlock), + existingList, nsGkAtoms::li); + } + } else { + // Nodes are dissimilar types. + rv = MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + if (brNode) { + rv = htmlEditor->DeleteNode(brNode); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + + +/** + * Moves the content from aRightBlock starting from aRightOffset into + * aLeftBlock at aLeftOffset. Note that the "block" might merely be inline + * nodes between <br>s, or between blocks, etc. DTD containment rules are + * followed throughout. + */ +nsresult +HTMLEditRules::MoveBlock(Element& aLeftBlock, + Element& aRightBlock, + int32_t aLeftOffset, + int32_t aRightOffset) +{ + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + // GetNodesFromPoint is the workhorse that figures out what we wnat to move. + nsresult rv = GetNodesFromPoint(EditorDOMPoint(&aRightBlock, aRightOffset), + EditAction::makeList, arrayOfNodes, + TouchContent::yes); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) { + // get the node to act on + if (IsBlockNode(arrayOfNodes[i])) { + // For block nodes, move their contents only, then delete block. + rv = MoveContents(*arrayOfNodes[i]->AsElement(), aLeftBlock, + &aLeftOffset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(arrayOfNodes[i]); + } else { + // Otherwise move the content as is, checking against the DTD. + rv = MoveNodeSmart(*arrayOfNodes[i]->AsContent(), aLeftBlock, + &aLeftOffset); + } + } + + // XXX We're only checking return value of the last iteration + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +/** + * This method is used to move node aNode to (aDestElement, aInOutDestOffset). + * DTD containment rules are followed throughout. aInOutDestOffset is updated + * to point _after_ inserted content. + */ +nsresult +HTMLEditRules::MoveNodeSmart(nsIContent& aNode, + Element& aDestElement, + int32_t* aInOutDestOffset) +{ + MOZ_ASSERT(aInOutDestOffset); + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Check if this node can go into the destination node + if (htmlEditor->CanContain(aDestElement, aNode)) { + // If it can, move it there + nsresult rv = + htmlEditor->MoveNode(&aNode, &aDestElement, *aInOutDestOffset); + NS_ENSURE_SUCCESS(rv, rv); + if (*aInOutDestOffset != -1) { + (*aInOutDestOffset)++; + } + } else { + // If it can't, move its children (if any), and then delete it. + if (aNode.IsElement()) { + nsresult rv = + MoveContents(*aNode.AsElement(), aDestElement, aInOutDestOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsresult rv = htmlEditor->DeleteNode(&aNode); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +/** + * Moves the _contents_ of aElement to (aDestElement, aInOutDestOffset). DTD + * containment rules are followed throughout. aInOutDestOffset is updated to + * point _after_ inserted content. + */ +nsresult +HTMLEditRules::MoveContents(Element& aElement, + Element& aDestElement, + int32_t* aInOutDestOffset) +{ + MOZ_ASSERT(aInOutDestOffset); + + NS_ENSURE_TRUE(&aElement != &aDestElement, NS_ERROR_ILLEGAL_VALUE); + + while (aElement.GetFirstChild()) { + nsresult rv = MoveNodeSmart(*aElement.GetFirstChild(), aDestElement, + aInOutDestOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + + +nsresult +HTMLEditRules::DeleteNonTableElements(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + if (!HTMLEditUtils::IsTableElementButNotTable(aNode)) { + NS_ENSURE_STATE(mHTMLEditor); + return mHTMLEditor->DeleteNode(aNode->AsDOMNode()); + } + + for (int32_t i = aNode->GetChildCount() - 1; i >= 0; --i) { + nsresult rv = DeleteNonTableElements(aNode->GetChildAt(i)); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult +HTMLEditRules::DidDeleteSelection(Selection* aSelection, + nsIEditor::EDirection aDir, + nsresult aResult) +{ + if (!aSelection) { + return NS_ERROR_NULL_POINTER; + } + + // find where we are + nsCOMPtr<nsINode> startNode; + int32_t startOffset; + nsresult rv = mTextEditor->GetStartNodeAndOffset(aSelection, + getter_AddRefs(startNode), + &startOffset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); + + // find any enclosing mailcite + nsCOMPtr<Element> citeNode = GetTopEnclosingMailCite(*startNode); + if (citeNode) { + bool isEmpty = true, seenBR = false; + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->IsEmptyNodeImpl(citeNode, &isEmpty, true, true, false, + &seenBR); + if (isEmpty) { + int32_t offset; + nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(citeNode, &offset); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(citeNode); + NS_ENSURE_SUCCESS(rv, rv); + if (parent && seenBR) { + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(parent, offset); + NS_ENSURE_STATE(brNode); + aSelection->Collapse(parent, offset); + } + } + } + + // call through to base class + return TextEditRules::DidDeleteSelection(aSelection, aDir, aResult); +} + +nsresult +HTMLEditRules::WillMakeList(Selection* aSelection, + const nsAString* aListType, + bool aEntireList, + const nsAString* aBulletType, + bool* aCancel, + bool* aHandled, + const nsAString* aItemType) +{ + if (!aSelection || !aListType || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + OwningNonNull<nsIAtom> listType = NS_Atomize(*aListType); + + WillInsert(*aSelection, aCancel); + + // initialize out param + // we want to ignore result of WillInsert() + *aCancel = false; + *aHandled = false; + + // deduce what tag to use for list items + nsCOMPtr<nsIAtom> itemType; + if (aItemType) { + itemType = NS_Atomize(*aItemType); + NS_ENSURE_TRUE(itemType, NS_ERROR_OUT_OF_MEMORY); + } else if (listType == nsGkAtoms::dl) { + itemType = nsGkAtoms::dd; + } else { + itemType = nsGkAtoms::li; + } + + // convert the selection ranges into "promoted" selection ranges: + // this basically just expands the range to include the immediate + // block parent, and then further expands to include any ancestors + // whose children are all in the range + + *aHandled = true; + + nsresult rv = NormalizeSelection(aSelection); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); + + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + rv = GetListActionNodes(arrayOfNodes, + aEntireList ? EntireList::yes : EntireList::no); + NS_ENSURE_SUCCESS(rv, rv); + + // check if all our nodes are <br>s, or empty inlines + bool bOnlyBreaks = true; + for (auto& curNode : arrayOfNodes) { + // if curNode is not a Break or empty inline, we're done + if (!TextEditUtils::IsBreak(curNode) && + !IsEmptyInline(curNode)) { + bOnlyBreaks = false; + break; + } + } + + // if no nodes, we make empty list. Ditto if the user tried to make a list + // of some # of breaks. + if (arrayOfNodes.IsEmpty() || bOnlyBreaks) { + // if only breaks, delete them + if (bOnlyBreaks) { + for (auto& node : arrayOfNodes) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(node); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // get selection location + NS_ENSURE_STATE(aSelection->RangeCount()); + nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent(); + int32_t offset = aSelection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_STATE(parent); + + // make sure we can put a list here + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->CanContainTag(*parent, listType)) { + *aCancel = true; + return NS_OK; + } + rv = SplitAsNeeded(listType, parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> theList = + mHTMLEditor->CreateNode(listType, parent, offset); + NS_ENSURE_STATE(theList); + + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> theListItem = + mHTMLEditor->CreateNode(itemType, theList, 0); + NS_ENSURE_STATE(theListItem); + + // remember our new block for postprocessing + mNewBlock = theListItem; + // put selection in new list item + *aHandled = true; + rv = aSelection->Collapse(theListItem, 0); + // Don't restore the selection + selectionRestorer.Abort(); + return rv; + } + + // if there is only one node in the array, and it is a list, div, or + // blockquote, then look inside of it until we find inner list or content. + + LookInsideDivBQandList(arrayOfNodes); + + // Ok, now go through all the nodes and put then in the list, + // or whatever is approriate. Wohoo! + + uint32_t listCount = arrayOfNodes.Length(); + nsCOMPtr<nsINode> curParent; + nsCOMPtr<Element> curList, prevListItem; + + for (uint32_t i = 0; i < listCount; i++) { + // here's where we actually figure out what to do + nsCOMPtr<Element> newBlock; + NS_ENSURE_STATE(arrayOfNodes[i]->IsContent()); + OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent(); + int32_t offset; + curParent = EditorBase::GetNodeLocation(curNode, &offset); + + // make sure we don't assemble content that is in different table cells + // into the same list. respect table cell boundaries when listifying. + if (curList && InDifferentTableElements(curList, curNode)) { + curList = nullptr; + } + + // if curNode is a Break, delete it, and quit remembering prev list item + if (TextEditUtils::IsBreak(curNode)) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(curNode); + NS_ENSURE_SUCCESS(rv, rv); + prevListItem = nullptr; + continue; + } else if (IsEmptyInline(curNode)) { + // if curNode is an empty inline container, delete it + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(curNode); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + + if (HTMLEditUtils::IsList(curNode)) { + // do we have a curList already? + if (curList && !EditorUtils::IsDescendantOf(curNode, curList)) { + // move all of our children into curList. cheezy way to do it: move + // whole list and then RemoveContainer() on the list. ConvertListType + // first: that routine handles converting the list item types, if + // needed + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, curList, -1); + NS_ENSURE_SUCCESS(rv, rv); + rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock), + listType, itemType); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->RemoveBlockContainer(*newBlock); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // replace list with new list type + rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock), + listType, itemType); + NS_ENSURE_SUCCESS(rv, rv); + curList = newBlock; + } + prevListItem = nullptr; + continue; + } + + if (HTMLEditUtils::IsListItem(curNode)) { + NS_ENSURE_STATE(mHTMLEditor); + if (!curParent->IsHTMLElement(listType)) { + // list item is in wrong type of list. if we don't have a curList, + // split the old list and make a new list of correct type. + if (!curList || EditorUtils::IsDescendantOf(curNode, curList)) { + NS_ENSURE_STATE(mHTMLEditor); + NS_ENSURE_STATE(curParent->IsContent()); + ErrorResult rv; + nsCOMPtr<nsIContent> splitNode = + mHTMLEditor->SplitNode(*curParent->AsContent(), offset, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + newBlock = splitNode ? splitNode->AsElement() : nullptr; + int32_t offset; + nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(curParent, + &offset); + NS_ENSURE_STATE(mHTMLEditor); + curList = mHTMLEditor->CreateNode(listType, parent, offset); + NS_ENSURE_STATE(curList); + } + // move list item to new list + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, curList, -1); + NS_ENSURE_SUCCESS(rv, rv); + // convert list item type if needed + NS_ENSURE_STATE(mHTMLEditor); + if (!curNode->IsHTMLElement(itemType)) { + NS_ENSURE_STATE(mHTMLEditor); + newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(), + itemType); + NS_ENSURE_STATE(newBlock); + } + } else { + // item is in right type of list. But we might still have to move it. + // and we might need to convert list item types. + if (!curList) { + curList = curParent->AsElement(); + } else if (curParent != curList) { + // move list item to new list + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, curList, -1); + NS_ENSURE_SUCCESS(rv, rv); + } + NS_ENSURE_STATE(mHTMLEditor); + if (!curNode->IsHTMLElement(itemType)) { + NS_ENSURE_STATE(mHTMLEditor); + newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(), + itemType); + NS_ENSURE_STATE(newBlock); + } + } + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIDOMElement> curElement = do_QueryInterface(curNode); + NS_NAMED_LITERAL_STRING(typestr, "type"); + if (aBulletType && !aBulletType->IsEmpty()) { + rv = mHTMLEditor->SetAttribute(curElement, typestr, *aBulletType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = mHTMLEditor->RemoveAttribute(curElement, typestr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + continue; + } + + // if we hit a div clear our prevListItem, insert divs contents + // into our node array, and remove the div + if (curNode->IsHTMLElement(nsGkAtoms::div)) { + prevListItem = nullptr; + int32_t j = i + 1; + GetInnerContent(*curNode, arrayOfNodes, &j); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->RemoveContainer(curNode); + NS_ENSURE_SUCCESS(rv, rv); + listCount = arrayOfNodes.Length(); + continue; + } + + // need to make a list to put things in if we haven't already, + if (!curList) { + rv = SplitAsNeeded(listType, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + curList = mHTMLEditor->CreateNode(listType, curParent, offset); + // remember our new block for postprocessing + mNewBlock = curList; + // curList is now the correct thing to put curNode in + prevListItem = nullptr; + } + + // if curNode isn't a list item, we must wrap it in one + nsCOMPtr<Element> listItem; + if (!HTMLEditUtils::IsListItem(curNode)) { + if (IsInlineNode(curNode) && prevListItem) { + // this is a continuation of some inline nodes that belong together in + // the same list item. use prevListItem + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, prevListItem, -1); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // don't wrap li around a paragraph. instead replace paragraph with li + if (curNode->IsHTMLElement(nsGkAtoms::p)) { + NS_ENSURE_STATE(mHTMLEditor); + listItem = mHTMLEditor->ReplaceContainer(curNode->AsElement(), + itemType); + NS_ENSURE_STATE(listItem); + } else { + NS_ENSURE_STATE(mHTMLEditor); + listItem = mHTMLEditor->InsertContainerAbove(curNode, itemType); + NS_ENSURE_STATE(listItem); + } + if (IsInlineNode(curNode)) { + prevListItem = listItem; + } else { + prevListItem = nullptr; + } + } + } else { + listItem = curNode->AsElement(); + } + + if (listItem) { + // if we made a new list item, deal with it: tuck the listItem into the + // end of the active list + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(listItem, curList, -1); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + + +nsresult +HTMLEditRules::WillRemoveList(Selection* aSelection, + bool aOrdered, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + // initialize out param + *aCancel = false; + *aHandled = true; + + nsresult rv = NormalizeSelection(aSelection); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); + + nsTArray<RefPtr<nsRange>> arrayOfRanges; + GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::makeList); + + // use these ranges to contruct a list of nodes to act on. + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + rv = GetListActionNodes(arrayOfNodes, EntireList::no); + NS_ENSURE_SUCCESS(rv, rv); + + // Remove all non-editable nodes. Leave them be. + for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) { + OwningNonNull<nsINode> testNode = arrayOfNodes[i]; + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsEditable(testNode)) { + arrayOfNodes.RemoveElementAt(i); + } + } + + // Only act on lists or list items in the array + for (auto& curNode : arrayOfNodes) { + // here's where we actually figure out what to do + if (HTMLEditUtils::IsListItem(curNode)) { + // unlist this listitem + bool bOutOfList; + do { + rv = PopListItem(GetAsDOMNode(curNode), &bOutOfList); + NS_ENSURE_SUCCESS(rv, rv); + } while (!bOutOfList); // keep popping it out until it's not in a list anymore + } else if (HTMLEditUtils::IsList(curNode)) { + // node is a list, move list items out + rv = RemoveListStructure(*curNode->AsElement()); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::WillMakeDefListItem(Selection* aSelection, + const nsAString *aItemType, + bool aEntireList, + bool* aCancel, + bool* aHandled) +{ + // for now we let WillMakeList handle this + NS_NAMED_LITERAL_STRING(listType, "dl"); + return WillMakeList(aSelection, &listType, aEntireList, nullptr, aCancel, aHandled, aItemType); +} + +nsresult +HTMLEditRules::WillMakeBasicBlock(Selection& aSelection, + const nsAString& aBlockType, + bool* aCancel, + bool* aHandled) +{ + MOZ_ASSERT(aCancel && aHandled); + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + OwningNonNull<nsIAtom> blockType = NS_Atomize(aBlockType); + + WillInsert(aSelection, aCancel); + // We want to ignore result of WillInsert() + *aCancel = false; + *aHandled = false; + + nsresult rv = NormalizeSelection(&aSelection); + NS_ENSURE_SUCCESS(rv, rv); + AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor); + AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor); + *aHandled = true; + + // Contruct a list of nodes to act on. + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + rv = GetNodesFromSelection(aSelection, EditAction::makeBasicBlock, + arrayOfNodes); + NS_ENSURE_SUCCESS(rv, rv); + + // Remove all non-editable nodes. Leave them be. + for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) { + if (!htmlEditor->IsEditable(arrayOfNodes[i])) { + arrayOfNodes.RemoveElementAt(i); + } + } + + // If nothing visible in list, make an empty block + if (ListIsEmptyLine(arrayOfNodes)) { + // Get selection location + NS_ENSURE_STATE(aSelection.GetRangeAt(0) && + aSelection.GetRangeAt(0)->GetStartParent()); + OwningNonNull<nsINode> parent = + *aSelection.GetRangeAt(0)->GetStartParent(); + int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); + + if (blockType == nsGkAtoms::normal || + blockType == nsGkAtoms::_empty) { + // We are removing blocks (going to "body text") + NS_ENSURE_TRUE(htmlEditor->GetBlock(parent), NS_ERROR_NULL_POINTER); + OwningNonNull<Element> curBlock = *htmlEditor->GetBlock(parent); + if (HTMLEditUtils::IsFormatNode(curBlock)) { + // If the first editable node after selection is a br, consume it. + // Otherwise it gets pushed into a following block after the split, + // which is visually bad. + nsCOMPtr<nsIContent> brNode = + htmlEditor->GetNextHTMLNode(parent, offset); + if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) { + rv = htmlEditor->DeleteNode(brNode); + NS_ENSURE_SUCCESS(rv, rv); + } + // Do the splits! + offset = htmlEditor->SplitNodeDeep(curBlock, *parent->AsContent(), + offset, + HTMLEditor::EmptyContainers::no); + NS_ENSURE_STATE(offset != -1); + // Put a br at the split point + brNode = htmlEditor->CreateBR(curBlock->GetParentNode(), offset); + NS_ENSURE_STATE(brNode); + // Put selection at the split point + *aHandled = true; + rv = aSelection.Collapse(curBlock->GetParentNode(), offset); + // Don't restore the selection + selectionRestorer.Abort(); + NS_ENSURE_SUCCESS(rv, rv); + } + // Else nothing to do! + } else { + // We are making a block. Consume a br, if needed. + nsCOMPtr<nsIContent> brNode = + htmlEditor->GetNextHTMLNode(parent, offset, true); + if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) { + rv = htmlEditor->DeleteNode(brNode); + NS_ENSURE_SUCCESS(rv, rv); + // We don't need to act on this node any more + arrayOfNodes.RemoveElement(brNode); + } + // Make sure we can put a block here + rv = SplitAsNeeded(blockType, parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<Element> block = + htmlEditor->CreateNode(blockType, parent, offset); + NS_ENSURE_STATE(block); + // Remember our new block for postprocessing + mNewBlock = block; + // Delete anything that was in the list of nodes + while (!arrayOfNodes.IsEmpty()) { + OwningNonNull<nsINode> curNode = arrayOfNodes[0]; + rv = htmlEditor->DeleteNode(curNode); + NS_ENSURE_SUCCESS(rv, rv); + arrayOfNodes.RemoveElementAt(0); + } + // Put selection in new block + *aHandled = true; + rv = aSelection.Collapse(block, 0); + // Don't restore the selection + selectionRestorer.Abort(); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; + } + // Okay, now go through all the nodes and make the right kind of blocks, or + // whatever is approriate. Woohoo! Note: blockquote is handled a little + // differently. + if (blockType == nsGkAtoms::blockquote) { + rv = MakeBlockquote(arrayOfNodes); + NS_ENSURE_SUCCESS(rv, rv); + } else if (blockType == nsGkAtoms::normal || + blockType == nsGkAtoms::_empty) { + rv = RemoveBlockStyle(arrayOfNodes); + NS_ENSURE_SUCCESS(rv, rv); + } else { + rv = ApplyBlockStyle(arrayOfNodes, blockType); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult +HTMLEditRules::DidMakeBasicBlock(Selection* aSelection, + RulesInfo* aInfo, + nsresult aResult) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + // check for empty block. if so, put a moz br in it. + if (!aSelection->Collapsed()) { + return NS_OK; + } + + NS_ENSURE_STATE(aSelection->GetRangeAt(0) && + aSelection->GetRangeAt(0)->GetStartParent()); + nsresult rv = + InsertMozBRIfNeeded(*aSelection->GetRangeAt(0)->GetStartParent()); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult +HTMLEditRules::WillIndent(Selection* aSelection, + bool* aCancel, + bool* aHandled) +{ + NS_ENSURE_STATE(mHTMLEditor); + if (mHTMLEditor->IsCSSEnabled()) { + nsresult rv = WillCSSIndent(aSelection, aCancel, aHandled); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsresult rv = WillHTMLIndent(aSelection, aCancel, aHandled); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::WillCSSIndent(Selection* aSelection, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + + WillInsert(*aSelection, aCancel); + + // initialize out param + // we want to ignore result of WillInsert() + *aCancel = false; + *aHandled = true; + + nsresult rv = NormalizeSelection(aSelection); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); + nsTArray<OwningNonNull<nsRange>> arrayOfRanges; + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + + // short circuit: detect case of collapsed selection inside an <li>. + // just sublist that <li>. This prevents bug 97797. + + nsCOMPtr<Element> liNode; + if (aSelection->Collapsed()) { + nsCOMPtr<nsINode> node; + int32_t offset; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetStartNodeAndOffset(aSelection, + getter_AddRefs(node), &offset); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*node); + if (block && HTMLEditUtils::IsListItem(block)) { + liNode = block; + } + } + + if (liNode) { + arrayOfNodes.AppendElement(*liNode); + } else { + // convert the selection ranges into "promoted" selection ranges: + // this basically just expands the range to include the immediate + // block parent, and then further expands to include any ancestors + // whose children are all in the range + rv = GetNodesFromSelection(*aSelection, EditAction::indent, arrayOfNodes); + NS_ENSURE_SUCCESS(rv, rv); + } + + // if nothing visible in list, make an empty block + if (ListIsEmptyLine(arrayOfNodes)) { + // get selection location + NS_ENSURE_STATE(aSelection->RangeCount()); + nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent(); + int32_t offset = aSelection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_STATE(parent); + + // make sure we can put a block here + rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::div, + parent, offset); + NS_ENSURE_STATE(theBlock); + // remember our new block for postprocessing + mNewBlock = theBlock; + ChangeIndentation(*theBlock, Change::plus); + // delete anything that was in the list of nodes + while (!arrayOfNodes.IsEmpty()) { + OwningNonNull<nsINode> curNode = arrayOfNodes[0]; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(curNode); + NS_ENSURE_SUCCESS(rv, rv); + arrayOfNodes.RemoveElementAt(0); + } + // put selection in new block + *aHandled = true; + rv = aSelection->Collapse(theBlock, 0); + // Don't restore the selection + selectionRestorer.Abort(); + return rv; + } + + // Ok, now go through all the nodes and put them in a blockquote, + // or whatever is appropriate. Wohoo! + nsCOMPtr<nsINode> curParent; + nsCOMPtr<Element> curList, curQuote; + nsCOMPtr<nsIContent> sibling; + int32_t listCount = arrayOfNodes.Length(); + for (int32_t i = 0; i < listCount; i++) { + // here's where we actually figure out what to do + NS_ENSURE_STATE(arrayOfNodes[i]->IsContent()); + nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent(); + + // Ignore all non-editable nodes. Leave them be. + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsEditable(curNode)) { + continue; + } + + curParent = curNode->GetParentNode(); + int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; + + // some logic for putting list items into nested lists... + if (HTMLEditUtils::IsList(curParent)) { + sibling = nullptr; + + // Check for whether we should join a list that follows curNode. + // We do this if the next element is a list, and the list is of the + // same type (li/ol) as curNode was a part it. + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetNextHTMLSibling(curNode); + if (sibling && HTMLEditUtils::IsList(sibling) && + curParent->NodeInfo()->NameAtom() == + sibling->NodeInfo()->NameAtom() && + curParent->NodeInfo()->NamespaceID() == + sibling->NodeInfo()->NamespaceID()) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, sibling, 0); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + // Check for whether we should join a list that preceeds curNode. + // We do this if the previous element is a list, and the list is of + // the same type (li/ol) as curNode was a part of. + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); + if (sibling && HTMLEditUtils::IsList(sibling) && + curParent->NodeInfo()->NameAtom() == + sibling->NodeInfo()->NameAtom() && + curParent->NodeInfo()->NamespaceID() == + sibling->NodeInfo()->NamespaceID()) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, sibling, -1); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + sibling = nullptr; + + // check to see if curList is still appropriate. Which it is if + // curNode is still right after it in the same list. + if (curList) { + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); + } + + if (!curList || (sibling && sibling != curList)) { + // create a new nested list of correct type + rv = + SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(), + curParent, offset); + NS_ENSURE_STATE(curList); + // curList is now the correct thing to put curNode in + // remember our new block for postprocessing + mNewBlock = curList; + } + // tuck the node into the end of the active list + uint32_t listLen = curList->Length(); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, curList, listLen); + NS_ENSURE_SUCCESS(rv, rv); + } + // Not a list item. + else { + if (curNode && IsBlockNode(*curNode)) { + ChangeIndentation(*curNode->AsElement(), Change::plus); + curQuote = nullptr; + } else { + if (!curQuote) { + // First, check that our element can contain a div. + if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::div)) { + return NS_OK; // cancelled + } + + rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + curQuote = mHTMLEditor->CreateNode(nsGkAtoms::div, curParent, + offset); + NS_ENSURE_STATE(curQuote); + ChangeIndentation(*curQuote, Change::plus); + // remember our new block for postprocessing + mNewBlock = curQuote; + // curQuote is now the correct thing to put curNode in + } + + // tuck the node into the end of the active blockquote + uint32_t quoteLen = curQuote->Length(); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, curQuote, quoteLen); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::WillHTMLIndent(Selection* aSelection, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + WillInsert(*aSelection, aCancel); + + // initialize out param + // we want to ignore result of WillInsert() + *aCancel = false; + *aHandled = true; + + nsresult rv = NormalizeSelection(aSelection); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); + + // convert the selection ranges into "promoted" selection ranges: + // this basically just expands the range to include the immediate + // block parent, and then further expands to include any ancestors + // whose children are all in the range + + nsTArray<RefPtr<nsRange>> arrayOfRanges; + GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::indent); + + // use these ranges to contruct a list of nodes to act on. + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, EditAction::indent); + NS_ENSURE_SUCCESS(rv, rv); + + // if nothing visible in list, make an empty block + if (ListIsEmptyLine(arrayOfNodes)) { + // get selection location + NS_ENSURE_STATE(aSelection->RangeCount()); + nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent(); + int32_t offset = aSelection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_STATE(parent); + + // make sure we can put a block here + rv = SplitAsNeeded(*nsGkAtoms::blockquote, parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, + parent, offset); + NS_ENSURE_STATE(theBlock); + // remember our new block for postprocessing + mNewBlock = theBlock; + // delete anything that was in the list of nodes + while (!arrayOfNodes.IsEmpty()) { + OwningNonNull<nsINode> curNode = arrayOfNodes[0]; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(curNode); + NS_ENSURE_SUCCESS(rv, rv); + arrayOfNodes.RemoveElementAt(0); + } + // put selection in new block + *aHandled = true; + rv = aSelection->Collapse(theBlock, 0); + // Don't restore the selection + selectionRestorer.Abort(); + return rv; + } + + // Ok, now go through all the nodes and put them in a blockquote, + // or whatever is appropriate. Wohoo! + nsCOMPtr<nsINode> curParent; + nsCOMPtr<nsIContent> sibling; + nsCOMPtr<Element> curList, curQuote, indentedLI; + int32_t listCount = arrayOfNodes.Length(); + for (int32_t i = 0; i < listCount; i++) { + // here's where we actually figure out what to do + NS_ENSURE_STATE(arrayOfNodes[i]->IsContent()); + nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent(); + + // Ignore all non-editable nodes. Leave them be. + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsEditable(curNode)) { + continue; + } + + curParent = curNode->GetParentNode(); + int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; + + // some logic for putting list items into nested lists... + if (HTMLEditUtils::IsList(curParent)) { + sibling = nullptr; + + // Check for whether we should join a list that follows curNode. + // We do this if the next element is a list, and the list is of the + // same type (li/ol) as curNode was a part it. + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetNextHTMLSibling(curNode); + if (sibling && HTMLEditUtils::IsList(sibling) && + curParent->NodeInfo()->NameAtom() == + sibling->NodeInfo()->NameAtom() && + curParent->NodeInfo()->NamespaceID() == + sibling->NodeInfo()->NamespaceID()) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, sibling, 0); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + + // Check for whether we should join a list that preceeds curNode. + // We do this if the previous element is a list, and the list is of + // the same type (li/ol) as curNode was a part of. + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); + if (sibling && HTMLEditUtils::IsList(sibling) && + curParent->NodeInfo()->NameAtom() == + sibling->NodeInfo()->NameAtom() && + curParent->NodeInfo()->NamespaceID() == + sibling->NodeInfo()->NamespaceID()) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, sibling, -1); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + + sibling = nullptr; + + // check to see if curList is still appropriate. Which it is if + // curNode is still right after it in the same list. + if (curList) { + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); + } + + if (!curList || (sibling && sibling != curList)) { + // create a new nested list of correct type + rv = + SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(), + curParent, offset); + NS_ENSURE_STATE(curList); + // curList is now the correct thing to put curNode in + // remember our new block for postprocessing + mNewBlock = curList; + } + // tuck the node into the end of the active list + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, curList, -1); + NS_ENSURE_SUCCESS(rv, rv); + // forget curQuote, if any + curQuote = nullptr; + } + // Not a list item, use blockquote? + else { + // if we are inside a list item, we don't want to blockquote, we want + // to sublist the list item. We may have several nodes listed in the + // array of nodes to act on, that are in the same list item. Since + // we only want to indent that li once, we must keep track of the most + // recent indented list item, and not indent it if we find another node + // to act on that is still inside the same li. + nsCOMPtr<Element> listItem = IsInListItem(curNode); + if (listItem) { + if (indentedLI == listItem) { + // already indented this list item + continue; + } + curParent = listItem->GetParentNode(); + offset = curParent ? curParent->IndexOf(listItem) : -1; + // check to see if curList is still appropriate. Which it is if + // curNode is still right after it in the same list. + if (curList) { + sibling = nullptr; + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); + } + + if (!curList || (sibling && sibling != curList)) { + // create a new nested list of correct type + rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, + offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(), + curParent, offset); + NS_ENSURE_STATE(curList); + } + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(listItem, curList, -1); + NS_ENSURE_SUCCESS(rv, rv); + // remember we indented this li + indentedLI = listItem; + } else { + // need to make a blockquote to put things in if we haven't already, + // or if this node doesn't go in blockquote we used earlier. + // One reason it might not go in prio blockquote is if we are now + // in a different table cell. + if (curQuote && InDifferentTableElements(curQuote, curNode)) { + curQuote = nullptr; + } + + if (!curQuote) { + // First, check that our element can contain a blockquote. + if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::blockquote)) { + return NS_OK; // cancelled + } + + rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + curQuote = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent, + offset); + NS_ENSURE_STATE(curQuote); + // remember our new block for postprocessing + mNewBlock = curQuote; + // curQuote is now the correct thing to put curNode in + } + + // tuck the node into the end of the active blockquote + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(curNode, curQuote, -1); + NS_ENSURE_SUCCESS(rv, rv); + // forget curList, if any + curList = nullptr; + } + } + } + return NS_OK; +} + + +nsresult +HTMLEditRules::WillOutdent(Selection& aSelection, + bool* aCancel, + bool* aHandled) +{ + MOZ_ASSERT(aCancel && aHandled); + *aCancel = false; + *aHandled = true; + nsCOMPtr<nsIContent> rememberedLeftBQ, rememberedRightBQ; + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + bool useCSS = htmlEditor->IsCSSEnabled(); + + nsresult rv = NormalizeSelection(&aSelection); + NS_ENSURE_SUCCESS(rv, rv); + + // Some scoping for selection resetting - we may need to tweak it + { + AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor); + + // Convert the selection ranges into "promoted" selection ranges: this + // basically just expands the range to include the immediate block parent, + // and then further expands to include any ancestors whose children are all + // in the range + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + rv = GetNodesFromSelection(aSelection, EditAction::outdent, arrayOfNodes); + NS_ENSURE_SUCCESS(rv, rv); + + // Okay, now go through all the nodes and remove a level of blockquoting, + // or whatever is appropriate. Wohoo! + + nsCOMPtr<Element> curBlockQuote; + nsCOMPtr<nsIContent> firstBQChild, lastBQChild; + bool curBlockQuoteIsIndentedWithCSS = false; + for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) { + if (!arrayOfNodes[i]->IsContent()) { + continue; + } + OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent(); + + // Here's where we actually figure out what to do + nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); + int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; + + // Is it a blockquote? + if (curNode->IsHTMLElement(nsGkAtoms::blockquote)) { + // If it is a blockquote, remove it. So we need to finish up dealng + // with any curBlockQuote first. + if (curBlockQuote) { + rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild, + curBlockQuoteIsIndentedWithCSS, + getter_AddRefs(rememberedLeftBQ), + getter_AddRefs(rememberedRightBQ)); + NS_ENSURE_SUCCESS(rv, rv); + curBlockQuote = nullptr; + firstBQChild = nullptr; + lastBQChild = nullptr; + curBlockQuoteIsIndentedWithCSS = false; + } + rv = htmlEditor->RemoveBlockContainer(curNode); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + // Is it a block with a 'margin' property? + if (useCSS && IsBlockNode(curNode)) { + nsIAtom& marginProperty = + MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode); + nsAutoString value; + htmlEditor->mCSSEditUtils->GetSpecifiedProperty(curNode, + marginProperty, + value); + float f; + nsCOMPtr<nsIAtom> unit; + NS_ENSURE_STATE(htmlEditor); + htmlEditor->mCSSEditUtils->ParseLength(value, &f, + getter_AddRefs(unit)); + if (f > 0) { + ChangeIndentation(*curNode->AsElement(), Change::minus); + continue; + } + } + // Is it a list item? + if (HTMLEditUtils::IsListItem(curNode)) { + // If it is a list item, that means we are not outdenting whole list. + // So we need to finish up dealing with any curBlockQuote, and then pop + // this list item. + if (curBlockQuote) { + rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild, + curBlockQuoteIsIndentedWithCSS, + getter_AddRefs(rememberedLeftBQ), + getter_AddRefs(rememberedRightBQ)); + NS_ENSURE_SUCCESS(rv, rv); + curBlockQuote = nullptr; + firstBQChild = nullptr; + lastBQChild = nullptr; + curBlockQuoteIsIndentedWithCSS = false; + } + bool unused; + rv = PopListItem(GetAsDOMNode(curNode), &unused); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + // Do we have a blockquote that we are already committed to removing? + if (curBlockQuote) { + // If so, is this node a descendant? + if (EditorUtils::IsDescendantOf(curNode, curBlockQuote)) { + lastBQChild = curNode; + // Then we don't need to do anything different for this node + continue; + } + // Otherwise, we have progressed beyond end of curBlockQuote, so + // let's handle it now. We need to remove the portion of + // curBlockQuote that contains [firstBQChild - lastBQChild]. + rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild, + curBlockQuoteIsIndentedWithCSS, + getter_AddRefs(rememberedLeftBQ), + getter_AddRefs(rememberedRightBQ)); + NS_ENSURE_SUCCESS(rv, rv); + curBlockQuote = nullptr; + firstBQChild = nullptr; + lastBQChild = nullptr; + curBlockQuoteIsIndentedWithCSS = false; + // Fall out and handle curNode + } + + // Are we inside a blockquote? + OwningNonNull<nsINode> n = curNode; + curBlockQuoteIsIndentedWithCSS = false; + // Keep looking up the hierarchy as long as we don't hit the body or the + // active editing host or a table element (other than an entire table) + while (!n->IsHTMLElement(nsGkAtoms::body) && + htmlEditor->IsDescendantOfEditorRoot(n) && + (n->IsHTMLElement(nsGkAtoms::table) || + !HTMLEditUtils::IsTableElement(n))) { + if (!n->GetParentNode()) { + break; + } + n = *n->GetParentNode(); + if (n->IsHTMLElement(nsGkAtoms::blockquote)) { + // If so, remember it and the first node we are taking out of it. + curBlockQuote = n->AsElement(); + firstBQChild = curNode; + lastBQChild = curNode; + break; + } else if (useCSS) { + nsIAtom& marginProperty = + MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode); + nsAutoString value; + htmlEditor->mCSSEditUtils->GetSpecifiedProperty(*n, marginProperty, + value); + float f; + nsCOMPtr<nsIAtom> unit; + htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit)); + if (f > 0 && !(HTMLEditUtils::IsList(curParent) && + HTMLEditUtils::IsList(curNode))) { + curBlockQuote = n->AsElement(); + firstBQChild = curNode; + lastBQChild = curNode; + curBlockQuoteIsIndentedWithCSS = true; + break; + } + } + } + + if (!curBlockQuote) { + // Couldn't find enclosing blockquote. Handle list cases. + if (HTMLEditUtils::IsList(curParent)) { + // Move node out of list + if (HTMLEditUtils::IsList(curNode)) { + // Just unwrap this sublist + rv = htmlEditor->RemoveBlockContainer(curNode); + NS_ENSURE_SUCCESS(rv, rv); + } + // handled list item case above + } else if (HTMLEditUtils::IsList(curNode)) { + // node is a list, but parent is non-list: move list items out + nsCOMPtr<nsIContent> child = curNode->GetLastChild(); + while (child) { + if (HTMLEditUtils::IsListItem(child)) { + bool unused; + rv = PopListItem(GetAsDOMNode(child), &unused); + NS_ENSURE_SUCCESS(rv, rv); + } else if (HTMLEditUtils::IsList(child)) { + // We have an embedded list, so move it out from under the parent + // list. Be sure to put it after the parent list because this + // loop iterates backwards through the parent's list of children. + + rv = htmlEditor->MoveNode(child, curParent, offset + 1); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Delete any non-list items for now + rv = htmlEditor->DeleteNode(child); + NS_ENSURE_SUCCESS(rv, rv); + } + child = curNode->GetLastChild(); + } + // Delete the now-empty list + rv = htmlEditor->RemoveBlockContainer(curNode); + NS_ENSURE_SUCCESS(rv, rv); + } else if (useCSS) { + nsCOMPtr<Element> element; + if (curNode->GetAsText()) { + // We want to outdent the parent of text nodes + element = curNode->GetParentElement(); + } else if (curNode->IsElement()) { + element = curNode->AsElement(); + } + if (element) { + ChangeIndentation(*element, Change::minus); + } + } + } + } + if (curBlockQuote) { + // We have a blockquote we haven't finished handling + rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild, + curBlockQuoteIsIndentedWithCSS, + getter_AddRefs(rememberedLeftBQ), + getter_AddRefs(rememberedRightBQ)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + // Make sure selection didn't stick to last piece of content in old bq (only + // a problem for collapsed selections) + if (rememberedLeftBQ || rememberedRightBQ) { + if (aSelection.Collapsed()) { + // Push selection past end of rememberedLeftBQ + NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_OK); + nsCOMPtr<nsINode> startNode = aSelection.GetRangeAt(0)->GetStartParent(); + int32_t startOffset = aSelection.GetRangeAt(0)->StartOffset(); + if (rememberedLeftBQ && + (startNode == rememberedLeftBQ || + EditorUtils::IsDescendantOf(startNode, rememberedLeftBQ))) { + // Selection is inside rememberedLeftBQ - push it past it. + startNode = rememberedLeftBQ->GetParentNode(); + startOffset = startNode ? 1 + startNode->IndexOf(rememberedLeftBQ) : 0; + aSelection.Collapse(startNode, startOffset); + } + // And pull selection before beginning of rememberedRightBQ + startNode = aSelection.GetRangeAt(0)->GetStartParent(); + startOffset = aSelection.GetRangeAt(0)->StartOffset(); + if (rememberedRightBQ && + (startNode == rememberedRightBQ || + EditorUtils::IsDescendantOf(startNode, rememberedRightBQ))) { + // Selection is inside rememberedRightBQ - push it before it. + startNode = rememberedRightBQ->GetParentNode(); + startOffset = startNode ? startNode->IndexOf(rememberedRightBQ) : -1; + aSelection.Collapse(startNode, startOffset); + } + } + return NS_OK; + } + return NS_OK; +} + + +/** + * RemovePartOfBlock() splits aBlock and move aStartChild to aEndChild out of + * aBlock. + */ +nsresult +HTMLEditRules::RemovePartOfBlock(Element& aBlock, + nsIContent& aStartChild, + nsIContent& aEndChild) +{ + SplitBlock(aBlock, aStartChild, aEndChild); + // Get rid of part of blockquote we are outdenting + + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->RemoveBlockContainer(aBlock); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void +HTMLEditRules::SplitBlock(Element& aBlock, + nsIContent& aStartChild, + nsIContent& aEndChild, + nsIContent** aOutLeftNode, + nsIContent** aOutRightNode, + nsIContent** aOutMiddleNode) +{ + // aStartChild and aEndChild must be exclusive descendants of aBlock + MOZ_ASSERT(EditorUtils::IsDescendantOf(&aStartChild, &aBlock) && + EditorUtils::IsDescendantOf(&aEndChild, &aBlock)); + NS_ENSURE_TRUE_VOID(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Get split point location + OwningNonNull<nsIContent> startParent = *aStartChild.GetParent(); + int32_t startOffset = startParent->IndexOf(&aStartChild); + + // Do the splits! + nsCOMPtr<nsIContent> newMiddleNode1; + htmlEditor->SplitNodeDeep(aBlock, startParent, startOffset, + HTMLEditor::EmptyContainers::no, + aOutLeftNode, getter_AddRefs(newMiddleNode1)); + + // Get split point location + OwningNonNull<nsIContent> endParent = *aEndChild.GetParent(); + // +1 because we want to be after the child + int32_t endOffset = 1 + endParent->IndexOf(&aEndChild); + + // Do the splits! + nsCOMPtr<nsIContent> newMiddleNode2; + htmlEditor->SplitNodeDeep(aBlock, endParent, endOffset, + HTMLEditor::EmptyContainers::no, + getter_AddRefs(newMiddleNode2), aOutRightNode); + + if (aOutMiddleNode) { + if (newMiddleNode2) { + newMiddleNode2.forget(aOutMiddleNode); + } else { + newMiddleNode1.forget(aOutMiddleNode); + } + } +} + +nsresult +HTMLEditRules::OutdentPartOfBlock(Element& aBlock, + nsIContent& aStartChild, + nsIContent& aEndChild, + bool aIsBlockIndentedWithCSS, + nsIContent** aOutLeftNode, + nsIContent** aOutRightNode) +{ + MOZ_ASSERT(aOutLeftNode && aOutRightNode); + + nsCOMPtr<nsIContent> middleNode; + SplitBlock(aBlock, aStartChild, aEndChild, aOutLeftNode, aOutRightNode, + getter_AddRefs(middleNode)); + + NS_ENSURE_STATE(middleNode); + + if (!aIsBlockIndentedWithCSS) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->RemoveBlockContainer(*middleNode); + NS_ENSURE_SUCCESS(rv, rv); + } else if (middleNode->IsElement()) { + // We do nothing if middleNode isn't an element + nsresult rv = ChangeIndentation(*middleNode->AsElement(), Change::minus); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +/** + * ConvertListType() converts list type and list item type. + */ +nsresult +HTMLEditRules::ConvertListType(Element* aList, + Element** aOutList, + nsIAtom* aListType, + nsIAtom* aItemType) +{ + MOZ_ASSERT(aList); + MOZ_ASSERT(aOutList); + MOZ_ASSERT(aListType); + MOZ_ASSERT(aItemType); + + nsCOMPtr<nsINode> child = aList->GetFirstChild(); + while (child) { + if (child->IsElement()) { + dom::Element* element = child->AsElement(); + if (HTMLEditUtils::IsListItem(element) && + !element->IsHTMLElement(aItemType)) { + child = mHTMLEditor->ReplaceContainer(element, aItemType); + NS_ENSURE_STATE(child); + } else if (HTMLEditUtils::IsList(element) && + !element->IsHTMLElement(aListType)) { + nsCOMPtr<dom::Element> temp; + nsresult rv = ConvertListType(child->AsElement(), getter_AddRefs(temp), + aListType, aItemType); + NS_ENSURE_SUCCESS(rv, rv); + child = temp.forget(); + } + } + child = child->GetNextSibling(); + } + + if (aList->IsHTMLElement(aListType)) { + nsCOMPtr<dom::Element> list = aList->AsElement(); + list.forget(aOutList); + return NS_OK; + } + + *aOutList = mHTMLEditor->ReplaceContainer(aList, aListType).take(); + NS_ENSURE_STATE(aOutList); + + return NS_OK; +} + + +/** + * CreateStyleForInsertText() takes care of clearing and setting appropriate + * style nodes for text insertion. + */ +nsresult +HTMLEditRules::CreateStyleForInsertText(Selection& aSelection, + nsIDocument& aDoc) +{ + MOZ_ASSERT(mHTMLEditor->mTypeInState); + + bool weDidSomething = false; + NS_ENSURE_STATE(aSelection.GetRangeAt(0)); + nsCOMPtr<nsINode> node = aSelection.GetRangeAt(0)->GetStartParent(); + int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); + + // next examine our present style and make sure default styles are either + // present or explicitly overridden. If neither, add the default style to + // the TypeInState + int32_t length = mHTMLEditor->mDefaultStyles.Length(); + for (int32_t j = 0; j < length; j++) { + PropItem* propItem = mHTMLEditor->mDefaultStyles[j]; + MOZ_ASSERT(propItem); + bool bFirst, bAny, bAll; + + // GetInlineProperty also examine TypeInState. The only gotcha here is + // that a cleared property looks like an unset property. For now I'm + // assuming that's not a problem: that default styles will always be + // multivalue styles (like font face or size) where clearing the style + // means we want to go back to the default. If we ever wanted a "toggle" + // style like bold for a default, though, I'll have to add code to detect + // the difference between unset and explicitly cleared, else user would + // never be able to unbold, for instance. + nsAutoString curValue; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetInlinePropertyBase(*propItem->tag, &propItem->attr, + nullptr, &bFirst, &bAny, &bAll, + &curValue, false); + NS_ENSURE_SUCCESS(rv, rv); + + if (!bAny) { + // no style set for this prop/attr + mHTMLEditor->mTypeInState->SetProp(propItem->tag, propItem->attr, + propItem->value); + } + } + + nsCOMPtr<Element> rootElement = aDoc.GetRootElement(); + NS_ENSURE_STATE(rootElement); + + // process clearing any styles first + nsAutoPtr<PropItem> item(mHTMLEditor->mTypeInState->TakeClearProperty()); + while (item && node != rootElement) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->ClearStyle(address_of(node), &offset, + item->tag, &item->attr); + NS_ENSURE_SUCCESS(rv, rv); + item = mHTMLEditor->mTypeInState->TakeClearProperty(); + weDidSomething = true; + } + + // then process setting any styles + int32_t relFontSize = mHTMLEditor->mTypeInState->TakeRelativeFontSize(); + item = mHTMLEditor->mTypeInState->TakeSetProperty(); + + if (item || relFontSize) { + // we have at least one style to add; make a new text node to insert style + // nodes above. + if (RefPtr<Text> text = node->GetAsText()) { + // if we are in a text node, split it + NS_ENSURE_STATE(mHTMLEditor); + offset = mHTMLEditor->SplitNodeDeep(*text, *text, offset); + NS_ENSURE_STATE(offset != -1); + node = node->GetParentNode(); + } + if (!mHTMLEditor->IsContainer(node)) { + return NS_OK; + } + OwningNonNull<Text> newNode = aDoc.CreateTextNode(EmptyString()); + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->InsertNode(*newNode, *node, offset); + NS_ENSURE_SUCCESS(rv, rv); + node = newNode; + offset = 0; + weDidSomething = true; + + if (relFontSize) { + // dir indicated bigger versus smaller. 1 = bigger, -1 = smaller + HTMLEditor::FontSize dir = relFontSize > 0 ? + HTMLEditor::FontSize::incr : HTMLEditor::FontSize::decr; + for (int32_t j = 0; j < DeprecatedAbs(relFontSize); j++) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->RelativeFontChangeOnTextNode(dir, newNode, 0, -1); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + while (item) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->SetInlinePropertyOnNode(*node->AsContent(), + *item->tag, &item->attr, + item->value); + NS_ENSURE_SUCCESS(rv, rv); + item = mHTMLEditor->mTypeInState->TakeSetProperty(); + } + } + if (weDidSomething) { + return aSelection.Collapse(node, offset); + } + + return NS_OK; +} + + +/** + * Figure out if aNode is (or is inside) an empty block. A block can have + * children and still be considered empty, if the children are empty or + * non-editable. + */ +nsresult +HTMLEditRules::IsEmptyBlock(Element& aNode, + bool* aOutIsEmptyBlock, + MozBRCounts aMozBRCounts) +{ + MOZ_ASSERT(aOutIsEmptyBlock); + *aOutIsEmptyBlock = true; + + NS_ENSURE_TRUE(IsBlockNode(aNode), NS_ERROR_NULL_POINTER); + + return mHTMLEditor->IsEmptyNode(aNode.AsDOMNode(), aOutIsEmptyBlock, + aMozBRCounts == MozBRCounts::yes ? false + : true); +} + + +nsresult +HTMLEditRules::WillAlign(Selection& aSelection, + const nsAString& aAlignType, + bool* aCancel, + bool* aHandled) +{ + MOZ_ASSERT(aCancel && aHandled); + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + WillInsert(aSelection, aCancel); + + // Initialize out param. We want to ignore result of WillInsert(). + *aCancel = false; + *aHandled = false; + + nsresult rv = NormalizeSelection(&aSelection); + NS_ENSURE_SUCCESS(rv, rv); + AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor); + + // Convert the selection ranges into "promoted" selection ranges: This + // basically just expands the range to include the immediate block parent, + // and then further expands to include any ancestors whose children are all + // in the range + *aHandled = true; + nsTArray<OwningNonNull<nsINode>> nodeArray; + rv = GetNodesFromSelection(aSelection, EditAction::align, nodeArray); + NS_ENSURE_SUCCESS(rv, rv); + + // If we don't have any nodes, or we have only a single br, then we are + // creating an empty alignment div. We have to do some different things for + // these. + bool emptyDiv = nodeArray.IsEmpty(); + if (nodeArray.Length() == 1) { + OwningNonNull<nsINode> node = nodeArray[0]; + + if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(node))) { + // The node is a table element, an hr, a paragraph, a div or a section + // header; in HTML 4, it can directly carry the ALIGN attribute and we + // don't need to make a div! If we are in CSS mode, all the work is done + // in AlignBlock + rv = AlignBlock(*node->AsElement(), aAlignType, ContentsOnly::yes); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + if (TextEditUtils::IsBreak(node)) { + // The special case emptyDiv code (below) that consumes BRs can cause + // tables to split if the start node of the selection is not in a table + // cell or caption, for example parent is a <tr>. Avoid this unnecessary + // splitting if possible by leaving emptyDiv FALSE so that we fall + // through to the normal case alignment code. + // + // XXX: It seems a little error prone for the emptyDiv special case code + // to assume that the start node of the selection is the parent of the + // single node in the nodeArray, as the paragraph above points out. Do we + // rely on the selection start node because of the fact that nodeArray + // can be empty? We should probably revisit this issue. - kin + + NS_ENSURE_STATE(aSelection.GetRangeAt(0) && + aSelection.GetRangeAt(0)->GetStartParent()); + OwningNonNull<nsINode> parent = + *aSelection.GetRangeAt(0)->GetStartParent(); + + emptyDiv = !HTMLEditUtils::IsTableElement(parent) || + HTMLEditUtils::IsTableCellOrCaption(parent); + } + } + if (emptyDiv) { + nsCOMPtr<nsINode> parent = + aSelection.GetRangeAt(0) ? aSelection.GetRangeAt(0)->GetStartParent() + : nullptr; + NS_ENSURE_STATE(parent); + int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); + + rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + // Consume a trailing br, if any. This is to keep an alignment from + // creating extra lines, if possible. + nsCOMPtr<nsIContent> brContent = + htmlEditor->GetNextHTMLNode(parent, offset); + if (brContent && TextEditUtils::IsBreak(brContent)) { + // Making use of html structure... if next node after where we are + // putting our div is not a block, then the br we found is in same block + // we are, so it's safe to consume it. + nsCOMPtr<nsIContent> sibling = htmlEditor->GetNextHTMLSibling(parent, + offset); + if (sibling && !IsBlockNode(*sibling)) { + rv = htmlEditor->DeleteNode(brContent); + NS_ENSURE_SUCCESS(rv, rv); + } + } + nsCOMPtr<Element> div = htmlEditor->CreateNode(nsGkAtoms::div, parent, + offset); + NS_ENSURE_STATE(div); + // Remember our new block for postprocessing + mNewBlock = div; + // Set up the alignment on the div, using HTML or CSS + rv = AlignBlock(*div, aAlignType, ContentsOnly::yes); + NS_ENSURE_SUCCESS(rv, rv); + *aHandled = true; + // Put in a moz-br so that it won't get deleted + rv = CreateMozBR(div->AsDOMNode(), 0); + NS_ENSURE_SUCCESS(rv, rv); + rv = aSelection.Collapse(div, 0); + // Don't restore the selection + selectionRestorer.Abort(); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + // Next we detect all the transitions in the array, where a transition + // means that adjacent nodes in the array don't have the same parent. + + nsTArray<bool> transitionList; + MakeTransitionList(nodeArray, transitionList); + + // Okay, now go through all the nodes and give them an align attrib or put + // them in a div, or whatever is appropriate. Woohoo! + + nsCOMPtr<Element> curDiv; + bool useCSS = htmlEditor->IsCSSEnabled(); + for (size_t i = 0; i < nodeArray.Length(); i++) { + auto& curNode = nodeArray[i]; + // Here's where we actually figure out what to do + + // Ignore all non-editable nodes. Leave them be. + if (!htmlEditor->IsEditable(curNode)) { + continue; + } + + // The node is a table element, an hr, a paragraph, a div or a section + // header; in HTML 4, it can directly carry the ALIGN attribute and we + // don't need to nest it, just set the alignment. In CSS, assign the + // corresponding CSS styles in AlignBlock + if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(curNode))) { + rv = AlignBlock(*curNode->AsElement(), aAlignType, ContentsOnly::no); + NS_ENSURE_SUCCESS(rv, rv); + // Clear out curDiv so that we don't put nodes after this one into it + curDiv = nullptr; + continue; + } + + nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); + int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; + + // Skip insignificant formatting text nodes to prevent unnecessary + // structure splitting! + bool isEmptyTextNode = false; + if (curNode->GetAsText() && + ((HTMLEditUtils::IsTableElement(curParent) && + !HTMLEditUtils::IsTableCellOrCaption(*curParent)) || + HTMLEditUtils::IsList(curParent) || + (NS_SUCCEEDED(htmlEditor->IsEmptyNode(curNode, &isEmptyTextNode)) && + isEmptyTextNode))) { + continue; + } + + // If it's a list item, or a list inside a list, forget any "current" div, + // and instead put divs inside the appropriate block (td, li, etc.) + if (HTMLEditUtils::IsListItem(curNode) || + HTMLEditUtils::IsList(curNode)) { + rv = RemoveAlignment(GetAsDOMNode(curNode), aAlignType, true); + NS_ENSURE_SUCCESS(rv, rv); + if (useCSS) { + htmlEditor->mCSSEditUtils->SetCSSEquivalentToHTMLStyle( + curNode->AsElement(), nullptr, &NS_LITERAL_STRING("align"), + &aAlignType, false); + curDiv = nullptr; + continue; + } + if (HTMLEditUtils::IsList(curParent)) { + // If we don't use CSS, add a contraint to list element: they have to + // be inside another list, i.e., >= second level of nesting + rv = AlignInnerBlocks(*curNode, &aAlignType); + NS_ENSURE_SUCCESS(rv, rv); + curDiv = nullptr; + continue; + } + // Clear out curDiv so that we don't put nodes after this one into it + } + + // Need to make a div to put things in if we haven't already, or if this + // node doesn't go in div we used earlier. + if (!curDiv || transitionList[i]) { + // First, check that our element can contain a div. + if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::div)) { + // Cancelled + return NS_OK; + } + + rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + curDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent, offset); + NS_ENSURE_STATE(curDiv); + // Remember our new block for postprocessing + mNewBlock = curDiv; + // Set up the alignment on the div + rv = AlignBlock(*curDiv, aAlignType, ContentsOnly::yes); + } + + NS_ENSURE_STATE(curNode->IsContent()); + + // Tuck the node into the end of the active div + rv = htmlEditor->MoveNode(curNode->AsContent(), curDiv, -1); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + + +/** + * AlignInnerBlocks() aligns inside table cells or list items. + */ +nsresult +HTMLEditRules::AlignInnerBlocks(nsINode& aNode, + const nsAString* alignType) +{ + NS_ENSURE_TRUE(alignType, NS_ERROR_NULL_POINTER); + + // Gather list of table cells or list items + nsTArray<OwningNonNull<nsINode>> nodeArray; + TableCellAndListItemFunctor functor; + DOMIterator iter(aNode); + iter.AppendList(functor, nodeArray); + + // Now that we have the list, align their contents as requested + for (auto& node : nodeArray) { + nsresult rv = AlignBlockContents(GetAsDOMNode(node), alignType); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + + +/** + * AlignBlockContents() aligns contents of a block element. + */ +nsresult +HTMLEditRules::AlignBlockContents(nsIDOMNode* aNode, + const nsAString* alignType) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node && alignType, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIContent> firstChild, lastChild; + nsCOMPtr<Element> divNode; + + bool useCSS = mHTMLEditor->IsCSSEnabled(); + + NS_ENSURE_STATE(mHTMLEditor); + firstChild = mHTMLEditor->GetFirstEditableChild(*node); + NS_ENSURE_STATE(mHTMLEditor); + lastChild = mHTMLEditor->GetLastEditableChild(*node); + NS_NAMED_LITERAL_STRING(attr, "align"); + if (!firstChild) { + // this cell has no content, nothing to align + } else if (firstChild == lastChild && + firstChild->IsHTMLElement(nsGkAtoms::div)) { + // the cell already has a div containing all of its content: just + // act on this div. + nsCOMPtr<nsIDOMElement> divElem = do_QueryInterface(firstChild); + if (useCSS) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->SetAttributeOrEquivalent(divElem, attr, + *alignType, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->SetAttribute(divElem, attr, *alignType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } else { + // else we need to put in a div, set the alignment, and toss in all the children + NS_ENSURE_STATE(mHTMLEditor); + divNode = mHTMLEditor->CreateNode(nsGkAtoms::div, node, 0); + NS_ENSURE_STATE(divNode); + // set up the alignment on the div + nsCOMPtr<nsIDOMElement> divElem = do_QueryInterface(divNode); + if (useCSS) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->SetAttributeOrEquivalent(divElem, attr, *alignType, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->SetAttribute(divElem, attr, *alignType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + // tuck the children into the end of the active div + while (lastChild && (lastChild != divNode)) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->MoveNode(lastChild, divNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + lastChild = mHTMLEditor->GetLastEditableChild(*node); + } + } + return NS_OK; +} + +/** + * CheckForEmptyBlock() is called by WillDeleteSelection() to detect and handle + * case of deleting from inside an empty block. + */ +nsresult +HTMLEditRules::CheckForEmptyBlock(nsINode* aStartNode, + Element* aBodyNode, + Selection* aSelection, + nsIEditor::EDirection aAction, + bool* aHandled) +{ + // If the editing host is an inline element, bail out early. + if (aBodyNode && IsInlineNode(*aBodyNode)) { + return NS_OK; + } + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // If we are inside an empty block, delete it. Note: do NOT delete table + // elements this way. + nsCOMPtr<Element> block = htmlEditor->GetBlock(*aStartNode); + bool bIsEmptyNode; + nsCOMPtr<Element> emptyBlock; + if (block && block != aBodyNode) { + // Efficiency hack, avoiding IsEmptyNode() call when in body + nsresult rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false); + NS_ENSURE_SUCCESS(rv, rv); + while (block && bIsEmptyNode && !HTMLEditUtils::IsTableElement(block) && + block != aBodyNode) { + emptyBlock = block; + block = htmlEditor->GetBlockNodeParent(emptyBlock); + if (block) { + rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + if (emptyBlock && emptyBlock->IsEditable()) { + nsCOMPtr<nsINode> blockParent = emptyBlock->GetParentNode(); + NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE); + int32_t offset = blockParent->IndexOf(emptyBlock); + + if (HTMLEditUtils::IsListItem(emptyBlock)) { + // Are we the first list item in the list? + bool bIsFirst; + NS_ENSURE_STATE(htmlEditor); + nsresult rv = + htmlEditor->IsFirstEditableChild(GetAsDOMNode(emptyBlock), &bIsFirst); + NS_ENSURE_SUCCESS(rv, rv); + if (bIsFirst) { + nsCOMPtr<nsINode> listParent = blockParent->GetParentNode(); + NS_ENSURE_TRUE(listParent, NS_ERROR_FAILURE); + int32_t listOffset = listParent->IndexOf(blockParent); + // If we are a sublist, skip the br creation + if (!HTMLEditUtils::IsList(listParent)) { + // Create a br before list + NS_ENSURE_STATE(htmlEditor); + nsCOMPtr<Element> br = + htmlEditor->CreateBR(listParent, listOffset); + NS_ENSURE_STATE(br); + // Adjust selection to be right before it + rv = aSelection->Collapse(listParent, listOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + // Else just let selection percolate up. We'll adjust it in + // AfterEdit() + } + } else { + if (aAction == nsIEditor::eNext || aAction == nsIEditor::eNextWord || + aAction == nsIEditor::eToEndOfLine) { + // Move to the start of the next node, if any + nsCOMPtr<nsIContent> nextNode = htmlEditor->GetNextNode(blockParent, + offset + 1, true); + if (nextNode) { + EditorDOMPoint pt = GetGoodSelPointForNode(*nextNode, aAction); + nsresult rv = aSelection->Collapse(pt.node, pt.offset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Adjust selection to be right after it. + nsresult rv = aSelection->Collapse(blockParent, offset + 1); + NS_ENSURE_SUCCESS(rv, rv); + } + } else if (aAction == nsIEditor::ePrevious || + aAction == nsIEditor::ePreviousWord || + aAction == nsIEditor::eToBeginningOfLine) { + // Move to the end of the previous node + nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorNode(blockParent, + offset, + true); + if (priorNode) { + EditorDOMPoint pt = GetGoodSelPointForNode(*priorNode, aAction); + nsresult rv = aSelection->Collapse(pt.node, pt.offset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsresult rv = aSelection->Collapse(blockParent, offset + 1); + NS_ENSURE_SUCCESS(rv, rv); + } + } else if (aAction != nsIEditor::eNone) { + NS_RUNTIMEABORT("CheckForEmptyBlock doesn't support this action yet"); + } + } + NS_ENSURE_STATE(htmlEditor); + *aHandled = true; + nsresult rv = htmlEditor->DeleteNode(emptyBlock); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +Element* +HTMLEditRules::CheckForInvisibleBR(Element& aBlock, + BRLocation aWhere, + int32_t aOffset) +{ + nsCOMPtr<nsINode> testNode; + int32_t testOffset = 0; + + if (aWhere == BRLocation::blockEnd) { + // No block crossing + nsCOMPtr<nsIContent> rightmostNode = + mHTMLEditor->GetRightmostChild(&aBlock, true); + + if (!rightmostNode) { + return nullptr; + } + + testNode = rightmostNode->GetParentNode(); + // Use offset + 1, so last node is included in our evaluation + testOffset = testNode->IndexOf(rightmostNode) + 1; + } else if (aOffset) { + testNode = &aBlock; + // We'll check everything to the left of the input position + testOffset = aOffset; + } else { + return nullptr; + } + + WSRunObject wsTester(mHTMLEditor, testNode, testOffset); + if (WSType::br == wsTester.mStartReason) { + return wsTester.mStartReasonNode->AsElement(); + } + + return nullptr; +} + +/** + * aLists and aTables allow the caller to specify what kind of content to + * "look inside". If aTables is Tables::yes, look inside any table content, + * and insert the inner content into the supplied issupportsarray at offset + * aIndex. Similarly with aLists and list content. aIndex is updated to + * point past inserted elements. + */ +void +HTMLEditRules::GetInnerContent( + nsINode& aNode, + nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, + int32_t* aIndex, + Lists aLists, + Tables aTables) +{ + MOZ_ASSERT(aIndex); + + for (nsCOMPtr<nsIContent> node = mHTMLEditor->GetFirstEditableChild(aNode); + node; node = node->GetNextSibling()) { + if ((aLists == Lists::yes && (HTMLEditUtils::IsList(node) || + HTMLEditUtils::IsListItem(node))) || + (aTables == Tables::yes && HTMLEditUtils::IsTableElement(node))) { + GetInnerContent(*node, aOutArrayOfNodes, aIndex, aLists, aTables); + } else { + aOutArrayOfNodes.InsertElementAt(*aIndex, *node); + (*aIndex)++; + } + } +} + +/** + * Promotes selection to include blocks that have all their children selected. + */ +nsresult +HTMLEditRules::ExpandSelectionForDeletion(Selection& aSelection) +{ + // Don't need to touch collapsed selections + if (aSelection.Collapsed()) { + return NS_OK; + } + + // We don't need to mess with cell selections, and we assume multirange + // selections are those. + if (aSelection.RangeCount() != 1) { + return NS_OK; + } + + // Find current sel start and end + NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_ERROR_NULL_POINTER); + OwningNonNull<nsRange> range = *aSelection.GetRangeAt(0); + + nsCOMPtr<nsINode> selStartNode = range->GetStartParent(); + int32_t selStartOffset = range->StartOffset(); + nsCOMPtr<nsINode> selEndNode = range->GetEndParent(); + int32_t selEndOffset = range->EndOffset(); + + // Find current selection common block parent + nsCOMPtr<Element> selCommon = + HTMLEditor::GetBlock(*range->GetCommonAncestor()); + NS_ENSURE_STATE(selCommon); + + // Set up for loops and cache our root element + nsCOMPtr<nsINode> firstBRParent; + nsCOMPtr<nsINode> unused; + int32_t visOffset = 0, firstBROffset = 0; + WSType wsType; + nsCOMPtr<Element> root = mHTMLEditor->GetActiveEditingHost(); + NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); + + // Find previous visible thingy before start of selection + if (selStartNode != selCommon && selStartNode != root) { + while (true) { + WSRunObject wsObj(mHTMLEditor, selStartNode, selStartOffset); + wsObj.PriorVisibleNode(selStartNode, selStartOffset, address_of(unused), + &visOffset, &wsType); + if (wsType != WSType::thisBlock) { + break; + } + // We want to keep looking up. But stop if we are crossing table + // element boundaries, or if we hit the root. + if (HTMLEditUtils::IsTableElement(wsObj.mStartReasonNode) || + selCommon == wsObj.mStartReasonNode || + root == wsObj.mStartReasonNode) { + break; + } + selStartNode = wsObj.mStartReasonNode->GetParentNode(); + selStartOffset = selStartNode ? + selStartNode->IndexOf(wsObj.mStartReasonNode) : -1; + } + } + + // Find next visible thingy after end of selection + if (selEndNode != selCommon && selEndNode != root) { + for (;;) { + WSRunObject wsObj(mHTMLEditor, selEndNode, selEndOffset); + wsObj.NextVisibleNode(selEndNode, selEndOffset, address_of(unused), + &visOffset, &wsType); + if (wsType == WSType::br) { + if (mHTMLEditor->IsVisBreak(wsObj.mEndReasonNode)) { + break; + } + if (!firstBRParent) { + firstBRParent = selEndNode; + firstBROffset = selEndOffset; + } + selEndNode = wsObj.mEndReasonNode->GetParentNode(); + selEndOffset = selEndNode + ? selEndNode->IndexOf(wsObj.mEndReasonNode) + 1 : 0; + } else if (wsType == WSType::thisBlock) { + // We want to keep looking up. But stop if we are crossing table + // element boundaries, or if we hit the root. + if (HTMLEditUtils::IsTableElement(wsObj.mEndReasonNode) || + selCommon == wsObj.mEndReasonNode || + root == wsObj.mEndReasonNode) { + break; + } + selEndNode = wsObj.mEndReasonNode->GetParentNode(); + selEndOffset = 1 + selEndNode->IndexOf(wsObj.mEndReasonNode); + } else { + break; + } + } + } + // Now set the selection to the new range + aSelection.Collapse(selStartNode, selStartOffset); + + // Expand selection endpoint only if we didn't pass a br, or if we really + // needed to pass that br (i.e., its block is now totally selected) + bool doEndExpansion = true; + if (firstBRParent) { + // Find block node containing br + nsCOMPtr<Element> brBlock = HTMLEditor::GetBlock(*firstBRParent); + bool nodeBefore = false, nodeAfter = false; + + // Create a range that represents expanded selection + RefPtr<nsRange> range = new nsRange(selStartNode); + nsresult rv = range->SetStart(selStartNode, selStartOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->SetEnd(selEndNode, selEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // Check if block is entirely inside range + if (brBlock) { + nsRange::CompareNodeToRange(brBlock, range, &nodeBefore, &nodeAfter); + } + + // If block isn't contained, forgo grabbing the br in expanded selection + if (nodeBefore || nodeAfter) { + doEndExpansion = false; + } + } + if (doEndExpansion) { + nsresult rv = aSelection.Extend(selEndNode, selEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Only expand to just before br + nsresult rv = aSelection.Extend(firstBRParent, firstBROffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +/** + * NormalizeSelection() tweaks non-collapsed selections to be more "natural". + * Idea here is to adjust selection endpoint so that they do not cross breaks + * or block boundaries unless something editable beyond that boundary is also + * selected. This adjustment makes it much easier for the various block + * operations to determine what nodes to act on. + */ +nsresult +HTMLEditRules::NormalizeSelection(Selection* inSelection) +{ + NS_ENSURE_TRUE(inSelection, NS_ERROR_NULL_POINTER); + + // don't need to touch collapsed selections + if (inSelection->Collapsed()) { + return NS_OK; + } + + int32_t rangeCount; + nsresult rv = inSelection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, rv); + + // we don't need to mess with cell selections, and we assume multirange selections are those. + if (rangeCount != 1) { + return NS_OK; + } + + RefPtr<nsRange> range = inSelection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIDOMNode> startNode, endNode; + int32_t startOffset, endOffset; + nsCOMPtr<nsIDOMNode> newStartNode, newEndNode; + int32_t newStartOffset, newEndOffset; + + rv = range->GetStartContainer(getter_AddRefs(startNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetStartOffset(&startOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetEndContainer(getter_AddRefs(endNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetEndOffset(&endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // adjusted values default to original values + newStartNode = startNode; + newStartOffset = startOffset; + newEndNode = endNode; + newEndOffset = endOffset; + + // some locals we need for whitespace code + nsCOMPtr<nsINode> unused; + int32_t offset; + WSType wsType; + + // let the whitespace code do the heavy lifting + WSRunObject wsEndObj(mHTMLEditor, endNode, endOffset); + // is there any intervening visible whitespace? if so we can't push selection past that, + // it would visibly change maening of users selection + nsCOMPtr<nsINode> endNode_(do_QueryInterface(endNode)); + wsEndObj.PriorVisibleNode(endNode_, endOffset, address_of(unused), + &offset, &wsType); + if (wsType != WSType::text && wsType != WSType::normalWS) { + // eThisBlock and eOtherBlock conveniently distinquish cases + // of going "down" into a block and "up" out of a block. + if (wsEndObj.mStartReason == WSType::otherBlock) { + // endpoint is just after the close of a block. + nsCOMPtr<nsIDOMNode> child = + GetAsDOMNode(mHTMLEditor->GetRightmostChild(wsEndObj.mStartReasonNode, + true)); + if (child) { + newEndNode = EditorBase::GetNodeLocation(child, &newEndOffset); + ++newEndOffset; // offset *after* child + } + // else block is empty - we can leave selection alone here, i think. + } else if (wsEndObj.mStartReason == WSType::thisBlock) { + // endpoint is just after start of this block + nsCOMPtr<nsIDOMNode> child; + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->GetPriorHTMLNode(endNode, endOffset, address_of(child)); + if (child) { + newEndNode = EditorBase::GetNodeLocation(child, &newEndOffset); + ++newEndOffset; // offset *after* child + } + // else block is empty - we can leave selection alone here, i think. + } else if (wsEndObj.mStartReason == WSType::br) { + // endpoint is just after break. lets adjust it to before it. + newEndNode = + EditorBase::GetNodeLocation(GetAsDOMNode(wsEndObj.mStartReasonNode), + &newEndOffset); + } + } + + + // similar dealio for start of range + WSRunObject wsStartObj(mHTMLEditor, startNode, startOffset); + // is there any intervening visible whitespace? if so we can't push selection past that, + // it would visibly change maening of users selection + nsCOMPtr<nsINode> startNode_(do_QueryInterface(startNode)); + wsStartObj.NextVisibleNode(startNode_, startOffset, address_of(unused), + &offset, &wsType); + if (wsType != WSType::text && wsType != WSType::normalWS) { + // eThisBlock and eOtherBlock conveniently distinquish cases + // of going "down" into a block and "up" out of a block. + if (wsStartObj.mEndReason == WSType::otherBlock) { + // startpoint is just before the start of a block. + nsCOMPtr<nsIDOMNode> child = + GetAsDOMNode(mHTMLEditor->GetLeftmostChild(wsStartObj.mEndReasonNode, + true)); + if (child) { + newStartNode = EditorBase::GetNodeLocation(child, &newStartOffset); + } + // else block is empty - we can leave selection alone here, i think. + } else if (wsStartObj.mEndReason == WSType::thisBlock) { + // startpoint is just before end of this block + nsCOMPtr<nsIDOMNode> child; + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->GetNextHTMLNode(startNode, startOffset, address_of(child)); + if (child) { + newStartNode = EditorBase::GetNodeLocation(child, &newStartOffset); + } + // else block is empty - we can leave selection alone here, i think. + } else if (wsStartObj.mEndReason == WSType::br) { + // startpoint is just before a break. lets adjust it to after it. + newStartNode = + EditorBase::GetNodeLocation(GetAsDOMNode(wsStartObj.mEndReasonNode), + &newStartOffset); + ++newStartOffset; // offset *after* break + } + } + + // there is a demented possiblity we have to check for. We might have a very strange selection + // that is not collapsed and yet does not contain any editable content, and satisfies some of the + // above conditions that cause tweaking. In this case we don't want to tweak the selection into + // a block it was never in, etc. There are a variety of strategies one might use to try to + // detect these cases, but I think the most straightforward is to see if the adjusted locations + // "cross" the old values: ie, new end before old start, or new start after old end. If so + // then just leave things alone. + + int16_t comp; + comp = nsContentUtils::ComparePoints(startNode, startOffset, + newEndNode, newEndOffset); + if (comp == 1) { + return NS_OK; // New end before old start. + } + comp = nsContentUtils::ComparePoints(newStartNode, newStartOffset, + endNode, endOffset); + if (comp == 1) { + return NS_OK; // New start after old end. + } + + // otherwise set selection to new values. + inSelection->Collapse(newStartNode, newStartOffset); + inSelection->Extend(newEndNode, newEndOffset); + return NS_OK; +} + +/** + * GetPromotedPoint() figures out where a start or end point for a block + * operation really is. + */ +void +HTMLEditRules::GetPromotedPoint(RulesEndpoint aWhere, + nsIDOMNode* aNode, + int32_t aOffset, + EditAction actionID, + nsCOMPtr<nsIDOMNode>* outNode, + int32_t* outOffset) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + MOZ_ASSERT(node && outNode && outOffset); + + // default values + *outNode = node->AsDOMNode(); + *outOffset = aOffset; + + // we do one thing for text actions, something else entirely for other + // actions + if (actionID == EditAction::insertText || + actionID == EditAction::insertIMEText || + actionID == EditAction::insertBreak || + actionID == EditAction::deleteText) { + bool isSpace, isNBSP; + nsCOMPtr<nsIContent> content = do_QueryInterface(node), temp; + // for text actions, we want to look backwards (or forwards, as + // appropriate) for additional whitespace or nbsp's. We may have to act on + // these later even though they are outside of the initial selection. Even + // if they are in another node! + while (content) { + int32_t offset; + if (aWhere == kStart) { + NS_ENSURE_TRUE_VOID(mHTMLEditor); + mHTMLEditor->IsPrevCharInNodeWhitespace(content, *outOffset, + &isSpace, &isNBSP, + getter_AddRefs(temp), &offset); + } else { + NS_ENSURE_TRUE_VOID(mHTMLEditor); + mHTMLEditor->IsNextCharInNodeWhitespace(content, *outOffset, + &isSpace, &isNBSP, + getter_AddRefs(temp), &offset); + } + if (isSpace || isNBSP) { + content = temp; + *outOffset = offset; + } else { + break; + } + } + + *outNode = content->AsDOMNode(); + return; + } + + int32_t offset = aOffset; + + // else not a text section. In this case we want to see if we should grab + // any adjacent inline nodes and/or parents and other ancestors + if (aWhere == kStart) { + // some special casing for text nodes + if (node->IsNodeOfType(nsINode::eTEXT)) { + if (!node->GetParentNode()) { + // Okay, can't promote any further + return; + } + offset = node->GetParentNode()->IndexOf(node); + node = node->GetParentNode(); + } + + // look back through any further inline nodes that aren't across a <br> + // from us, and that are enclosed in the same block. + NS_ENSURE_TRUE_VOID(mHTMLEditor); + nsCOMPtr<nsINode> priorNode = + mHTMLEditor->GetPriorHTMLNode(node, offset, true); + + while (priorNode && priorNode->GetParentNode() && + mHTMLEditor && !mHTMLEditor->IsVisBreak(priorNode) && + !IsBlockNode(*priorNode)) { + offset = priorNode->GetParentNode()->IndexOf(priorNode); + node = priorNode->GetParentNode(); + NS_ENSURE_TRUE_VOID(mHTMLEditor); + priorNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true); + } + + // finding the real start for this point. look up the tree for as long as + // we are the first node in the container, and as long as we haven't hit + // the body node. + NS_ENSURE_TRUE_VOID(mHTMLEditor); + nsCOMPtr<nsIContent> nearNode = + mHTMLEditor->GetPriorHTMLNode(node, offset, true); + while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) && + node->GetParentNode()) { + // some cutoffs are here: we don't need to also include them in the + // aWhere == kEnd case. as long as they are in one or the other it will + // work. special case for outdent: don't keep looking up if we have + // found a blockquote element to act on + if (actionID == EditAction::outdent && + node->IsHTMLElement(nsGkAtoms::blockquote)) { + break; + } + + int32_t parentOffset = node->GetParentNode()->IndexOf(node); + nsCOMPtr<nsINode> parent = node->GetParentNode(); + + // Don't walk past the editable section. Note that we need to check + // before walking up to a parent because we need to return the parent + // object, so the parent itself might not be in the editable area, but + // it's OK if we're not performing a block-level action. + bool blockLevelAction = actionID == EditAction::indent || + actionID == EditAction::outdent || + actionID == EditAction::align || + actionID == EditAction::makeBasicBlock; + NS_ENSURE_TRUE_VOID(mHTMLEditor); + if (!mHTMLEditor->IsDescendantOfEditorRoot(parent) && + (blockLevelAction || !mHTMLEditor || + !mHTMLEditor->IsDescendantOfEditorRoot(node))) { + NS_ENSURE_TRUE_VOID(mHTMLEditor); + break; + } + + node = parent; + offset = parentOffset; + NS_ENSURE_TRUE_VOID(mHTMLEditor); + nearNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true); + } + *outNode = node->AsDOMNode(); + *outOffset = offset; + return; + } + + // aWhere == kEnd + // some special casing for text nodes + if (node->IsNodeOfType(nsINode::eTEXT)) { + if (!node->GetParentNode()) { + // Okay, can't promote any further + return; + } + // want to be after the text node + offset = 1 + node->GetParentNode()->IndexOf(node); + node = node->GetParentNode(); + } + + // look ahead through any further inline nodes that aren't across a <br> from + // us, and that are enclosed in the same block. + NS_ENSURE_TRUE(mHTMLEditor, /* void */); + nsCOMPtr<nsIContent> nextNode = + mHTMLEditor->GetNextHTMLNode(node, offset, true); + + while (nextNode && !IsBlockNode(*nextNode) && nextNode->GetParentNode()) { + offset = 1 + nextNode->GetParentNode()->IndexOf(nextNode); + node = nextNode->GetParentNode(); + NS_ENSURE_TRUE_VOID(mHTMLEditor); + if (mHTMLEditor->IsVisBreak(nextNode)) { + break; + } + + // Check for newlines in pre-formatted text nodes. + bool isPRE; + mHTMLEditor->IsPreformatted(nextNode->AsDOMNode(), &isPRE); + if (isPRE) { + nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(nextNode); + if (textNode) { + nsAutoString tempString; + textNode->GetData(tempString); + int32_t newlinePos = tempString.FindChar(nsCRT::LF); + if (newlinePos >= 0) { + if (static_cast<uint32_t>(newlinePos) + 1 == tempString.Length()) { + // No need for special processing if the newline is at the end. + break; + } + *outNode = nextNode->AsDOMNode(); + *outOffset = newlinePos + 1; + return; + } + } + } + NS_ENSURE_TRUE_VOID(mHTMLEditor); + nextNode = mHTMLEditor->GetNextHTMLNode(node, offset, true); + } + + // finding the real end for this point. look up the tree for as long as we + // are the last node in the container, and as long as we haven't hit the body + // node. + NS_ENSURE_TRUE_VOID(mHTMLEditor); + nsCOMPtr<nsIContent> nearNode = + mHTMLEditor->GetNextHTMLNode(node, offset, true); + while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) && + node->GetParentNode()) { + int32_t parentOffset = node->GetParentNode()->IndexOf(node); + nsCOMPtr<nsINode> parent = node->GetParentNode(); + + // Don't walk past the editable section. Note that we need to check before + // walking up to a parent because we need to return the parent object, so + // the parent itself might not be in the editable area, but it's OK. + if ((!mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(node)) && + (!mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(parent))) { + NS_ENSURE_TRUE_VOID(mHTMLEditor); + break; + } + + node = parent; + // we want to be AFTER nearNode + offset = parentOffset + 1; + NS_ENSURE_TRUE_VOID(mHTMLEditor); + nearNode = mHTMLEditor->GetNextHTMLNode(node, offset, true); + } + *outNode = node->AsDOMNode(); + *outOffset = offset; +} + +/** + * GetPromotedRanges() runs all the selection range endpoint through + * GetPromotedPoint(). + */ +void +HTMLEditRules::GetPromotedRanges(Selection& aSelection, + nsTArray<RefPtr<nsRange>>& outArrayOfRanges, + EditAction inOperationType) +{ + uint32_t rangeCount = aSelection.RangeCount(); + + for (uint32_t i = 0; i < rangeCount; i++) { + RefPtr<nsRange> selectionRange = aSelection.GetRangeAt(i); + MOZ_ASSERT(selectionRange); + + // Clone range so we don't muck with actual selection ranges + RefPtr<nsRange> opRange = selectionRange->CloneRange(); + + // Make a new adjusted range to represent the appropriate block content. + // The basic idea is to push out the range endpoints to truly enclose the + // blocks that we will affect. This call alters opRange. + PromoteRange(*opRange, inOperationType); + + // Stuff new opRange into array + outArrayOfRanges.AppendElement(opRange); + } +} + +/** + * PromoteRange() expands a range to include any parents for which all editable + * children are already in range. + */ +void +HTMLEditRules::PromoteRange(nsRange& aRange, + EditAction aOperationType) +{ + NS_ENSURE_TRUE(mHTMLEditor, ); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + nsCOMPtr<nsINode> startNode = aRange.GetStartParent(); + nsCOMPtr<nsINode> endNode = aRange.GetEndParent(); + int32_t startOffset = aRange.StartOffset(); + int32_t endOffset = aRange.EndOffset(); + + // MOOSE major hack: + // GetPromotedPoint doesn't really do the right thing for collapsed ranges + // inside block elements that contain nothing but a solo <br>. It's easier + // to put a workaround here than to revamp GetPromotedPoint. :-( + if (startNode == endNode && startOffset == endOffset) { + nsCOMPtr<Element> block = htmlEditor->GetBlock(*startNode); + if (block) { + bool bIsEmptyNode = false; + nsCOMPtr<nsIContent> root = htmlEditor->GetActiveEditingHost(); + // Make sure we don't go higher than our root element in the content tree + NS_ENSURE_TRUE(root, ); + if (!nsContentUtils::ContentIsDescendantOf(root, block)) { + htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false); + } + if (bIsEmptyNode) { + startNode = block; + endNode = block; + startOffset = 0; + endOffset = block->Length(); + } + } + } + + // Make a new adjusted range to represent the appropriate block content. + // This is tricky. The basic idea is to push out the range endpoints to + // truly enclose the blocks that we will affect. + + nsCOMPtr<nsIDOMNode> opStartNode; + nsCOMPtr<nsIDOMNode> opEndNode; + int32_t opStartOffset, opEndOffset; + + GetPromotedPoint(kStart, GetAsDOMNode(startNode), startOffset, + aOperationType, address_of(opStartNode), &opStartOffset); + GetPromotedPoint(kEnd, GetAsDOMNode(endNode), endOffset, aOperationType, + address_of(opEndNode), &opEndOffset); + + // Make sure that the new range ends up to be in the editable section. + if (!htmlEditor->IsDescendantOfEditorRoot( + EditorBase::GetNodeAtRangeOffsetPoint(opStartNode, opStartOffset)) || + !htmlEditor->IsDescendantOfEditorRoot( + EditorBase::GetNodeAtRangeOffsetPoint(opEndNode, opEndOffset - 1))) { + return; + } + + DebugOnly<nsresult> rv = aRange.SetStart(opStartNode, opStartOffset); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = aRange.SetEnd(opEndNode, opEndOffset); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +class UniqueFunctor final : public BoolDomIterFunctor +{ +public: + explicit UniqueFunctor(nsTArray<OwningNonNull<nsINode>>& aArray) + : mArray(aArray) + { + } + + // Used to build list of all nodes iterator covers. + virtual bool operator()(nsINode* aNode) const + { + return !mArray.Contains(aNode); + } + +private: + nsTArray<OwningNonNull<nsINode>>& mArray; +}; + +/** + * GetNodesForOperation() runs through the ranges in the array and construct a + * new array of nodes to be acted on. + */ +nsresult +HTMLEditRules::GetNodesForOperation( + nsTArray<RefPtr<nsRange>>& aArrayOfRanges, + nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, + EditAction aOperationType, + TouchContent aTouchContent) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + int32_t rangeCount = aArrayOfRanges.Length(); + if (aTouchContent == TouchContent::yes) { + // Split text nodes. This is necessary, since GetPromotedPoint() may return a + // range ending in a text node in case where part of a pre-formatted + // elements needs to be moved. + for (int32_t i = 0; i < rangeCount; i++) { + RefPtr<nsRange> r = aArrayOfRanges[i]; + nsCOMPtr<nsIContent> endParent = do_QueryInterface(r->GetEndParent()); + if (!htmlEditor->IsTextNode(endParent)) { + continue; + } + nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(endParent); + if (textNode) { + int32_t offset = r->EndOffset(); + nsAutoString tempString; + textNode->GetData(tempString); + + if (0 < offset && offset < static_cast<int32_t>(tempString.Length())) { + // Split the text node. + nsCOMPtr<nsIDOMNode> tempNode; + nsresult rv = htmlEditor->SplitNode(endParent->AsDOMNode(), offset, + getter_AddRefs(tempNode)); + NS_ENSURE_SUCCESS(rv, rv); + + // Correct the range. + // The new end parent becomes the parent node of the text. + nsCOMPtr<nsIContent> newParent = endParent->GetParent(); + r->SetEnd(newParent, newParent->IndexOf(endParent)); + } + } + } + } + + // Bust up any inlines that cross our range endpoints, but only if we are + // allowed to touch content. + + if (aTouchContent == TouchContent::yes) { + nsTArray<OwningNonNull<RangeItem>> rangeItemArray; + rangeItemArray.AppendElements(rangeCount); + + // First register ranges for special editor gravity + for (int32_t i = 0; i < rangeCount; i++) { + rangeItemArray[i] = new RangeItem(); + rangeItemArray[i]->StoreRange(aArrayOfRanges[0]); + htmlEditor->mRangeUpdater.RegisterRangeItem(rangeItemArray[i]); + aArrayOfRanges.RemoveElementAt(0); + } + // Now bust up inlines. + nsresult rv = NS_OK; + for (auto& item : Reversed(rangeItemArray)) { + rv = BustUpInlinesAtRangeEndpoints(*item); + if (NS_FAILED(rv)) { + break; + } + } + // Then unregister the ranges + for (auto& item : rangeItemArray) { + htmlEditor->mRangeUpdater.DropRangeItem(item); + aArrayOfRanges.AppendElement(item->GetRange()); + } + NS_ENSURE_SUCCESS(rv, rv); + } + // Gather up a list of all the nodes + for (auto& range : aArrayOfRanges) { + DOMSubtreeIterator iter; + nsresult rv = iter.Init(*range); + NS_ENSURE_SUCCESS(rv, rv); + if (aOutArrayOfNodes.IsEmpty()) { + iter.AppendList(TrivialFunctor(), aOutArrayOfNodes); + } else { + // We don't want duplicates in aOutArrayOfNodes, so we use an + // iterator/functor that only return nodes that are not already in + // aOutArrayOfNodes. + nsTArray<OwningNonNull<nsINode>> nodes; + iter.AppendList(UniqueFunctor(aOutArrayOfNodes), nodes); + aOutArrayOfNodes.AppendElements(nodes); + } + } + + // Certain operations should not act on li's and td's, but rather inside + // them. Alter the list as needed. + if (aOperationType == EditAction::makeBasicBlock) { + for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { + OwningNonNull<nsINode> node = aOutArrayOfNodes[i]; + if (HTMLEditUtils::IsListItem(node)) { + int32_t j = i; + aOutArrayOfNodes.RemoveElementAt(i); + GetInnerContent(*node, aOutArrayOfNodes, &j); + } + } + } + // Indent/outdent already do something special for list items, but we still + // need to make sure we don't act on table elements + else if (aOperationType == EditAction::outdent || + aOperationType == EditAction::indent || + aOperationType == EditAction::setAbsolutePosition) { + for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { + OwningNonNull<nsINode> node = aOutArrayOfNodes[i]; + if (HTMLEditUtils::IsTableElementButNotTable(node)) { + int32_t j = i; + aOutArrayOfNodes.RemoveElementAt(i); + GetInnerContent(*node, aOutArrayOfNodes, &j); + } + } + } + // Outdent should look inside of divs. + if (aOperationType == EditAction::outdent && + !htmlEditor->IsCSSEnabled()) { + for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { + OwningNonNull<nsINode> node = aOutArrayOfNodes[i]; + if (node->IsHTMLElement(nsGkAtoms::div)) { + int32_t j = i; + aOutArrayOfNodes.RemoveElementAt(i); + GetInnerContent(*node, aOutArrayOfNodes, &j, Lists::no, Tables::no); + } + } + } + + + // Post-process the list to break up inline containers that contain br's, but + // only for operations that might care, like making lists or paragraphs + if (aOperationType == EditAction::makeBasicBlock || + aOperationType == EditAction::makeList || + aOperationType == EditAction::align || + aOperationType == EditAction::setAbsolutePosition || + aOperationType == EditAction::indent || + aOperationType == EditAction::outdent) { + for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { + OwningNonNull<nsINode> node = aOutArrayOfNodes[i]; + if (aTouchContent == TouchContent::yes && IsInlineNode(node) && + htmlEditor->IsContainer(node) && !htmlEditor->IsTextNode(node)) { + nsTArray<OwningNonNull<nsINode>> arrayOfInlines; + nsresult rv = BustUpInlinesAtBRs(*node->AsContent(), arrayOfInlines); + NS_ENSURE_SUCCESS(rv, rv); + + // Put these nodes in aOutArrayOfNodes, replacing the current node + aOutArrayOfNodes.RemoveElementAt(i); + aOutArrayOfNodes.InsertElementsAt(i, arrayOfInlines); + } + } + } + return NS_OK; +} + +void +HTMLEditRules::GetChildNodesForOperation( + nsINode& aNode, + nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes) +{ + for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild(); + child; child = child->GetNextSibling()) { + outArrayOfNodes.AppendElement(*child); + } +} + +nsresult +HTMLEditRules::GetListActionNodes( + nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, + EntireList aEntireList, + TouchContent aTouchContent) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + RefPtr<Selection> selection = htmlEditor->GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + // Added this in so that ui code can ask to change an entire list, even if + // selection is only in part of it. used by list item dialog. + if (aEntireList == EntireList::yes) { + uint32_t rangeCount = selection->RangeCount(); + for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) { + RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); + for (nsCOMPtr<nsINode> parent = range->GetCommonAncestor(); + parent; parent = parent->GetParentNode()) { + if (HTMLEditUtils::IsList(parent)) { + aOutArrayOfNodes.AppendElement(*parent); + break; + } + } + } + // If we didn't find any nodes this way, then try the normal way. Perhaps + // the selection spans multiple lists but with no common list parent. + if (!aOutArrayOfNodes.IsEmpty()) { + return NS_OK; + } + } + + { + // We don't like other people messing with our selection! + AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor); + + // contruct a list of nodes to act on. + nsresult rv = GetNodesFromSelection(*selection, EditAction::makeList, + aOutArrayOfNodes, aTouchContent); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Pre-process our list of nodes + for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { + OwningNonNull<nsINode> testNode = aOutArrayOfNodes[i]; + + // Remove all non-editable nodes. Leave them be. + if (!htmlEditor->IsEditable(testNode)) { + aOutArrayOfNodes.RemoveElementAt(i); + continue; + } + + // Scan for table elements and divs. If we find table elements other than + // table, replace it with a list of any editable non-table content. + if (HTMLEditUtils::IsTableElementButNotTable(testNode)) { + int32_t j = i; + aOutArrayOfNodes.RemoveElementAt(i); + GetInnerContent(*testNode, aOutArrayOfNodes, &j, Lists::no); + } + } + + // If there is only one node in the array, and it is a list, div, or + // blockquote, then look inside of it until we find inner list or content. + LookInsideDivBQandList(aOutArrayOfNodes); + + return NS_OK; +} + +void +HTMLEditRules::LookInsideDivBQandList( + nsTArray<OwningNonNull<nsINode>>& aNodeArray) +{ + NS_ENSURE_TRUE(mHTMLEditor, ); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // If there is only one node in the array, and it is a list, div, or + // blockquote, then look inside of it until we find inner list or content. + if (aNodeArray.Length() != 1) { + return; + } + + OwningNonNull<nsINode> curNode = aNodeArray[0]; + + while (curNode->IsHTMLElement(nsGkAtoms::div) || + HTMLEditUtils::IsList(curNode) || + curNode->IsHTMLElement(nsGkAtoms::blockquote)) { + // Dive as long as there's only one child, and it's a list, div, blockquote + uint32_t numChildren = htmlEditor->CountEditableChildren(curNode); + if (numChildren != 1) { + break; + } + + // Keep diving! XXX One would expect to dive into the one editable node. + nsCOMPtr<nsIContent> child = curNode->GetFirstChild(); + if (!child->IsHTMLElement(nsGkAtoms::div) && + !HTMLEditUtils::IsList(child) && + !child->IsHTMLElement(nsGkAtoms::blockquote)) { + break; + } + + // check editability XXX floppy moose + curNode = child; + } + + // We've found innermost list/blockquote/div: replace the one node in the + // array with these nodes + aNodeArray.RemoveElementAt(0); + if (curNode->IsAnyOfHTMLElements(nsGkAtoms::div, + nsGkAtoms::blockquote)) { + int32_t j = 0; + GetInnerContent(*curNode, aNodeArray, &j, Lists::no, Tables::no); + return; + } + + aNodeArray.AppendElement(*curNode); +} + +void +HTMLEditRules::GetDefinitionListItemTypes(dom::Element* aElement, + bool* aDT, + bool* aDD) +{ + MOZ_ASSERT(aElement); + MOZ_ASSERT(aElement->IsHTMLElement(nsGkAtoms::dl)); + MOZ_ASSERT(aDT); + MOZ_ASSERT(aDD); + + *aDT = *aDD = false; + for (nsIContent* child = aElement->GetFirstChild(); + child; + child = child->GetNextSibling()) { + if (child->IsHTMLElement(nsGkAtoms::dt)) { + *aDT = true; + } else if (child->IsHTMLElement(nsGkAtoms::dd)) { + *aDD = true; + } + } +} + +nsresult +HTMLEditRules::GetParagraphFormatNodes( + nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, + TouchContent aTouchContent) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + RefPtr<Selection> selection = htmlEditor->GetSelection(); + NS_ENSURE_STATE(selection); + + // Contruct a list of nodes to act on. + nsresult rv = GetNodesFromSelection(*selection, EditAction::makeBasicBlock, + outArrayOfNodes, aTouchContent); + NS_ENSURE_SUCCESS(rv, rv); + + // Pre-process our list of nodes + for (int32_t i = outArrayOfNodes.Length() - 1; i >= 0; i--) { + OwningNonNull<nsINode> testNode = outArrayOfNodes[i]; + + // Remove all non-editable nodes. Leave them be. + if (!htmlEditor->IsEditable(testNode)) { + outArrayOfNodes.RemoveElementAt(i); + continue; + } + + // Scan for table elements. If we find table elements other than table, + // replace it with a list of any editable non-table content. Ditto for + // list elements. + if (HTMLEditUtils::IsTableElement(testNode) || + HTMLEditUtils::IsList(testNode) || + HTMLEditUtils::IsListItem(testNode)) { + int32_t j = i; + outArrayOfNodes.RemoveElementAt(i); + GetInnerContent(testNode, outArrayOfNodes, &j); + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::BustUpInlinesAtRangeEndpoints(RangeItem& item) +{ + bool isCollapsed = ((item.startNode == item.endNode) && (item.startOffset == item.endOffset)); + + nsCOMPtr<nsIContent> endInline = GetHighestInlineParent(*item.endNode); + + // if we have inline parents above range endpoints, split them + if (endInline && !isCollapsed) { + nsCOMPtr<nsINode> resultEndNode = endInline->GetParentNode(); + NS_ENSURE_STATE(mHTMLEditor); + // item.endNode must be content if endInline isn't null + int32_t resultEndOffset = + mHTMLEditor->SplitNodeDeep(*endInline, *item.endNode->AsContent(), + item.endOffset, + EditorBase::EmptyContainers::no); + NS_ENSURE_TRUE(resultEndOffset != -1, NS_ERROR_FAILURE); + // reset range + item.endNode = resultEndNode; + item.endOffset = resultEndOffset; + } + + nsCOMPtr<nsIContent> startInline = GetHighestInlineParent(*item.startNode); + + if (startInline) { + nsCOMPtr<nsINode> resultStartNode = startInline->GetParentNode(); + NS_ENSURE_STATE(mHTMLEditor); + int32_t resultStartOffset = + mHTMLEditor->SplitNodeDeep(*startInline, *item.startNode->AsContent(), + item.startOffset, + EditorBase::EmptyContainers::no); + NS_ENSURE_TRUE(resultStartOffset != -1, NS_ERROR_FAILURE); + // reset range + item.startNode = resultStartNode; + item.startOffset = resultStartOffset; + } + + return NS_OK; +} + +nsresult +HTMLEditRules::BustUpInlinesAtBRs( + nsIContent& aNode, + nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // First build up a list of all the break nodes inside the inline container. + nsTArray<OwningNonNull<nsINode>> arrayOfBreaks; + BRNodeFunctor functor; + DOMIterator iter(aNode); + iter.AppendList(functor, arrayOfBreaks); + + // If there aren't any breaks, just put inNode itself in the array + if (arrayOfBreaks.IsEmpty()) { + aOutArrayOfNodes.AppendElement(aNode); + return NS_OK; + } + + // Else we need to bust up inNode along all the breaks + nsCOMPtr<nsINode> inlineParentNode = aNode.GetParentNode(); + nsCOMPtr<nsIContent> splitDeepNode = &aNode; + nsCOMPtr<nsIContent> leftNode, rightNode; + + for (uint32_t i = 0; i < arrayOfBreaks.Length(); i++) { + OwningNonNull<Element> breakNode = *arrayOfBreaks[i]->AsElement(); + NS_ENSURE_TRUE(splitDeepNode, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(breakNode->GetParent(), NS_ERROR_NULL_POINTER); + OwningNonNull<nsIContent> splitParentNode = *breakNode->GetParent(); + int32_t splitOffset = splitParentNode->IndexOf(breakNode); + + int32_t resultOffset = + htmlEditor->SplitNodeDeep(*splitDeepNode, splitParentNode, splitOffset, + HTMLEditor::EmptyContainers::yes, + getter_AddRefs(leftNode), + getter_AddRefs(rightNode)); + NS_ENSURE_STATE(resultOffset != -1); + + // Put left node in node list + if (leftNode) { + // Might not be a left node. A break might have been at the very + // beginning of inline container, in which case SplitNodeDeep would not + // actually split anything + aOutArrayOfNodes.AppendElement(*leftNode); + } + // Move break outside of container and also put in node list + nsresult rv = + htmlEditor->MoveNode(breakNode, inlineParentNode, resultOffset); + NS_ENSURE_SUCCESS(rv, rv); + aOutArrayOfNodes.AppendElement(*breakNode); + + // Now rightNode becomes the new node to split + splitDeepNode = rightNode; + } + // Now tack on remaining rightNode, if any, to the list + if (rightNode) { + aOutArrayOfNodes.AppendElement(*rightNode); + } + return NS_OK; +} + +nsIContent* +HTMLEditRules::GetHighestInlineParent(nsINode& aNode) +{ + if (!aNode.IsContent() || IsBlockNode(aNode)) { + return nullptr; + } + OwningNonNull<nsIContent> node = *aNode.AsContent(); + + while (node->GetParent() && IsInlineNode(*node->GetParent())) { + node = *node->GetParent(); + } + return node; +} + +/** + * GetNodesFromPoint() constructs a list of nodes from a point that will be + * operated on. + */ +nsresult +HTMLEditRules::GetNodesFromPoint( + EditorDOMPoint aPoint, + EditAction aOperation, + nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, + TouchContent aTouchContent) +{ + NS_ENSURE_STATE(aPoint.node); + RefPtr<nsRange> range = new nsRange(aPoint.node); + nsresult rv = range->SetStart(aPoint.node, aPoint.offset); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Expand the range to include adjacent inlines + PromoteRange(*range, aOperation); + + // Make array of ranges + nsTArray<RefPtr<nsRange>> arrayOfRanges; + + // Stuff new opRange into array + arrayOfRanges.AppendElement(range); + + // Use these ranges to contruct a list of nodes to act on + rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes, aOperation, + aTouchContent); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/** + * GetNodesFromSelection() constructs a list of nodes from the selection that + * will be operated on. + */ +nsresult +HTMLEditRules::GetNodesFromSelection( + Selection& aSelection, + EditAction aOperation, + nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, + TouchContent aTouchContent) +{ + // Promote selection ranges + nsTArray<RefPtr<nsRange>> arrayOfRanges; + GetPromotedRanges(aSelection, arrayOfRanges, aOperation); + + // Use these ranges to contruct a list of nodes to act on. + nsresult rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes, + aOperation, aTouchContent); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/** + * MakeTransitionList() detects all the transitions in the array, where a + * transition means that adjacent nodes in the array don't have the same parent. + */ +void +HTMLEditRules::MakeTransitionList(nsTArray<OwningNonNull<nsINode>>& aNodeArray, + nsTArray<bool>& aTransitionArray) +{ + nsCOMPtr<nsINode> prevParent; + + aTransitionArray.EnsureLengthAtLeast(aNodeArray.Length()); + for (uint32_t i = 0; i < aNodeArray.Length(); i++) { + if (aNodeArray[i]->GetParentNode() != prevParent) { + // Different parents: transition point + aTransitionArray[i] = true; + } else { + // Same parents: these nodes grew up together + aTransitionArray[i] = false; + } + prevParent = aNodeArray[i]->GetParentNode(); + } +} + +/** + * If aNode is the descendant of a listitem, return that li. But table element + * boundaries are stoppers on the search. Also stops on the active editor host + * (contenteditable). Also test if aNode is an li itself. + */ +Element* +HTMLEditRules::IsInListItem(nsINode* aNode) +{ + NS_ENSURE_TRUE(aNode, nullptr); + if (HTMLEditUtils::IsListItem(aNode)) { + return aNode->AsElement(); + } + + Element* parent = aNode->GetParentElement(); + while (parent && + mHTMLEditor && mHTMLEditor->IsDescendantOfEditorRoot(parent) && + !HTMLEditUtils::IsTableElement(parent)) { + if (HTMLEditUtils::IsListItem(parent)) { + return parent; + } + parent = parent->GetParentElement(); + } + return nullptr; +} + +/** + * ReturnInHeader: do the right thing for returns pressed in headers + */ +nsresult +HTMLEditRules::ReturnInHeader(Selection& aSelection, + Element& aHeader, + nsINode& aNode, + int32_t aOffset) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Remember where the header is + nsCOMPtr<nsINode> headerParent = aHeader.GetParentNode(); + int32_t offset = headerParent ? headerParent->IndexOf(&aHeader) : -1; + + // Get ws code to adjust any ws + nsCOMPtr<nsINode> node = &aNode; + nsresult rv = WSRunObject::PrepareToSplitAcrossBlocks(htmlEditor, + address_of(node), + &aOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // Split the header + NS_ENSURE_STATE(node->IsContent()); + htmlEditor->SplitNodeDeep(aHeader, *node->AsContent(), aOffset); + + // If the left-hand heading is empty, put a mozbr in it + nsCOMPtr<nsIContent> prevItem = htmlEditor->GetPriorHTMLSibling(&aHeader); + if (prevItem && HTMLEditUtils::IsHeader(*prevItem)) { + bool isEmptyNode; + rv = htmlEditor->IsEmptyNode(prevItem, &isEmptyNode); + NS_ENSURE_SUCCESS(rv, rv); + if (isEmptyNode) { + rv = CreateMozBR(prevItem->AsDOMNode(), 0); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // If the new (righthand) header node is empty, delete it + bool isEmpty; + rv = IsEmptyBlock(aHeader, &isEmpty, MozBRCounts::no); + NS_ENSURE_SUCCESS(rv, rv); + if (isEmpty) { + rv = htmlEditor->DeleteNode(&aHeader); + NS_ENSURE_SUCCESS(rv, rv); + // Layout tells the caret to blink in a weird place if we don't place a + // break after the header. + nsCOMPtr<nsIContent> sibling = + htmlEditor->GetNextHTMLSibling(headerParent, offset + 1); + if (!sibling || !sibling->IsHTMLElement(nsGkAtoms::br)) { + ClearCachedStyles(); + htmlEditor->mTypeInState->ClearAllProps(); + + // Create a paragraph + nsCOMPtr<Element> pNode = + htmlEditor->CreateNode(nsGkAtoms::p, headerParent, offset + 1); + NS_ENSURE_STATE(pNode); + + // Append a <br> to it + nsCOMPtr<Element> brNode = htmlEditor->CreateBR(pNode, 0); + NS_ENSURE_STATE(brNode); + + // Set selection to before the break + rv = aSelection.Collapse(pNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + } else { + headerParent = sibling->GetParentNode(); + offset = headerParent ? headerParent->IndexOf(sibling) : -1; + // Put selection after break + rv = aSelection.Collapse(headerParent, offset + 1); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Put selection at front of righthand heading + rv = aSelection.Collapse(&aHeader, 0); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +/** + * ReturnInParagraph() does the right thing for returns pressed in paragraphs. + */ +nsresult +HTMLEditRules::ReturnInParagraph(Selection* aSelection, + nsIDOMNode* aPara, + nsIDOMNode* aNode, + int32_t aOffset, + bool* aCancel, + bool* aHandled) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + if (!aSelection || !aPara || !node || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + *aCancel = false; + *aHandled = false; + + int32_t offset; + nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(node, &offset); + + NS_ENSURE_STATE(mHTMLEditor); + bool doesCRCreateNewP = mHTMLEditor->GetReturnInParagraphCreatesNewParagraph(); + + bool newBRneeded = false; + bool newSelNode = false; + nsCOMPtr<nsIContent> sibling; + nsCOMPtr<nsIDOMNode> selNode = aNode; + int32_t selOffset = aOffset; + + NS_ENSURE_STATE(mHTMLEditor); + if (aNode == aPara && doesCRCreateNewP) { + // we are at the edges of the block, newBRneeded not needed! + sibling = node->AsContent(); + } else if (mHTMLEditor->IsTextNode(aNode)) { + nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(aNode); + uint32_t strLength; + nsresult rv = textNode->GetLength(&strLength); + NS_ENSURE_SUCCESS(rv, rv); + + // at beginning of text node? + if (!aOffset) { + // is there a BR prior to it? + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetPriorHTMLSibling(node); + if (!sibling || !mHTMLEditor || !mHTMLEditor->IsVisBreak(sibling) || + TextEditUtils::HasMozAttr(GetAsDOMNode(sibling))) { + NS_ENSURE_STATE(mHTMLEditor); + newBRneeded = true; + } + } else if (aOffset == (int32_t)strLength) { + // we're at the end of text node... + // is there a BR after to it? + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->GetNextHTMLSibling(node); + if (!sibling || !mHTMLEditor || !mHTMLEditor->IsVisBreak(sibling) || + TextEditUtils::HasMozAttr(GetAsDOMNode(sibling))) { + NS_ENSURE_STATE(mHTMLEditor); + newBRneeded = true; + offset++; + } + } else { + if (doesCRCreateNewP) { + nsCOMPtr<nsIDOMNode> tmp; + rv = mTextEditor->SplitNode(aNode, aOffset, getter_AddRefs(tmp)); + NS_ENSURE_SUCCESS(rv, rv); + selNode = tmp; + } + + newBRneeded = true; + offset++; + } + } else { + // not in a text node. + // is there a BR prior to it? + nsCOMPtr<nsIContent> nearNode; + NS_ENSURE_STATE(mHTMLEditor); + nearNode = mHTMLEditor->GetPriorHTMLNode(node, aOffset); + NS_ENSURE_STATE(mHTMLEditor); + if (!nearNode || !mHTMLEditor->IsVisBreak(nearNode) || + TextEditUtils::HasMozAttr(GetAsDOMNode(nearNode))) { + // is there a BR after it? + NS_ENSURE_STATE(mHTMLEditor); + nearNode = mHTMLEditor->GetNextHTMLNode(node, aOffset); + NS_ENSURE_STATE(mHTMLEditor); + if (!nearNode || !mHTMLEditor->IsVisBreak(nearNode) || + TextEditUtils::HasMozAttr(GetAsDOMNode(nearNode))) { + newBRneeded = true; + parent = node; + offset = aOffset; + newSelNode = true; + } + } + if (!newBRneeded) { + sibling = nearNode; + } + } + if (newBRneeded) { + // if CR does not create a new P, default to BR creation + NS_ENSURE_TRUE(doesCRCreateNewP, NS_OK); + + NS_ENSURE_STATE(mHTMLEditor); + sibling = mHTMLEditor->CreateBR(parent, offset); + if (newSelNode) { + // We split the parent after the br we've just inserted. + selNode = GetAsDOMNode(parent); + selOffset = offset + 1; + } + } + *aHandled = true; + return SplitParagraph(aPara, sibling, aSelection, address_of(selNode), &selOffset); +} + +/** + * SplitParagraph() splits a paragraph at selection point, possibly deleting a + * br. + */ +nsresult +HTMLEditRules::SplitParagraph(nsIDOMNode *aPara, + nsIContent* aBRNode, + Selection* aSelection, + nsCOMPtr<nsIDOMNode>* aSelNode, + int32_t* aOffset) +{ + nsCOMPtr<Element> para = do_QueryInterface(aPara); + NS_ENSURE_TRUE(para && aBRNode && aSelNode && *aSelNode && aOffset && + aSelection, NS_ERROR_NULL_POINTER); + + // split para + // get ws code to adjust any ws + nsCOMPtr<nsIContent> leftPara, rightPara; + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsINode> selNode(do_QueryInterface(*aSelNode)); + nsresult rv = + WSRunObject::PrepareToSplitAcrossBlocks(mHTMLEditor, + address_of(selNode), aOffset); + // XXX When it fails, why do we need to return selection node? (Why can the + // caller trust the result even when it returns error?) + *aSelNode = GetAsDOMNode(selNode); + NS_ENSURE_SUCCESS(rv, rv); + // split the paragraph + NS_ENSURE_STATE(mHTMLEditor); + NS_ENSURE_STATE(selNode->IsContent()); + mHTMLEditor->SplitNodeDeep(*para, *selNode->AsContent(), *aOffset, + HTMLEditor::EmptyContainers::yes, + getter_AddRefs(leftPara), + getter_AddRefs(rightPara)); + // get rid of the break, if it is visible (otherwise it may be needed to prevent an empty p) + NS_ENSURE_STATE(mHTMLEditor); + if (mHTMLEditor->IsVisBreak(aBRNode)) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->DeleteNode(aBRNode); + NS_ENSURE_SUCCESS(rv, rv); + } + + // remove ID attribute on the paragraph we just created + nsCOMPtr<nsIDOMElement> rightElt = do_QueryInterface(rightPara); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->RemoveAttribute(rightElt, NS_LITERAL_STRING("id")); + NS_ENSURE_SUCCESS(rv, rv); + + // check both halves of para to see if we need mozBR + rv = InsertMozBRIfNeeded(*leftPara); + NS_ENSURE_SUCCESS(rv, rv); + rv = InsertMozBRIfNeeded(*rightPara); + NS_ENSURE_SUCCESS(rv, rv); + + // selection to beginning of right hand para; + // look inside any containers that are up front. + nsCOMPtr<nsINode> rightParaNode = do_QueryInterface(rightPara); + NS_ENSURE_STATE(mHTMLEditor && rightParaNode); + nsCOMPtr<nsIDOMNode> child = + GetAsDOMNode(mHTMLEditor->GetLeftmostChild(rightParaNode, true)); + if (mHTMLEditor->IsTextNode(child) || + mHTMLEditor->IsContainer(child)) { + aSelection->Collapse(child,0); + } else { + int32_t offset; + nsCOMPtr<nsIDOMNode> parent = EditorBase::GetNodeLocation(child, &offset); + aSelection->Collapse(parent,offset); + } + return NS_OK; +} + +/** + * ReturnInListItem: do the right thing for returns pressed in list items + */ +nsresult +HTMLEditRules::ReturnInListItem(Selection& aSelection, + Element& aListItem, + nsINode& aNode, + int32_t aOffset) +{ + MOZ_ASSERT(HTMLEditUtils::IsListItem(&aListItem)); + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Get the item parent and the active editing host. + nsCOMPtr<Element> root = htmlEditor->GetActiveEditingHost(); + + nsCOMPtr<Element> list = aListItem.GetParentElement(); + int32_t itemOffset = list ? list->IndexOf(&aListItem) : -1; + + // If we are in an empty item, then we want to pop up out of the list, but + // only if prefs say it's okay and if the parent isn't the active editing + // host. + bool isEmpty; + nsresult rv = IsEmptyBlock(aListItem, &isEmpty, MozBRCounts::no); + NS_ENSURE_SUCCESS(rv, rv); + if (isEmpty && root != list && mReturnInEmptyLIKillsList) { + // Get the list offset now -- before we might eventually split the list + nsCOMPtr<nsINode> listParent = list->GetParentNode(); + int32_t offset = listParent ? listParent->IndexOf(list) : -1; + + // Are we the last list item in the list? + bool isLast; + rv = htmlEditor->IsLastEditableChild(aListItem.AsDOMNode(), &isLast); + NS_ENSURE_SUCCESS(rv, rv); + if (!isLast) { + // We need to split the list! + ErrorResult rv; + htmlEditor->SplitNode(*list, itemOffset, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + } + + // Are we in a sublist? + if (HTMLEditUtils::IsList(listParent)) { + // If so, move item out of this list and into the grandparent list + rv = htmlEditor->MoveNode(&aListItem, listParent, offset + 1); + NS_ENSURE_SUCCESS(rv, rv); + rv = aSelection.Collapse(&aListItem, 0); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Otherwise kill this item + rv = htmlEditor->DeleteNode(&aListItem); + NS_ENSURE_SUCCESS(rv, rv); + + // Time to insert a paragraph + nsCOMPtr<Element> pNode = + htmlEditor->CreateNode(nsGkAtoms::p, listParent, offset + 1); + NS_ENSURE_STATE(pNode); + + // Append a <br> to it + nsCOMPtr<Element> brNode = htmlEditor->CreateBR(pNode, 0); + NS_ENSURE_STATE(brNode); + + // Set selection to before the break + rv = aSelection.Collapse(pNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; + } + + // Else we want a new list item at the same list level. Get ws code to + // adjust any ws. + nsCOMPtr<nsINode> selNode = &aNode; + rv = WSRunObject::PrepareToSplitAcrossBlocks(htmlEditor, + address_of(selNode), &aOffset); + NS_ENSURE_SUCCESS(rv, rv); + // Now split list item + NS_ENSURE_STATE(selNode->IsContent()); + htmlEditor->SplitNodeDeep(aListItem, *selNode->AsContent(), aOffset); + + // Hack: until I can change the damaged doc range code back to being + // extra-inclusive, I have to manually detect certain list items that may be + // left empty. + nsCOMPtr<nsIContent> prevItem = htmlEditor->GetPriorHTMLSibling(&aListItem); + if (prevItem && HTMLEditUtils::IsListItem(prevItem)) { + bool isEmptyNode; + rv = htmlEditor->IsEmptyNode(prevItem, &isEmptyNode); + NS_ENSURE_SUCCESS(rv, rv); + if (isEmptyNode) { + rv = CreateMozBR(prevItem->AsDOMNode(), 0); + NS_ENSURE_SUCCESS(rv, rv); + } else { + rv = htmlEditor->IsEmptyNode(&aListItem, &isEmptyNode, true); + NS_ENSURE_SUCCESS(rv, rv); + if (isEmptyNode) { + nsCOMPtr<nsIAtom> nodeAtom = aListItem.NodeInfo()->NameAtom(); + if (nodeAtom == nsGkAtoms::dd || nodeAtom == nsGkAtoms::dt) { + nsCOMPtr<nsINode> list = aListItem.GetParentNode(); + int32_t itemOffset = list ? list->IndexOf(&aListItem) : -1; + + nsIAtom* listAtom = nodeAtom == nsGkAtoms::dt ? nsGkAtoms::dd + : nsGkAtoms::dt; + nsCOMPtr<Element> newListItem = + htmlEditor->CreateNode(listAtom, list, itemOffset + 1); + NS_ENSURE_STATE(newListItem); + rv = mTextEditor->DeleteNode(&aListItem); + NS_ENSURE_SUCCESS(rv, rv); + rv = aSelection.Collapse(newListItem, 0); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + nsCOMPtr<Element> brNode; + rv = htmlEditor->CopyLastEditableChildStyles(GetAsDOMNode(prevItem), + GetAsDOMNode(&aListItem), + getter_AddRefs(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + if (brNode) { + nsCOMPtr<nsINode> brParent = brNode->GetParentNode(); + int32_t offset = brParent ? brParent->IndexOf(brNode) : -1; + rv = aSelection.Collapse(brParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + } else { + WSRunObject wsObj(htmlEditor, &aListItem, 0); + nsCOMPtr<nsINode> visNode; + int32_t visOffset = 0; + WSType wsType; + wsObj.NextVisibleNode(&aListItem, 0, address_of(visNode), + &visOffset, &wsType); + if (wsType == WSType::special || wsType == WSType::br || + visNode->IsHTMLElement(nsGkAtoms::hr)) { + nsCOMPtr<nsINode> parent = visNode->GetParentNode(); + int32_t offset = parent ? parent->IndexOf(visNode) : -1; + rv = aSelection.Collapse(parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } else { + rv = aSelection.Collapse(visNode, visOffset); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + } + } + } + rv = aSelection.Collapse(&aListItem, 0); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +/** + * MakeBlockquote() puts the list of nodes into one or more blockquotes. + */ +nsresult +HTMLEditRules::MakeBlockquote(nsTArray<OwningNonNull<nsINode>>& aNodeArray) +{ + // The idea here is to put the nodes into a minimal number of blockquotes. + // When the user blockquotes something, they expect one blockquote. That may + // not be possible (for instance, if they have two table cells selected, you + // need two blockquotes inside the cells). + nsCOMPtr<Element> curBlock; + nsCOMPtr<nsINode> prevParent; + + for (auto& curNode : aNodeArray) { + // Get the node to act on, and its location + NS_ENSURE_STATE(curNode->IsContent()); + + // If the node is a table element or list item, dive inside + if (HTMLEditUtils::IsTableElementButNotTable(curNode) || + HTMLEditUtils::IsListItem(curNode)) { + // Forget any previous block + curBlock = nullptr; + // Recursion time + nsTArray<OwningNonNull<nsINode>> childArray; + GetChildNodesForOperation(*curNode, childArray); + nsresult rv = MakeBlockquote(childArray); + NS_ENSURE_SUCCESS(rv, rv); + } + + // If the node has different parent than previous node, further nodes in a + // new parent + if (prevParent) { + if (prevParent != curNode->GetParentNode()) { + // Forget any previous blockquote node we were using + curBlock = nullptr; + prevParent = curNode->GetParentNode(); + } + } else { + prevParent = curNode->GetParentNode(); + } + + // If no curBlock, make one + if (!curBlock) { + nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); + int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; + nsresult rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + curBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent, + offset); + NS_ENSURE_STATE(curBlock); + // remember our new block for postprocessing + mNewBlock = curBlock; + // note: doesn't matter if we set mNewBlock multiple times. + } + + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->MoveNode(curNode->AsContent(), curBlock, -1); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +/** + * RemoveBlockStyle() makes the nodes have no special block type. + */ +nsresult +HTMLEditRules::RemoveBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Intent of this routine is to be used for converting to/from headers, + // paragraphs, pre, and address. Those blocks that pretty much just contain + // inline things... + nsCOMPtr<Element> curBlock; + nsCOMPtr<nsIContent> firstNode, lastNode; + for (auto& curNode : aNodeArray) { + // If curNode is a address, p, header, address, or pre, remove it + if (HTMLEditUtils::IsFormatNode(curNode)) { + // Process any partial progress saved + if (curBlock) { + nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); + NS_ENSURE_SUCCESS(rv, rv); + firstNode = lastNode = curBlock = nullptr; + } + // Remove current block + nsresult rv = htmlEditor->RemoveBlockContainer(*curNode->AsContent()); + NS_ENSURE_SUCCESS(rv, rv); + } else if (curNode->IsAnyOfHTMLElements(nsGkAtoms::table, + nsGkAtoms::tr, + nsGkAtoms::tbody, + nsGkAtoms::td, + nsGkAtoms::li, + nsGkAtoms::blockquote, + nsGkAtoms::div) || + HTMLEditUtils::IsList(curNode)) { + // Process any partial progress saved + if (curBlock) { + nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); + NS_ENSURE_SUCCESS(rv, rv); + firstNode = lastNode = curBlock = nullptr; + } + // Recursion time + nsTArray<OwningNonNull<nsINode>> childArray; + GetChildNodesForOperation(*curNode, childArray); + nsresult rv = RemoveBlockStyle(childArray); + NS_ENSURE_SUCCESS(rv, rv); + } else if (IsInlineNode(curNode)) { + if (curBlock) { + // If so, is this node a descendant? + if (EditorUtils::IsDescendantOf(curNode, curBlock)) { + // Then we don't need to do anything different for this node + lastNode = curNode->AsContent(); + continue; + } + // Otherwise, we have progressed beyond end of curBlock, so let's + // handle it now. We need to remove the portion of curBlock that + // contains [firstNode - lastNode]. + nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); + NS_ENSURE_SUCCESS(rv, rv); + firstNode = lastNode = curBlock = nullptr; + // Fall out and handle curNode + } + curBlock = htmlEditor->GetBlockNodeParent(curNode); + if (curBlock && HTMLEditUtils::IsFormatNode(curBlock)) { + firstNode = lastNode = curNode->AsContent(); + } else { + // Not a block kind that we care about. + curBlock = nullptr; + } + } else if (curBlock) { + // Some node that is already sans block style. Skip over it and process + // any partial progress saved. + nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); + NS_ENSURE_SUCCESS(rv, rv); + firstNode = lastNode = curBlock = nullptr; + } + } + // Process any partial progress saved + if (curBlock) { + nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); + NS_ENSURE_SUCCESS(rv, rv); + firstNode = lastNode = curBlock = nullptr; + } + return NS_OK; +} + +/** + * ApplyBlockStyle() does whatever it takes to make the list of nodes into one + * or more blocks of type aBlockTag. + */ +nsresult +HTMLEditRules::ApplyBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray, + nsIAtom& aBlockTag) +{ + // Intent of this routine is to be used for converting to/from headers, + // paragraphs, pre, and address. Those blocks that pretty much just contain + // inline things... + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Remove all non-editable nodes. Leave them be. + for (int32_t i = aNodeArray.Length() - 1; i >= 0; i--) { + if (!htmlEditor->IsEditable(aNodeArray[i])) { + aNodeArray.RemoveElementAt(i); + } + } + + nsCOMPtr<Element> newBlock; + + nsCOMPtr<Element> curBlock; + for (auto& curNode : aNodeArray) { + nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); + int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; + + // Is it already the right kind of block? + if (curNode->IsHTMLElement(&aBlockTag)) { + // Forget any previous block used for previous inline nodes + curBlock = nullptr; + // Do nothing to this block + continue; + } + + // If curNode is a address, p, header, address, or pre, replace it with a + // new block of correct type. + // XXX: pre can't hold everything the others can + if (HTMLEditUtils::IsMozDiv(curNode) || + HTMLEditUtils::IsFormatNode(curNode)) { + // Forget any previous block used for previous inline nodes + curBlock = nullptr; + newBlock = htmlEditor->ReplaceContainer(curNode->AsElement(), + &aBlockTag, nullptr, nullptr, + EditorBase::eCloneAttributes); + NS_ENSURE_STATE(newBlock); + } else if (HTMLEditUtils::IsTable(curNode) || + HTMLEditUtils::IsList(curNode) || + curNode->IsAnyOfHTMLElements(nsGkAtoms::tbody, + nsGkAtoms::tr, + nsGkAtoms::td, + nsGkAtoms::li, + nsGkAtoms::blockquote, + nsGkAtoms::div)) { + // Forget any previous block used for previous inline nodes + curBlock = nullptr; + // Recursion time + nsTArray<OwningNonNull<nsINode>> childArray; + GetChildNodesForOperation(*curNode, childArray); + if (!childArray.IsEmpty()) { + nsresult rv = ApplyBlockStyle(childArray, aBlockTag); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Make sure we can put a block here + nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<Element> theBlock = + htmlEditor->CreateNode(&aBlockTag, curParent, offset); + NS_ENSURE_STATE(theBlock); + // Remember our new block for postprocessing + mNewBlock = theBlock; + } + } else if (curNode->IsHTMLElement(nsGkAtoms::br)) { + // If the node is a break, we honor it by putting further nodes in a new + // parent + if (curBlock) { + // Forget any previous block used for previous inline nodes + curBlock = nullptr; + nsresult rv = htmlEditor->DeleteNode(curNode); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // The break is the first (or even only) node we encountered. Create a + // block for it. + nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + curBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset); + NS_ENSURE_STATE(curBlock); + // Remember our new block for postprocessing + mNewBlock = curBlock; + // Note: doesn't matter if we set mNewBlock multiple times. + rv = htmlEditor->MoveNode(curNode->AsContent(), curBlock, -1); + NS_ENSURE_SUCCESS(rv, rv); + } + } else if (IsInlineNode(curNode)) { + // If curNode is inline, pull it into curBlock. Note: it's assumed that + // consecutive inline nodes in aNodeArray are actually members of the + // same block parent. This happens to be true now as a side effect of + // how aNodeArray is contructed, but some additional logic should be + // added here if that should change + // + // If curNode is a non editable, drop it if we are going to <pre>. + if (&aBlockTag == nsGkAtoms::pre && !htmlEditor->IsEditable(curNode)) { + // Do nothing to this block + continue; + } + + // If no curBlock, make one + if (!curBlock) { + nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + curBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset); + NS_ENSURE_STATE(curBlock); + // Remember our new block for postprocessing + mNewBlock = curBlock; + // Note: doesn't matter if we set mNewBlock multiple times. + } + + // XXX If curNode is a br, replace it with a return if going to <pre> + + // This is a continuation of some inline nodes that belong together in + // the same block item. Use curBlock. + nsresult rv = htmlEditor->MoveNode(curNode->AsContent(), curBlock, -1); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +/** + * Given a tag name, split inOutParent up to the point where we can insert the + * tag. Adjust inOutParent and inOutOffset to point to new location for tag. + */ +nsresult +HTMLEditRules::SplitAsNeeded(nsIAtom& aTag, + OwningNonNull<nsINode>& aInOutParent, + int32_t& aInOutOffset) +{ + // XXX Is there a better way to do this? + nsCOMPtr<nsINode> parent = aInOutParent.forget(); + nsresult rv = SplitAsNeeded(aTag, parent, aInOutOffset); + aInOutParent = parent.forget(); + return rv; +} + +nsresult +HTMLEditRules::SplitAsNeeded(nsIAtom& aTag, + nsCOMPtr<nsINode>& inOutParent, + int32_t& inOutOffset) +{ + NS_ENSURE_TRUE(inOutParent, NS_ERROR_NULL_POINTER); + + // Check that we have a place that can legally contain the tag + nsCOMPtr<nsINode> tagParent, splitNode; + for (nsCOMPtr<nsINode> parent = inOutParent; parent; + parent = parent->GetParentNode()) { + // Sniffing up the parent tree until we find a legal place for the block + + // Don't leave the active editing host + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsDescendantOfEditorRoot(parent)) { + // XXX Why do we need to check mHTMLEditor again here? + NS_ENSURE_STATE(mHTMLEditor); + if (parent != mHTMLEditor->GetActiveEditingHost()) { + return NS_ERROR_FAILURE; + } + } + + NS_ENSURE_STATE(mHTMLEditor); + if (mHTMLEditor->CanContainTag(*parent, aTag)) { + // Success + tagParent = parent; + break; + } + + splitNode = parent; + } + if (!tagParent) { + // Could not find a place to build tag! + return NS_ERROR_FAILURE; + } + if (splitNode && splitNode->IsContent() && inOutParent->IsContent()) { + // We found a place for block, but above inOutParent. We need to split. + NS_ENSURE_STATE(mHTMLEditor); + int32_t offset = mHTMLEditor->SplitNodeDeep(*splitNode->AsContent(), + *inOutParent->AsContent(), + inOutOffset); + NS_ENSURE_STATE(offset != -1); + inOutParent = tagParent; + inOutOffset = offset; + } + return NS_OK; +} + +/** + * JoinNodesSmart: Join two nodes, doing whatever makes sense for their + * children (which often means joining them, too). aNodeLeft & aNodeRight must + * be same type of node. + * + * Returns the point where they're merged, or (nullptr, -1) on failure. + */ +EditorDOMPoint +HTMLEditRules::JoinNodesSmart(nsIContent& aNodeLeft, + nsIContent& aNodeRight) +{ + // Caller responsible for left and right node being the same type + nsCOMPtr<nsINode> parent = aNodeLeft.GetParentNode(); + NS_ENSURE_TRUE(parent, EditorDOMPoint()); + int32_t parOffset = parent->IndexOf(&aNodeLeft); + nsCOMPtr<nsINode> rightParent = aNodeRight.GetParentNode(); + + // If they don't have the same parent, first move the right node to after the + // left one + if (parent != rightParent) { + NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); + nsresult rv = mHTMLEditor->MoveNode(&aNodeRight, parent, parOffset); + NS_ENSURE_SUCCESS(rv, EditorDOMPoint()); + } + + EditorDOMPoint ret(&aNodeRight, aNodeLeft.Length()); + + // Separate join rules for differing blocks + if (HTMLEditUtils::IsList(&aNodeLeft) || aNodeLeft.GetAsText()) { + // For lists, merge shallow (wouldn't want to combine list items) + nsresult rv = mHTMLEditor->JoinNodes(aNodeLeft, aNodeRight); + NS_ENSURE_SUCCESS(rv, EditorDOMPoint()); + return ret; + } + + // Remember the last left child, and first right child + NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); + nsCOMPtr<nsIContent> lastLeft = mHTMLEditor->GetLastEditableChild(aNodeLeft); + NS_ENSURE_TRUE(lastLeft, EditorDOMPoint()); + + NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); + nsCOMPtr<nsIContent> firstRight = mHTMLEditor->GetFirstEditableChild(aNodeRight); + NS_ENSURE_TRUE(firstRight, EditorDOMPoint()); + + // For list items, divs, etc., merge smart + NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); + nsresult rv = mHTMLEditor->JoinNodes(aNodeLeft, aNodeRight); + NS_ENSURE_SUCCESS(rv, EditorDOMPoint()); + + if (lastLeft && firstRight && mHTMLEditor && + mHTMLEditor->AreNodesSameType(lastLeft, firstRight) && + (lastLeft->GetAsText() || !mHTMLEditor || + (lastLeft->IsElement() && firstRight->IsElement() && + mHTMLEditor->mCSSEditUtils->ElementsSameStyle(lastLeft->AsElement(), + firstRight->AsElement())))) { + NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); + return JoinNodesSmart(*lastLeft, *firstRight); + } + return ret; +} + +Element* +HTMLEditRules::GetTopEnclosingMailCite(nsINode& aNode) +{ + nsCOMPtr<Element> ret; + + for (nsCOMPtr<nsINode> node = &aNode; node; node = node->GetParentNode()) { + if ((IsPlaintextEditor() && node->IsHTMLElement(nsGkAtoms::pre)) || + HTMLEditUtils::IsMailCite(node)) { + ret = node->AsElement(); + } + if (node->IsHTMLElement(nsGkAtoms::body)) { + break; + } + } + + return ret; +} + +nsresult +HTMLEditRules::CacheInlineStyles(nsIDOMNode* aNode) +{ + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + + NS_ENSURE_STATE(mHTMLEditor); + bool useCSS = mHTMLEditor->IsCSSEnabled(); + + for (int32_t j = 0; j < SIZE_STYLE_TABLE; ++j) { + // If type-in state is set, don't intervene + bool typeInSet, unused; + if (NS_WARN_IF(!mHTMLEditor)) { + return NS_ERROR_UNEXPECTED; + } + mHTMLEditor->mTypeInState->GetTypingState(typeInSet, unused, + mCachedStyles[j].tag, mCachedStyles[j].attr, nullptr); + if (typeInSet) { + continue; + } + + bool isSet = false; + nsAutoString outValue; + // Don't use CSS for <font size>, we don't support it usefully (bug 780035) + if (!useCSS || (mCachedStyles[j].tag == nsGkAtoms::font && + mCachedStyles[j].attr.EqualsLiteral("size"))) { + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->IsTextPropertySetByContent(aNode, mCachedStyles[j].tag, + &(mCachedStyles[j].attr), nullptr, + isSet, &outValue); + } else { + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(aNode, + mCachedStyles[j].tag, &(mCachedStyles[j].attr), isSet, outValue, + CSSEditUtils::eComputed); + } + if (isSet) { + mCachedStyles[j].mPresent = true; + mCachedStyles[j].value.Assign(outValue); + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::ReapplyCachedStyles() +{ + // The idea here is to examine our cached list of styles and see if any have + // been removed. If so, add typeinstate for them, so that they will be + // reinserted when new content is added. + + // remember if we are in css mode + NS_ENSURE_STATE(mHTMLEditor); + bool useCSS = mHTMLEditor->IsCSSEnabled(); + + // get selection point; if it doesn't exist, we have nothing to do + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<Selection> selection = mHTMLEditor->GetSelection(); + if (!selection) { + // If the document is removed from its parent document during executing an + // editor operation with DOMMutationEvent or something, there may be no + // selection. + return NS_OK; + } + if (!selection->RangeCount()) { + // Nothing to do + return NS_OK; + } + nsCOMPtr<nsIContent> selNode = + do_QueryInterface(selection->GetRangeAt(0)->GetStartParent()); + if (!selNode) { + // Nothing to do + return NS_OK; + } + + for (int32_t i = 0; i < SIZE_STYLE_TABLE; ++i) { + if (mCachedStyles[i].mPresent) { + bool bFirst, bAny, bAll; + bFirst = bAny = bAll = false; + + nsAutoString curValue; + if (useCSS) { + // check computed style first in css case + NS_ENSURE_STATE(mHTMLEditor); + bAny = mHTMLEditor->mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet( + selNode, mCachedStyles[i].tag, &(mCachedStyles[i].attr), curValue, + CSSEditUtils::eComputed); + } + if (!bAny) { + // then check typeinstate and html style + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetInlinePropertyBase(*mCachedStyles[i].tag, + &(mCachedStyles[i].attr), + &(mCachedStyles[i].value), + &bFirst, &bAny, &bAll, + &curValue, false); + NS_ENSURE_SUCCESS(rv, rv); + } + // this style has disappeared through deletion. Add to our typeinstate: + if (!bAny || IsStyleCachePreservingAction(mTheAction)) { + NS_ENSURE_STATE(mHTMLEditor); + mHTMLEditor->mTypeInState->SetProp(mCachedStyles[i].tag, + mCachedStyles[i].attr, + mCachedStyles[i].value); + } + } + } + + return NS_OK; +} + +void +HTMLEditRules::ClearCachedStyles() +{ + // clear the mPresent bits in mCachedStyles array + for (uint32_t j = 0; j < SIZE_STYLE_TABLE; j++) { + mCachedStyles[j].mPresent = false; + mCachedStyles[j].value.Truncate(); + } +} + +void +HTMLEditRules::AdjustSpecialBreaks() +{ + NS_ENSURE_TRUE_VOID(mHTMLEditor); + + // Gather list of empty nodes + nsTArray<OwningNonNull<nsINode>> nodeArray; + EmptyEditableFunctor functor(mHTMLEditor); + DOMIterator iter; + if (NS_WARN_IF(NS_FAILED(iter.Init(*mDocChangeRange)))) { + return; + } + iter.AppendList(functor, nodeArray); + + // Put moz-br's into these empty li's and td's + for (auto& node : nodeArray) { + // Need to put br at END of node. It may have empty containers in it and + // still pass the "IsEmptyNode" test, and we want the br's to be after + // them. Also, we want the br to be after the selection if the selection + // is in this node. + nsresult rv = CreateMozBR(node->AsDOMNode(), (int32_t)node->Length()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } +} + +nsresult +HTMLEditRules::AdjustWhitespace(Selection* aSelection) +{ + // get selection point + nsCOMPtr<nsIDOMNode> selNode; + int32_t selOffset; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetStartNodeAndOffset(aSelection, + getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // ask whitespace object to tweak nbsp's + NS_ENSURE_STATE(mHTMLEditor); + return WSRunObject(mHTMLEditor, selNode, selOffset).AdjustWhitespace(); +} + +nsresult +HTMLEditRules::PinSelectionToNewBlock(Selection* aSelection) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + if (!aSelection->Collapsed()) { + return NS_OK; + } + + // get the (collapsed) selection location + nsCOMPtr<nsIDOMNode> selNode, temp; + int32_t selOffset; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetStartNodeAndOffset(aSelection, + getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + temp = selNode; + + // use ranges and sRangeHelper to compare sel point to new block + nsCOMPtr<nsINode> node = do_QueryInterface(selNode); + NS_ENSURE_STATE(node); + RefPtr<nsRange> range = new nsRange(node); + rv = range->SetStart(selNode, selOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->SetEnd(selNode, selOffset); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIContent> block = mNewBlock.get(); + NS_ENSURE_TRUE(block, NS_ERROR_NO_INTERFACE); + bool nodeBefore, nodeAfter; + rv = nsRange::CompareNodeToRange(block, range, &nodeBefore, &nodeAfter); + NS_ENSURE_SUCCESS(rv, rv); + + if (nodeBefore && nodeAfter) { + return NS_OK; // selection is inside block + } else if (nodeBefore) { + // selection is after block. put at end of block. + nsCOMPtr<nsIDOMNode> tmp = GetAsDOMNode(mNewBlock); + NS_ENSURE_STATE(mHTMLEditor); + tmp = GetAsDOMNode(mHTMLEditor->GetLastEditableChild(*block)); + uint32_t endPoint; + if (mHTMLEditor->IsTextNode(tmp) || + mHTMLEditor->IsContainer(tmp)) { + rv = EditorBase::GetLengthOfDOMNode(tmp, endPoint); + NS_ENSURE_SUCCESS(rv, rv); + } else { + tmp = EditorBase::GetNodeLocation(tmp, (int32_t*)&endPoint); + endPoint++; // want to be after this node + } + return aSelection->Collapse(tmp, (int32_t)endPoint); + } else { + // selection is before block. put at start of block. + nsCOMPtr<nsIDOMNode> tmp = GetAsDOMNode(mNewBlock); + NS_ENSURE_STATE(mHTMLEditor); + tmp = GetAsDOMNode(mHTMLEditor->GetFirstEditableChild(*block)); + int32_t offset; + if (mHTMLEditor->IsTextNode(tmp) || + mHTMLEditor->IsContainer(tmp)) { + tmp = EditorBase::GetNodeLocation(tmp, &offset); + } + return aSelection->Collapse(tmp, 0); + } +} + +void +HTMLEditRules::CheckInterlinePosition(Selection& aSelection) +{ + // If the selection isn't collapsed, do nothing. + if (!aSelection.Collapsed()) { + return; + } + + NS_ENSURE_TRUE_VOID(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Get the (collapsed) selection location + NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) && + aSelection.GetRangeAt(0)->GetStartParent()); + OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent(); + int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset(); + + // First, let's check to see if we are after a <br>. We take care of this + // special-case first so that we don't accidentally fall through into one of + // the other conditionals. + nsCOMPtr<nsIContent> node = + htmlEditor->GetPriorHTMLNode(selNode, selOffset, true); + if (node && node->IsHTMLElement(nsGkAtoms::br)) { + aSelection.SetInterlinePosition(true); + return; + } + + // Are we after a block? If so try set caret to following content + node = htmlEditor->GetPriorHTMLSibling(selNode, selOffset); + if (node && IsBlockNode(*node)) { + aSelection.SetInterlinePosition(true); + return; + } + + // Are we before a block? If so try set caret to prior content + node = htmlEditor->GetNextHTMLSibling(selNode, selOffset); + if (node && IsBlockNode(*node)) { + aSelection.SetInterlinePosition(false); + } +} + +nsresult +HTMLEditRules::AdjustSelection(Selection* aSelection, + nsIEditor::EDirection aAction) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + + // if the selection isn't collapsed, do nothing. + // moose: one thing to do instead is check for the case of + // only a single break selected, and collapse it. Good thing? Beats me. + if (!aSelection->Collapsed()) { + return NS_OK; + } + + // get the (collapsed) selection location + nsCOMPtr<nsINode> selNode, temp; + int32_t selOffset; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetStartNodeAndOffset(aSelection, + getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + temp = selNode; + + // are we in an editable node? + NS_ENSURE_STATE(mHTMLEditor); + while (!mHTMLEditor->IsEditable(selNode)) { + // scan up the tree until we find an editable place to be + selNode = EditorBase::GetNodeLocation(temp, &selOffset); + NS_ENSURE_TRUE(selNode, NS_ERROR_FAILURE); + temp = selNode; + NS_ENSURE_STATE(mHTMLEditor); + } + + // make sure we aren't in an empty block - user will see no cursor. If this + // is happening, put a <br> in the block if allowed. + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> theblock = mHTMLEditor->GetBlock(*selNode); + + if (theblock && mHTMLEditor->IsEditable(theblock)) { + bool bIsEmptyNode; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->IsEmptyNode(theblock, &bIsEmptyNode, false, false); + NS_ENSURE_SUCCESS(rv, rv); + // check if br can go into the destination node + NS_ENSURE_STATE(mHTMLEditor); + if (bIsEmptyNode && mHTMLEditor->CanContainTag(*selNode, *nsGkAtoms::br)) { + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> rootNode = mHTMLEditor->GetRoot(); + NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE); + if (selNode == rootNode) { + // Our root node is completely empty. Don't add a <br> here. + // AfterEditInner() will add one for us when it calls + // CreateBogusNodeIfNeeded()! + return NS_OK; + } + + // we know we can skip the rest of this routine given the cirumstance + return CreateMozBR(GetAsDOMNode(selNode), selOffset); + } + } + + // are we in a text node? + nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(selNode); + if (textNode) + return NS_OK; // we LIKE it when we are in a text node. that RULZ + + // do we need to insert a special mozBR? We do if we are: + // 1) prior node is in same block where selection is AND + // 2) prior node is a br AND + // 3) that br is not visible + + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIContent> nearNode = + mHTMLEditor->GetPriorHTMLNode(selNode, selOffset); + if (nearNode) { + // is nearNode also a descendant of same block? + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*selNode); + nsCOMPtr<Element> nearBlock = mHTMLEditor->GetBlockNodeParent(nearNode); + if (block && block == nearBlock) { + if (nearNode && TextEditUtils::IsBreak(nearNode)) { + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsVisBreak(nearNode)) { + // need to insert special moz BR. Why? Because if we don't + // the user will see no new line for the break. Also, things + // like table cells won't grow in height. + nsCOMPtr<nsIDOMNode> brNode; + rv = CreateMozBR(GetAsDOMNode(selNode), selOffset, + getter_AddRefs(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIDOMNode> brParent = + EditorBase::GetNodeLocation(brNode, &selOffset); + // selection stays *before* moz-br, sticking to it + aSelection->SetInterlinePosition(true); + rv = aSelection->Collapse(brParent, selOffset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIContent> nextNode = + mHTMLEditor->GetNextHTMLNode(nearNode, true); + if (nextNode && TextEditUtils::IsMozBR(nextNode)) { + // selection between br and mozbr. make it stick to mozbr + // so that it will be on blank line. + aSelection->SetInterlinePosition(true); + } + } + } + } + } + + // we aren't in a textnode: are we adjacent to text or a break or an image? + NS_ENSURE_STATE(mHTMLEditor); + nearNode = mHTMLEditor->GetPriorHTMLNode(selNode, selOffset, true); + if (nearNode && (TextEditUtils::IsBreak(nearNode) || + EditorBase::IsTextNode(nearNode) || + HTMLEditUtils::IsImage(nearNode) || + nearNode->IsHTMLElement(nsGkAtoms::hr))) { + // this is a good place for the caret to be + return NS_OK; + } + NS_ENSURE_STATE(mHTMLEditor); + nearNode = mHTMLEditor->GetNextHTMLNode(selNode, selOffset, true); + if (nearNode && (TextEditUtils::IsBreak(nearNode) || + EditorBase::IsTextNode(nearNode) || + nearNode->IsAnyOfHTMLElements(nsGkAtoms::img, + nsGkAtoms::hr))) { + return NS_OK; // this is a good place for the caret to be + } + + // look for a nearby text node. + // prefer the correct direction. + nsCOMPtr<nsIDOMNode> nearNodeDOM = GetAsDOMNode(nearNode); + rv = FindNearSelectableNode(GetAsDOMNode(selNode), selOffset, aAction, + address_of(nearNodeDOM)); + NS_ENSURE_SUCCESS(rv, rv); + nearNode = do_QueryInterface(nearNodeDOM); + + if (!nearNode) { + return NS_OK; + } + EditorDOMPoint pt = GetGoodSelPointForNode(*nearNode, aAction); + rv = aSelection->Collapse(pt.node, pt.offset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + + +nsresult +HTMLEditRules::FindNearSelectableNode(nsIDOMNode* aSelNode, + int32_t aSelOffset, + nsIEditor::EDirection& aDirection, + nsCOMPtr<nsIDOMNode>* outSelectableNode) +{ + NS_ENSURE_TRUE(aSelNode && outSelectableNode, NS_ERROR_NULL_POINTER); + *outSelectableNode = nullptr; + + nsCOMPtr<nsIDOMNode> nearNode, curNode; + if (aDirection == nsIEditor::ePrevious) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset, address_of(nearNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset, address_of(nearNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Try the other direction then. + if (!nearNode) { + if (aDirection == nsIEditor::ePrevious) { + aDirection = nsIEditor::eNext; + } else { + aDirection = nsIEditor::ePrevious; + } + + if (aDirection == nsIEditor::ePrevious) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset, + address_of(nearNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset, + address_of(nearNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + // scan in the right direction until we find an eligible text node, + // but don't cross any breaks, images, or table elements. + NS_ENSURE_STATE(mHTMLEditor); + while (nearNode && !(mHTMLEditor->IsTextNode(nearNode) || + TextEditUtils::IsBreak(nearNode) || + HTMLEditUtils::IsImage(nearNode))) { + curNode = nearNode; + if (aDirection == nsIEditor::ePrevious) { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetPriorHTMLNode(curNode, address_of(nearNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->GetNextHTMLNode(curNode, address_of(nearNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + NS_ENSURE_STATE(mHTMLEditor); + } + + if (nearNode) { + // don't cross any table elements + if (InDifferentTableElements(nearNode, aSelNode)) { + return NS_OK; + } + + // otherwise, ok, we have found a good spot to put the selection + *outSelectableNode = do_QueryInterface(nearNode); + } + return NS_OK; +} + +bool +HTMLEditRules::InDifferentTableElements(nsIDOMNode* aNode1, + nsIDOMNode* aNode2) +{ + nsCOMPtr<nsINode> node1 = do_QueryInterface(aNode1); + nsCOMPtr<nsINode> node2 = do_QueryInterface(aNode2); + return InDifferentTableElements(node1, node2); +} + +bool +HTMLEditRules::InDifferentTableElements(nsINode* aNode1, + nsINode* aNode2) +{ + MOZ_ASSERT(aNode1 && aNode2); + + while (aNode1 && !HTMLEditUtils::IsTableElement(aNode1)) { + aNode1 = aNode1->GetParentNode(); + } + + while (aNode2 && !HTMLEditUtils::IsTableElement(aNode2)) { + aNode2 = aNode2->GetParentNode(); + } + + return aNode1 != aNode2; +} + + +nsresult +HTMLEditRules::RemoveEmptyNodes() +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + // Some general notes on the algorithm used here: the goal is to examine all + // the nodes in mDocChangeRange, and remove the empty ones. We do this by + // using a content iterator to traverse all the nodes in the range, and + // placing the empty nodes into an array. After finishing the iteration, we + // delete the empty nodes in the array. (They cannot be deleted as we find + // them because that would invalidate the iterator.) + // + // Since checking to see if a node is empty can be costly for nodes with many + // descendants, there are some optimizations made. I rely on the fact that + // the iterator is post-order: it will visit children of a node before + // visiting the parent node. So if I find that a child node is not empty, I + // know that its parent is not empty without even checking. So I put the + // parent on a "skipList" which is just a voidArray of nodes I can skip the + // empty check on. If I encounter a node on the skiplist, i skip the + // processing for that node and replace its slot in the skiplist with that + // node's parent. + // + // An interesting idea is to go ahead and regard parent nodes that are NOT on + // the skiplist as being empty (without even doing the IsEmptyNode check) on + // the theory that if they weren't empty, we would have encountered a + // non-empty child earlier and thus put this parent node on the skiplist. + // + // Unfortunately I can't use that strategy here, because the range may + // include some children of a node while excluding others. Thus I could find + // all the _examined_ children empty, but still not have an empty parent. + + // need an iterator + nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator(); + + nsresult rv = iter->Init(mDocChangeRange); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<OwningNonNull<nsINode>> arrayOfEmptyNodes, arrayOfEmptyCites, skipList; + + // Check for empty nodes + while (!iter->IsDone()) { + OwningNonNull<nsINode> node = *iter->GetCurrentNode(); + + nsCOMPtr<nsINode> parent = node->GetParentNode(); + + size_t idx = skipList.IndexOf(node); + if (idx != skipList.NoIndex) { + // This node is on our skip list. Skip processing for this node, and + // replace its value in the skip list with the value of its parent + if (parent) { + skipList[idx] = parent; + } + } else { + bool bIsCandidate = false; + bool bIsEmptyNode = false; + bool bIsMailCite = false; + + if (node->IsElement()) { + if (node->IsHTMLElement(nsGkAtoms::body)) { + // Don't delete the body + } else if ((bIsMailCite = HTMLEditUtils::IsMailCite(node)) || + node->IsHTMLElement(nsGkAtoms::a) || + HTMLEditUtils::IsInlineStyle(node) || + HTMLEditUtils::IsList(node) || + node->IsHTMLElement(nsGkAtoms::div)) { + // Only consider certain nodes to be empty for purposes of removal + bIsCandidate = true; + } else if (HTMLEditUtils::IsFormatNode(node) || + HTMLEditUtils::IsListItem(node) || + node->IsHTMLElement(nsGkAtoms::blockquote)) { + // These node types are candidates if selection is not in them. If + // it is one of these, don't delete if selection inside. This is so + // we can create empty headings, etc., for the user to type into. + bool bIsSelInNode; + rv = SelectionEndpointInNode(node, &bIsSelInNode); + NS_ENSURE_SUCCESS(rv, rv); + if (!bIsSelInNode) { + bIsCandidate = true; + } + } + } + + if (bIsCandidate) { + // We delete mailcites even if they have a solo br in them. Other + // nodes we require to be empty. + rv = htmlEditor->IsEmptyNode(node->AsDOMNode(), &bIsEmptyNode, + bIsMailCite, true); + NS_ENSURE_SUCCESS(rv, rv); + if (bIsEmptyNode) { + if (bIsMailCite) { + // mailcites go on a separate list from other empty nodes + arrayOfEmptyCites.AppendElement(*node); + } else { + arrayOfEmptyNodes.AppendElement(*node); + } + } + } + + if (!bIsEmptyNode && parent) { + // put parent on skip list + skipList.AppendElement(*parent); + } + } + + iter->Next(); + } + + // now delete the empty nodes + for (auto& delNode : arrayOfEmptyNodes) { + if (htmlEditor->IsModifiableNode(delNode)) { + rv = htmlEditor->DeleteNode(delNode); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Now delete the empty mailcites. This is a separate step because we want + // to pull out any br's and preserve them. + for (auto& delNode : arrayOfEmptyCites) { + bool bIsEmptyNode; + rv = htmlEditor->IsEmptyNode(delNode, &bIsEmptyNode, false, true); + NS_ENSURE_SUCCESS(rv, rv); + if (!bIsEmptyNode) { + // We are deleting a cite that has just a br. We want to delete cite, + // but preserve br. + nsCOMPtr<nsINode> parent = delNode->GetParentNode(); + int32_t offset = parent ? parent->IndexOf(delNode) : -1; + nsCOMPtr<Element> br = htmlEditor->CreateBR(parent, offset); + NS_ENSURE_STATE(br); + } + rv = htmlEditor->DeleteNode(delNode); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +HTMLEditRules::SelectionEndpointInNode(nsINode* aNode, + bool* aResult) +{ + NS_ENSURE_TRUE(aNode && aResult, NS_ERROR_NULL_POINTER); + + nsIDOMNode* node = aNode->AsDOMNode(); + + *aResult = false; + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<Selection> selection = mHTMLEditor->GetSelection(); + NS_ENSURE_STATE(selection); + + uint32_t rangeCount = selection->RangeCount(); + for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) { + RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); + nsCOMPtr<nsIDOMNode> startParent, endParent; + range->GetStartContainer(getter_AddRefs(startParent)); + if (startParent) { + if (node == startParent) { + *aResult = true; + return NS_OK; + } + if (EditorUtils::IsDescendantOf(startParent, node)) { + *aResult = true; + return NS_OK; + } + } + range->GetEndContainer(getter_AddRefs(endParent)); + if (startParent == endParent) { + continue; + } + if (endParent) { + if (node == endParent) { + *aResult = true; + return NS_OK; + } + if (EditorUtils::IsDescendantOf(endParent, node)) { + *aResult = true; + return NS_OK; + } + } + } + return NS_OK; +} + +/** + * IsEmptyInline: Return true if aNode is an empty inline container + */ +bool +HTMLEditRules::IsEmptyInline(nsINode& aNode) +{ + NS_ENSURE_TRUE(mHTMLEditor, false); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + if (IsInlineNode(aNode) && htmlEditor->IsContainer(&aNode)) { + bool isEmpty = true; + htmlEditor->IsEmptyNode(&aNode, &isEmpty); + return isEmpty; + } + return false; +} + + +bool +HTMLEditRules::ListIsEmptyLine(nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes) +{ + // We have a list of nodes which we are candidates for being moved into a new + // block. Determine if it's anything more than a blank line. Look for + // editable content above and beyond one single BR. + NS_ENSURE_TRUE(aArrayOfNodes.Length(), true); + + NS_ENSURE_TRUE(mHTMLEditor, false); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + int32_t brCount = 0; + + for (auto& node : aArrayOfNodes) { + if (!htmlEditor->IsEditable(node)) { + continue; + } + if (TextEditUtils::IsBreak(node)) { + // First break doesn't count + if (brCount) { + return false; + } + brCount++; + } else if (IsEmptyInline(node)) { + // Empty inline, keep looking + } else { + return false; + } + } + return true; +} + + +nsresult +HTMLEditRules::PopListItem(nsIDOMNode* aListItem, + bool* aOutOfList) +{ + nsCOMPtr<Element> listItem = do_QueryInterface(aListItem); + // check parms + NS_ENSURE_TRUE(listItem && aOutOfList, NS_ERROR_NULL_POINTER); + + // init out params + *aOutOfList = false; + + nsCOMPtr<nsINode> curParent = listItem->GetParentNode(); + int32_t offset = curParent ? curParent->IndexOf(listItem) : -1; + + if (!HTMLEditUtils::IsListItem(listItem)) { + return NS_ERROR_FAILURE; + } + + // if it's first or last list item, don't need to split the list + // otherwise we do. + nsCOMPtr<nsINode> curParPar = curParent->GetParentNode(); + int32_t parOffset = curParPar ? curParPar->IndexOf(curParent) : -1; + + bool bIsFirstListItem; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->IsFirstEditableChild(aListItem, &bIsFirstListItem); + NS_ENSURE_SUCCESS(rv, rv); + + bool bIsLastListItem; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->IsLastEditableChild(aListItem, &bIsLastListItem); + NS_ENSURE_SUCCESS(rv, rv); + + if (!bIsFirstListItem && !bIsLastListItem) { + // split the list + nsCOMPtr<nsIDOMNode> newBlock; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->SplitNode(GetAsDOMNode(curParent), offset, + getter_AddRefs(newBlock)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!bIsFirstListItem) { + parOffset++; + } + + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->MoveNode(listItem, curParPar, parOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // unwrap list item contents if they are no longer in a list + if (!HTMLEditUtils::IsList(curParPar) && + HTMLEditUtils::IsListItem(listItem)) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->RemoveBlockContainer(*listItem); + NS_ENSURE_SUCCESS(rv, rv); + *aOutOfList = true; + } + return NS_OK; +} + +nsresult +HTMLEditRules::RemoveListStructure(Element& aList) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + while (aList.GetFirstChild()) { + OwningNonNull<nsIContent> child = *aList.GetFirstChild(); + + if (HTMLEditUtils::IsListItem(child)) { + bool isOutOfList; + // Keep popping it out until it's not in a list anymore + do { + nsresult rv = PopListItem(child->AsDOMNode(), &isOutOfList); + NS_ENSURE_SUCCESS(rv, rv); + } while (!isOutOfList); + } else if (HTMLEditUtils::IsList(child)) { + nsresult rv = RemoveListStructure(*child->AsElement()); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Delete any non-list items for now + nsresult rv = htmlEditor->DeleteNode(child); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Delete the now-empty list + nsresult rv = htmlEditor->RemoveBlockContainer(aList); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +HTMLEditRules::ConfirmSelectionInBody() +{ + // get the body + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(mHTMLEditor->GetRoot()); + NS_ENSURE_TRUE(rootElement, NS_ERROR_UNEXPECTED); + + // get the selection + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<Selection> selection = mHTMLEditor->GetSelection(); + NS_ENSURE_STATE(selection); + + // get the selection start location + nsCOMPtr<nsIDOMNode> selNode, temp, parent; + int32_t selOffset; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetStartNodeAndOffset(selection, + getter_AddRefs(selNode), &selOffset); + if (NS_FAILED(rv)) { + return rv; + } + + temp = selNode; + + // check that selNode is inside body + while (temp && !TextEditUtils::IsBody(temp)) { + temp->GetParentNode(getter_AddRefs(parent)); + temp = parent; + } + + // if we aren't in the body, force the issue + if (!temp) { +// uncomment this to see when we get bad selections +// NS_NOTREACHED("selection not in body"); + selection->Collapse(rootElement, 0); + } + + // get the selection end location + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->GetEndNodeAndOffset(selection, + getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + temp = selNode; + + // check that selNode is inside body + while (temp && !TextEditUtils::IsBody(temp)) { + rv = temp->GetParentNode(getter_AddRefs(parent)); + temp = parent; + } + + // if we aren't in the body, force the issue + if (!temp) { +// uncomment this to see when we get bad selections +// NS_NOTREACHED("selection not in body"); + selection->Collapse(rootElement, 0); + } + + // XXX This is the result of the last call of GetParentNode(), it doesn't + // make sense... + return rv; +} + +nsresult +HTMLEditRules::UpdateDocChangeRange(nsRange* aRange) +{ + // first make sure aRange is in the document. It might not be if + // portions of our editting action involved manipulating nodes + // prior to placing them in the document (e.g., populating a list item + // before placing it in its list) + nsCOMPtr<nsIDOMNode> startNode; + nsresult rv = aRange->GetStartContainer(getter_AddRefs(startNode)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + if (!mHTMLEditor->IsDescendantOfRoot(startNode)) { + // just return - we don't need to adjust mDocChangeRange in this case + return NS_OK; + } + + if (!mDocChangeRange) { + // clone aRange. + mDocChangeRange = aRange->CloneRange(); + } else { + int16_t result; + + // compare starts of ranges + rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START, + aRange, &result); + if (rv == NS_ERROR_NOT_INITIALIZED) { + // This will happen is mDocChangeRange is non-null, but the range is + // uninitialized. In this case we'll set the start to aRange start. + // The same test won't be needed further down since after we've set + // the start the range will be collapsed to that point. + result = 1; + rv = NS_OK; + } + NS_ENSURE_SUCCESS(rv, rv); + // Positive result means mDocChangeRange start is after aRange start. + if (result > 0) { + int32_t startOffset; + rv = aRange->GetStartOffset(&startOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mDocChangeRange->SetStart(startNode, startOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + // compare ends of ranges + rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::END_TO_END, + aRange, &result); + NS_ENSURE_SUCCESS(rv, rv); + // Negative result means mDocChangeRange end is before aRange end. + if (result < 0) { + nsCOMPtr<nsIDOMNode> endNode; + int32_t endOffset; + rv = aRange->GetEndContainer(getter_AddRefs(endNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aRange->GetEndOffset(&endOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mDocChangeRange->SetEnd(endNode, endOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::InsertMozBRIfNeeded(nsINode& aNode) +{ + if (!IsBlockNode(aNode)) { + return NS_OK; + } + + bool isEmpty; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->IsEmptyNode(&aNode, &isEmpty); + NS_ENSURE_SUCCESS(rv, rv); + if (!isEmpty) { + return NS_OK; + } + + return CreateMozBR(aNode.AsDOMNode(), 0); +} + +NS_IMETHODIMP +HTMLEditRules::WillCreateNode(const nsAString& aTag, + nsIDOMNode* aParent, + int32_t aPosition) +{ + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditRules::DidCreateNode(const nsAString& aTag, + nsIDOMNode* aNode, + nsIDOMNode* aParent, + int32_t aPosition, + nsresult aResult) +{ + if (!mListenerEnabled) { + return NS_OK; + } + // assumption that Join keeps the righthand node + nsresult rv = mUtilRange->SelectNode(aNode); + NS_ENSURE_SUCCESS(rv, rv); + return UpdateDocChangeRange(mUtilRange); +} + +NS_IMETHODIMP +HTMLEditRules::WillInsertNode(nsIDOMNode* aNode, + nsIDOMNode* aParent, + int32_t aPosition) +{ + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditRules::DidInsertNode(nsIDOMNode* aNode, + nsIDOMNode* aParent, + int32_t aPosition, + nsresult aResult) +{ + if (!mListenerEnabled) { + return NS_OK; + } + nsresult rv = mUtilRange->SelectNode(aNode); + NS_ENSURE_SUCCESS(rv, rv); + return UpdateDocChangeRange(mUtilRange); +} + +NS_IMETHODIMP +HTMLEditRules::WillDeleteNode(nsIDOMNode* aChild) +{ + if (!mListenerEnabled) { + return NS_OK; + } + nsresult rv = mUtilRange->SelectNode(aChild); + NS_ENSURE_SUCCESS(rv, rv); + return UpdateDocChangeRange(mUtilRange); +} + +NS_IMETHODIMP +HTMLEditRules::DidDeleteNode(nsIDOMNode* aChild, + nsresult aResult) +{ + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditRules::WillSplitNode(nsIDOMNode* aExistingRightNode, + int32_t aOffset) +{ + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditRules::DidSplitNode(nsIDOMNode* aExistingRightNode, + int32_t aOffset, + nsIDOMNode* aNewLeftNode, + nsresult aResult) +{ + if (!mListenerEnabled) { + return NS_OK; + } + nsresult rv = mUtilRange->SetStart(aNewLeftNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + rv = mUtilRange->SetEnd(aExistingRightNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + return UpdateDocChangeRange(mUtilRange); +} + +NS_IMETHODIMP +HTMLEditRules::WillJoinNodes(nsIDOMNode* aLeftNode, + nsIDOMNode* aRightNode, + nsIDOMNode* aParent) +{ + if (!mListenerEnabled) { + return NS_OK; + } + // remember split point + return EditorBase::GetLengthOfDOMNode(aLeftNode, mJoinOffset); +} + +NS_IMETHODIMP +HTMLEditRules::DidJoinNodes(nsIDOMNode* aLeftNode, + nsIDOMNode* aRightNode, + nsIDOMNode* aParent, + nsresult aResult) +{ + if (!mListenerEnabled) { + return NS_OK; + } + // assumption that Join keeps the righthand node + nsresult rv = mUtilRange->SetStart(aRightNode, mJoinOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mUtilRange->SetEnd(aRightNode, mJoinOffset); + NS_ENSURE_SUCCESS(rv, rv); + return UpdateDocChangeRange(mUtilRange); +} + +NS_IMETHODIMP +HTMLEditRules::WillInsertText(nsIDOMCharacterData* aTextNode, + int32_t aOffset, + const nsAString& aString) +{ + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditRules::DidInsertText(nsIDOMCharacterData* aTextNode, + int32_t aOffset, + const nsAString& aString, + nsresult aResult) +{ + if (!mListenerEnabled) { + return NS_OK; + } + int32_t length = aString.Length(); + nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode); + nsresult rv = mUtilRange->SetStart(theNode, aOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mUtilRange->SetEnd(theNode, aOffset+length); + NS_ENSURE_SUCCESS(rv, rv); + return UpdateDocChangeRange(mUtilRange); +} + +NS_IMETHODIMP +HTMLEditRules::WillDeleteText(nsIDOMCharacterData* aTextNode, + int32_t aOffset, + int32_t aLength) +{ + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditRules::DidDeleteText(nsIDOMCharacterData* aTextNode, + int32_t aOffset, + int32_t aLength, + nsresult aResult) +{ + if (!mListenerEnabled) { + return NS_OK; + } + nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode); + nsresult rv = mUtilRange->SetStart(theNode, aOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mUtilRange->SetEnd(theNode, aOffset); + NS_ENSURE_SUCCESS(rv, rv); + return UpdateDocChangeRange(mUtilRange); +} + +NS_IMETHODIMP +HTMLEditRules::WillDeleteSelection(nsISelection* aSelection) +{ + if (!mListenerEnabled) { + return NS_OK; + } + if (NS_WARN_IF(!aSelection)) { + return NS_ERROR_INVALID_ARG; + } + RefPtr<Selection> selection = aSelection->AsSelection(); + // get the (collapsed) selection location + nsCOMPtr<nsIDOMNode> selNode; + int32_t selOffset; + + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetStartNodeAndOffset(selection, + getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mUtilRange->SetStart(selNode, selOffset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->GetEndNodeAndOffset(selection, + getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mUtilRange->SetEnd(selNode, selOffset); + NS_ENSURE_SUCCESS(rv, rv); + return UpdateDocChangeRange(mUtilRange); +} + +NS_IMETHODIMP +HTMLEditRules::DidDeleteSelection(nsISelection *aSelection) +{ + return NS_OK; +} + +// Let's remove all alignment hints in the children of aNode; it can +// be an ALIGN attribute (in case we just remove it) or a CENTER +// element (here we have to remove the container and keep its +// children). We break on tables and don't look at their children. +nsresult +HTMLEditRules::RemoveAlignment(nsIDOMNode* aNode, + const nsAString& aAlignType, + bool aChildrenOnly) +{ + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + + NS_ENSURE_STATE(mHTMLEditor); + if (mHTMLEditor->IsTextNode(aNode) || HTMLEditUtils::IsTable(aNode)) { + return NS_OK; + } + + nsCOMPtr<nsIDOMNode> child = aNode,tmp; + if (aChildrenOnly) { + aNode->GetFirstChild(getter_AddRefs(child)); + } + NS_ENSURE_STATE(mHTMLEditor); + bool useCSS = mHTMLEditor->IsCSSEnabled(); + + while (child) { + if (aChildrenOnly) { + // get the next sibling right now because we could have to remove child + child->GetNextSibling(getter_AddRefs(tmp)); + } else { + tmp = nullptr; + } + bool isBlock; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->NodeIsBlockStatic(child, &isBlock); + NS_ENSURE_SUCCESS(rv, rv); + + if (EditorBase::NodeIsType(child, nsGkAtoms::center)) { + // the current node is a CENTER element + // first remove children's alignment + rv = RemoveAlignment(child, aAlignType, true); + NS_ENSURE_SUCCESS(rv, rv); + + // we may have to insert BRs in first and last position of element's children + // if the nodes before/after are not blocks and not BRs + rv = MakeSureElemStartsOrEndsOnCR(child); + NS_ENSURE_SUCCESS(rv, rv); + + // now remove the CENTER container + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<Element> childAsElement = do_QueryInterface(child); + NS_ENSURE_STATE(childAsElement); + rv = mHTMLEditor->RemoveContainer(childAsElement); + NS_ENSURE_SUCCESS(rv, rv); + } else if (isBlock || HTMLEditUtils::IsHR(child)) { + // the current node is a block element + nsCOMPtr<nsIDOMElement> curElem = do_QueryInterface(child); + if (HTMLEditUtils::SupportsAlignAttr(child)) { + // remove the ALIGN attribute if this element can have it + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->RemoveAttribute(curElem, NS_LITERAL_STRING("align")); + NS_ENSURE_SUCCESS(rv, rv); + } + if (useCSS) { + if (HTMLEditUtils::IsTable(child) || HTMLEditUtils::IsHR(child)) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->SetAttributeOrEquivalent(curElem, + NS_LITERAL_STRING("align"), + aAlignType, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsAutoString dummyCssValue; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->mCSSEditUtils->RemoveCSSInlineStyle( + child, + nsGkAtoms::textAlign, + dummyCssValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + if (!HTMLEditUtils::IsTable(child)) { + // unless this is a table, look at children + rv = RemoveAlignment(child, aAlignType, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } + child = tmp; + } + return NS_OK; +} + +// Let's insert a BR as first (resp. last) child of aNode if its +// first (resp. last) child is not a block nor a BR, and if the +// previous (resp. next) sibling is not a block nor a BR +nsresult +HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode, + bool aStarts) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMNode> child; + if (aStarts) { + NS_ENSURE_STATE(mHTMLEditor); + child = GetAsDOMNode(mHTMLEditor->GetFirstEditableChild(*node)); + } else { + NS_ENSURE_STATE(mHTMLEditor); + child = GetAsDOMNode(mHTMLEditor->GetLastEditableChild(*node)); + } + NS_ENSURE_TRUE(child, NS_OK); + bool isChildBlock; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = mHTMLEditor->NodeIsBlockStatic(child, &isChildBlock); + NS_ENSURE_SUCCESS(rv, rv); + bool foundCR = false; + if (isChildBlock || TextEditUtils::IsBreak(child)) { + foundCR = true; + } else { + nsCOMPtr<nsIDOMNode> sibling; + if (aStarts) { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->GetPriorHTMLSibling(aNode, address_of(sibling)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->GetNextHTMLSibling(aNode, address_of(sibling)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + if (sibling) { + bool isBlock; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->NodeIsBlockStatic(sibling, &isBlock); + NS_ENSURE_SUCCESS(rv, rv); + if (isBlock || TextEditUtils::IsBreak(sibling)) { + foundCR = true; + } + } else { + foundCR = true; + } + } + if (!foundCR) { + int32_t offset = 0; + if (!aStarts) { + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_STATE(node); + offset = node->GetChildCount(); + } + nsCOMPtr<nsIDOMNode> brNode; + NS_ENSURE_STATE(mHTMLEditor); + rv = mHTMLEditor->CreateBR(aNode, offset, address_of(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult +HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode) +{ + nsresult rv = MakeSureElemStartsOrEndsOnCR(aNode, false); + NS_ENSURE_SUCCESS(rv, rv); + return MakeSureElemStartsOrEndsOnCR(aNode, true); +} + +nsresult +HTMLEditRules::AlignBlock(Element& aElement, + const nsAString& aAlignType, + ContentsOnly aContentsOnly) +{ + if (!IsBlockNode(aElement) && !aElement.IsHTMLElement(nsGkAtoms::hr)) { + // We deal only with blocks; early way out + return NS_OK; + } + + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + nsresult rv = RemoveAlignment(aElement.AsDOMNode(), aAlignType, + aContentsOnly == ContentsOnly::yes); + NS_ENSURE_SUCCESS(rv, rv); + NS_NAMED_LITERAL_STRING(attr, "align"); + if (htmlEditor->IsCSSEnabled()) { + // Let's use CSS alignment; we use margin-left and margin-right for tables + // and text-align for other block-level elements + rv = htmlEditor->SetAttributeOrEquivalent( + static_cast<nsIDOMElement*>(aElement.AsDOMNode()), + attr, aAlignType, false); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // HTML case; this code is supposed to be called ONLY if the element + // supports the align attribute but we'll never know... + if (HTMLEditUtils::SupportsAlignAttr(aElement.AsDOMNode())) { + rv = htmlEditor->SetAttribute( + static_cast<nsIDOMElement*>(aElement.AsDOMNode()), + attr, aAlignType); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::ChangeIndentation(Element& aElement, + Change aChange) +{ + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + nsIAtom& marginProperty = + MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, aElement); + nsAutoString value; + htmlEditor->mCSSEditUtils->GetSpecifiedProperty(aElement, marginProperty, + value); + float f; + nsCOMPtr<nsIAtom> unit; + htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit)); + if (!f) { + nsAutoString defaultLengthUnit; + htmlEditor->mCSSEditUtils->GetDefaultLengthUnit(defaultLengthUnit); + unit = NS_Atomize(defaultLengthUnit); + } + int8_t multiplier = aChange == Change::plus ? +1 : -1; + if (nsGkAtoms::in == unit) { + f += NS_EDITOR_INDENT_INCREMENT_IN * multiplier; + } else if (nsGkAtoms::cm == unit) { + f += NS_EDITOR_INDENT_INCREMENT_CM * multiplier; + } else if (nsGkAtoms::mm == unit) { + f += NS_EDITOR_INDENT_INCREMENT_MM * multiplier; + } else if (nsGkAtoms::pt == unit) { + f += NS_EDITOR_INDENT_INCREMENT_PT * multiplier; + } else if (nsGkAtoms::pc == unit) { + f += NS_EDITOR_INDENT_INCREMENT_PC * multiplier; + } else if (nsGkAtoms::em == unit) { + f += NS_EDITOR_INDENT_INCREMENT_EM * multiplier; + } else if (nsGkAtoms::ex == unit) { + f += NS_EDITOR_INDENT_INCREMENT_EX * multiplier; + } else if (nsGkAtoms::px == unit) { + f += NS_EDITOR_INDENT_INCREMENT_PX * multiplier; + } else if (nsGkAtoms::percentage == unit) { + f += NS_EDITOR_INDENT_INCREMENT_PERCENT * multiplier; + } + + if (0 < f) { + nsAutoString newValue; + newValue.AppendFloat(f); + newValue.Append(nsDependentAtomString(unit)); + htmlEditor->mCSSEditUtils->SetCSSProperty(aElement, marginProperty, + newValue); + return NS_OK; + } + + htmlEditor->mCSSEditUtils->RemoveCSSProperty(aElement, marginProperty, + value); + + // Remove unnecessary divs + if (!aElement.IsHTMLElement(nsGkAtoms::div) || + &aElement == htmlEditor->GetActiveEditingHost() || + !htmlEditor->IsDescendantOfEditorRoot(&aElement) || + HTMLEditor::HasAttributes(&aElement)) { + return NS_OK; + } + + nsresult rv = htmlEditor->RemoveContainer(&aElement); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +HTMLEditRules::WillAbsolutePosition(Selection& aSelection, + bool* aCancel, + bool* aHandled) +{ + MOZ_ASSERT(aCancel && aHandled); + NS_ENSURE_STATE(mHTMLEditor); + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + + WillInsert(aSelection, aCancel); + + // We want to ignore result of WillInsert() + *aCancel = false; + *aHandled = true; + + nsCOMPtr<Element> focusElement = htmlEditor->GetSelectionContainer(); + if (focusElement && HTMLEditUtils::IsImage(focusElement)) { + mNewBlock = focusElement; + return NS_OK; + } + + nsresult rv = NormalizeSelection(&aSelection); + NS_ENSURE_SUCCESS(rv, rv); + AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor); + + // Convert the selection ranges into "promoted" selection ranges: this + // basically just expands the range to include the immediate block parent, + // and then further expands to include any ancestors whose children are all + // in the range. + + nsTArray<RefPtr<nsRange>> arrayOfRanges; + GetPromotedRanges(aSelection, arrayOfRanges, + EditAction::setAbsolutePosition); + + // Use these ranges to contruct a list of nodes to act on. + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, + EditAction::setAbsolutePosition); + NS_ENSURE_SUCCESS(rv, rv); + + // If nothing visible in list, make an empty block + if (ListIsEmptyLine(arrayOfNodes)) { + // Get selection location + NS_ENSURE_STATE(aSelection.GetRangeAt(0) && + aSelection.GetRangeAt(0)->GetStartParent()); + OwningNonNull<nsINode> parent = + *aSelection.GetRangeAt(0)->GetStartParent(); + int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); + + // Make sure we can put a block here + rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<Element> positionedDiv = + htmlEditor->CreateNode(nsGkAtoms::div, parent, offset); + NS_ENSURE_STATE(positionedDiv); + // Remember our new block for postprocessing + mNewBlock = positionedDiv; + // Delete anything that was in the list of nodes + while (!arrayOfNodes.IsEmpty()) { + OwningNonNull<nsINode> curNode = arrayOfNodes[0]; + rv = htmlEditor->DeleteNode(curNode); + NS_ENSURE_SUCCESS(rv, rv); + arrayOfNodes.RemoveElementAt(0); + } + // Put selection in new block + *aHandled = true; + rv = aSelection.Collapse(positionedDiv, 0); + // Don't restore the selection + selectionRestorer.Abort(); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + // Okay, now go through all the nodes and put them in a blockquote, or + // whatever is appropriate. Woohoo! + nsCOMPtr<Element> curList, curPositionedDiv, indentedLI; + for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) { + // Here's where we actually figure out what to do + NS_ENSURE_STATE(arrayOfNodes[i]->IsContent()); + OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent(); + + // Ignore all non-editable nodes. Leave them be. + if (!htmlEditor->IsEditable(curNode)) { + continue; + } + + nsCOMPtr<nsIContent> sibling; + + nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); + int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; + + // Some logic for putting list items into nested lists... + if (HTMLEditUtils::IsList(curParent)) { + // Check to see if curList is still appropriate. Which it is if curNode + // is still right after it in the same list. + if (curList) { + sibling = htmlEditor->GetPriorHTMLSibling(curNode); + } + + if (!curList || (sibling && sibling != curList)) { + // Create a new nested list of correct type + rv = + SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + if (!curPositionedDiv) { + nsCOMPtr<nsINode> curParentParent = curParent->GetParentNode(); + int32_t parentOffset = curParentParent + ? curParentParent->IndexOf(curParent) : -1; + curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParentParent, + parentOffset); + mNewBlock = curPositionedDiv; + } + curList = htmlEditor->CreateNode(curParent->NodeInfo()->NameAtom(), + curPositionedDiv, -1); + NS_ENSURE_STATE(curList); + // curList is now the correct thing to put curNode in. Remember our + // new block for postprocessing. + } + // Tuck the node into the end of the active list + rv = htmlEditor->MoveNode(curNode, curList, -1); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Not a list item, use blockquote? If we are inside a list item, we + // don't want to blockquote, we want to sublist the list item. We may + // have several nodes listed in the array of nodes to act on, that are in + // the same list item. Since we only want to indent that li once, we + // must keep track of the most recent indented list item, and not indent + // it if we find another node to act on that is still inside the same li. + nsCOMPtr<Element> listItem = IsInListItem(curNode); + if (listItem) { + if (indentedLI == listItem) { + // Already indented this list item + continue; + } + curParent = listItem->GetParentNode(); + offset = curParent ? curParent->IndexOf(listItem) : -1; + // Check to see if curList is still appropriate. Which it is if + // curNode is still right after it in the same list. + if (curList) { + sibling = htmlEditor->GetPriorHTMLSibling(curNode); + } + + if (!curList || (sibling && sibling != curList)) { + // Create a new nested list of correct type + rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, + offset); + NS_ENSURE_SUCCESS(rv, rv); + if (!curPositionedDiv) { + nsCOMPtr<nsINode> curParentParent = curParent->GetParentNode(); + int32_t parentOffset = curParentParent ? + curParentParent->IndexOf(curParent) : -1; + curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, + curParentParent, + parentOffset); + mNewBlock = curPositionedDiv; + } + curList = htmlEditor->CreateNode(curParent->NodeInfo()->NameAtom(), + curPositionedDiv, -1); + NS_ENSURE_STATE(curList); + } + rv = htmlEditor->MoveNode(listItem, curList, -1); + NS_ENSURE_SUCCESS(rv, rv); + // Remember we indented this li + indentedLI = listItem; + } else { + // Need to make a div to put things in if we haven't already + + if (!curPositionedDiv) { + if (curNode->IsHTMLElement(nsGkAtoms::div)) { + curPositionedDiv = curNode->AsElement(); + mNewBlock = curPositionedDiv; + curList = nullptr; + continue; + } + rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset); + NS_ENSURE_SUCCESS(rv, rv); + curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent, + offset); + NS_ENSURE_STATE(curPositionedDiv); + // Remember our new block for postprocessing + mNewBlock = curPositionedDiv; + // curPositionedDiv is now the correct thing to put curNode in + } + + // Tuck the node into the end of the active blockquote + rv = htmlEditor->MoveNode(curNode, curPositionedDiv, -1); + NS_ENSURE_SUCCESS(rv, rv); + // Forget curList, if any + curList = nullptr; + } + } + } + return NS_OK; +} + +nsresult +HTMLEditRules::DidAbsolutePosition() +{ + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor; + nsCOMPtr<nsIDOMElement> elt = + static_cast<nsIDOMElement*>(GetAsDOMNode(mNewBlock)); + return absPosHTMLEditor->AbsolutelyPositionElement(elt, true); +} + +nsresult +HTMLEditRules::WillRemoveAbsolutePosition(Selection* aSelection, + bool* aCancel, + bool* aHandled) { + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + WillInsert(*aSelection, aCancel); + + // initialize out param + // we want to ignore aCancel from WillInsert() + *aCancel = false; + *aHandled = true; + + nsCOMPtr<nsIDOMElement> elt; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_STATE(mHTMLEditor); + AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); + + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor; + return absPosHTMLEditor->AbsolutelyPositionElement(elt, false); +} + +nsresult +HTMLEditRules::WillRelativeChangeZIndex(Selection* aSelection, + int32_t aChange, + bool* aCancel, + bool* aHandled) +{ + if (!aSelection || !aCancel || !aHandled) { + return NS_ERROR_NULL_POINTER; + } + WillInsert(*aSelection, aCancel); + + // initialize out param + // we want to ignore aCancel from WillInsert() + *aCancel = false; + *aHandled = true; + + nsCOMPtr<nsIDOMElement> elt; + NS_ENSURE_STATE(mHTMLEditor); + nsresult rv = + mHTMLEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_STATE(mHTMLEditor); + AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); + + NS_ENSURE_STATE(mHTMLEditor); + nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor; + int32_t zIndex; + return absPosHTMLEditor->RelativeChangeElementZIndex(elt, aChange, &zIndex); +} + +NS_IMETHODIMP +HTMLEditRules::DocumentModified() +{ + nsContentUtils::AddScriptRunner( + NewRunnableMethod(this, &HTMLEditRules::DocumentModifiedWorker)); + return NS_OK; +} + +void +HTMLEditRules::DocumentModifiedWorker() +{ + if (!mHTMLEditor) { + return; + } + + // DeleteNode below may cause a flush, which could destroy the editor + nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker; + + RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); + RefPtr<Selection> selection = htmlEditor->GetSelection(); + if (!selection) { + return; + } + + // Delete our bogus node, if we have one, since the document might not be + // empty any more. + if (mBogusNode) { + mTextEditor->DeleteNode(mBogusNode); + mBogusNode = nullptr; + } + + // Try to recreate the bogus node if needed. + CreateBogusNodeIfNeeded(selection); +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLEditRules.h b/editor/libeditor/HTMLEditRules.h new file mode 100644 index 000000000..40c5e2afd --- /dev/null +++ b/editor/libeditor/HTMLEditRules.h @@ -0,0 +1,377 @@ +/* -*- 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/. */ + +#ifndef HTMLEditRules_h +#define HTMLEditRules_h + +#include "TypeInState.h" +#include "mozilla/SelectionState.h" +#include "mozilla/TextEditRules.h" +#include "nsCOMPtr.h" +#include "nsIEditActionListener.h" +#include "nsIEditor.h" +#include "nsIHTMLEditor.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" +#include "nscore.h" + +class nsIAtom; +class nsIDOMCharacterData; +class nsIDOMDocument; +class nsIDOMElement; +class nsIDOMNode; +class nsIEditor; +class nsINode; +class nsRange; + +namespace mozilla { + +class HTMLEditor; +class RulesInfo; +class TextEditor; +struct EditorDOMPoint; +namespace dom { +class Element; +class Selection; +} // namespace dom + +struct StyleCache final : public PropItem +{ + bool mPresent; + + StyleCache() + : PropItem() + , mPresent(false) + { + MOZ_COUNT_CTOR(StyleCache); + } + + StyleCache(nsIAtom* aTag, + const nsAString& aAttr, + const nsAString& aValue) + : PropItem(aTag, aAttr, aValue) + , mPresent(false) + { + MOZ_COUNT_CTOR(StyleCache); + } + + ~StyleCache() + { + MOZ_COUNT_DTOR(StyleCache); + } +}; + +#define SIZE_STYLE_TABLE 19 + +class HTMLEditRules : public TextEditRules + , public nsIEditActionListener +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLEditRules, TextEditRules) + + HTMLEditRules(); + + // nsIEditRules methods + NS_IMETHOD Init(TextEditor* aTextEditor) override; + NS_IMETHOD DetachEditor() override; + NS_IMETHOD BeforeEdit(EditAction action, + nsIEditor::EDirection aDirection) override; + NS_IMETHOD AfterEdit(EditAction action, + nsIEditor::EDirection aDirection) override; + NS_IMETHOD WillDoAction(Selection* aSelection, RulesInfo* aInfo, + bool* aCancel, bool* aHandled) override; + NS_IMETHOD DidDoAction(Selection* aSelection, RulesInfo* aInfo, + nsresult aResult) override; + NS_IMETHOD DocumentModified() override; + + nsresult GetListState(bool* aMixed, bool* aOL, bool* aUL, bool* aDL); + nsresult GetListItemState(bool* aMixed, bool* aLI, bool* aDT, bool* aDD); + nsresult GetIndentState(bool* aCanIndent, bool* aCanOutdent); + nsresult GetAlignment(bool* aMixed, nsIHTMLEditor::EAlignment* aAlign); + nsresult GetParagraphState(bool* aMixed, nsAString& outFormat); + nsresult MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode); + + // nsIEditActionListener methods + + NS_IMETHOD WillCreateNode(const nsAString& aTag, nsIDOMNode* aParent, + int32_t aPosition) override; + NS_IMETHOD DidCreateNode(const nsAString& aTag, nsIDOMNode* aNode, + nsIDOMNode* aParent, int32_t aPosition, + nsresult aResult) override; + NS_IMETHOD WillInsertNode(nsIDOMNode* aNode, nsIDOMNode* aParent, + int32_t aPosition) override; + NS_IMETHOD DidInsertNode(nsIDOMNode* aNode, nsIDOMNode* aParent, + int32_t aPosition, nsresult aResult) override; + NS_IMETHOD WillDeleteNode(nsIDOMNode* aChild) override; + NS_IMETHOD DidDeleteNode(nsIDOMNode* aChild, nsresult aResult) override; + NS_IMETHOD WillSplitNode(nsIDOMNode* aExistingRightNode, + int32_t aOffset) override; + NS_IMETHOD DidSplitNode(nsIDOMNode* aExistingRightNode, int32_t aOffset, + nsIDOMNode* aNewLeftNode, nsresult aResult) override; + NS_IMETHOD WillJoinNodes(nsIDOMNode* aLeftNode, nsIDOMNode* aRightNode, + nsIDOMNode* aParent) override; + NS_IMETHOD DidJoinNodes(nsIDOMNode* aLeftNode, nsIDOMNode* aRightNode, + nsIDOMNode* aParent, nsresult aResult) override; + NS_IMETHOD WillInsertText(nsIDOMCharacterData* aTextNode, int32_t aOffset, + const nsAString &aString) override; + NS_IMETHOD DidInsertText(nsIDOMCharacterData* aTextNode, int32_t aOffset, + const nsAString &aString, nsresult aResult) override; + NS_IMETHOD WillDeleteText(nsIDOMCharacterData* aTextNode, int32_t aOffset, + int32_t aLength) override; + NS_IMETHOD DidDeleteText(nsIDOMCharacterData* aTextNode, int32_t aOffset, + int32_t aLength, nsresult aResult) override; + NS_IMETHOD WillDeleteSelection(nsISelection* aSelection) override; + NS_IMETHOD DidDeleteSelection(nsISelection* aSelection) override; + void DeleteNodeIfCollapsedText(nsINode& aNode); + +protected: + virtual ~HTMLEditRules(); + + enum RulesEndpoint + { + kStart, + kEnd + }; + + void InitFields(); + + void WillInsert(Selection& aSelection, bool* aCancel); + nsresult WillInsertText(EditAction aAction, + Selection* aSelection, + bool* aCancel, + bool* aHandled, + const nsAString* inString, + nsAString* outString, + int32_t aMaxLength); + nsresult WillLoadHTML(Selection* aSelection, bool* aCancel); + nsresult WillInsertBreak(Selection& aSelection, bool* aCancel, + bool* aHandled); + nsresult StandardBreakImpl(nsINode& aNode, int32_t aOffset, + Selection& aSelection); + nsresult DidInsertBreak(Selection* aSelection, nsresult aResult); + nsresult SplitMailCites(Selection* aSelection, bool* aHandled); + nsresult WillDeleteSelection(Selection* aSelection, + nsIEditor::EDirection aAction, + nsIEditor::EStripWrappers aStripWrappers, + bool* aCancel, bool* aHandled); + nsresult DidDeleteSelection(Selection* aSelection, + nsIEditor::EDirection aDir, + nsresult aResult); + nsresult InsertBRIfNeeded(Selection* aSelection); + mozilla::EditorDOMPoint GetGoodSelPointForNode(nsINode& aNode, + nsIEditor::EDirection aAction); + nsresult JoinBlocks(nsIContent& aLeftNode, nsIContent& aRightNode, + bool* aCanceled); + nsresult MoveBlock(Element& aLeftBlock, Element& aRightBlock, + int32_t aLeftOffset, int32_t aRightOffset); + nsresult MoveNodeSmart(nsIContent& aNode, Element& aDestElement, + int32_t* aOffset); + nsresult MoveContents(Element& aElement, Element& aDestElement, + int32_t* aOffset); + nsresult DeleteNonTableElements(nsINode* aNode); + nsresult WillMakeList(Selection* aSelection, + const nsAString* aListType, + bool aEntireList, + const nsAString* aBulletType, + bool* aCancel, bool* aHandled, + const nsAString* aItemType = nullptr); + nsresult WillRemoveList(Selection* aSelection, bool aOrdered, bool* aCancel, + bool* aHandled); + nsresult WillIndent(Selection* aSelection, bool* aCancel, bool* aHandled); + nsresult WillCSSIndent(Selection* aSelection, bool* aCancel, bool* aHandled); + nsresult WillHTMLIndent(Selection* aSelection, bool* aCancel, + bool* aHandled); + nsresult WillOutdent(Selection& aSelection, bool* aCancel, bool* aHandled); + nsresult WillAlign(Selection& aSelection, const nsAString& aAlignType, + bool* aCancel, bool* aHandled); + nsresult WillAbsolutePosition(Selection& aSelection, bool* aCancel, + bool* aHandled); + nsresult WillRemoveAbsolutePosition(Selection* aSelection, bool* aCancel, + bool* aHandled); + nsresult WillRelativeChangeZIndex(Selection* aSelection, int32_t aChange, + bool* aCancel, bool* aHandled); + nsresult WillMakeDefListItem(Selection* aSelection, + const nsAString* aBlockType, bool aEntireList, + bool* aCancel, bool* aHandled); + nsresult WillMakeBasicBlock(Selection& aSelection, + const nsAString& aBlockType, + bool* aCancel, bool* aHandled); + nsresult DidMakeBasicBlock(Selection* aSelection, RulesInfo* aInfo, + nsresult aResult); + nsresult DidAbsolutePosition(); + nsresult AlignInnerBlocks(nsINode& aNode, const nsAString* alignType); + nsresult AlignBlockContents(nsIDOMNode* aNode, const nsAString* alignType); + nsresult AppendInnerFormatNodes(nsTArray<OwningNonNull<nsINode>>& aArray, + nsINode* aNode); + nsresult GetFormatString(nsIDOMNode* aNode, nsAString &outFormat); + enum class Lists { no, yes }; + enum class Tables { no, yes }; + void GetInnerContent(nsINode& aNode, + nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, + int32_t* aIndex, Lists aLists = Lists::yes, + Tables aTables = Tables::yes); + Element* IsInListItem(nsINode* aNode); + nsresult ReturnInHeader(Selection& aSelection, Element& aHeader, + nsINode& aNode, int32_t aOffset); + nsresult ReturnInParagraph(Selection* aSelection, nsIDOMNode* aHeader, + nsIDOMNode* aTextNode, int32_t aOffset, + bool* aCancel, bool* aHandled); + nsresult SplitParagraph(nsIDOMNode* aPara, + nsIContent* aBRNode, + Selection* aSelection, + nsCOMPtr<nsIDOMNode>* aSelNode, + int32_t* aOffset); + nsresult ReturnInListItem(Selection& aSelection, Element& aHeader, + nsINode& aNode, int32_t aOffset); + nsresult AfterEditInner(EditAction action, + nsIEditor::EDirection aDirection); + nsresult RemovePartOfBlock(Element& aBlock, nsIContent& aStartChild, + nsIContent& aEndChild); + void SplitBlock(Element& aBlock, + nsIContent& aStartChild, + nsIContent& aEndChild, + nsIContent** aOutLeftNode = nullptr, + nsIContent** aOutRightNode = nullptr, + nsIContent** aOutMiddleNode = nullptr); + nsresult OutdentPartOfBlock(Element& aBlock, + nsIContent& aStartChild, + nsIContent& aEndChild, + bool aIsBlockIndentedWithCSS, + nsIContent** aOutLeftNode, + nsIContent** aOutRightNode); + + nsresult ConvertListType(Element* aList, Element** aOutList, + nsIAtom* aListType, nsIAtom* aItemType); + + nsresult CreateStyleForInsertText(Selection& aSelection, nsIDocument& aDoc); + enum class MozBRCounts { yes, no }; + nsresult IsEmptyBlock(Element& aNode, bool* aOutIsEmptyBlock, + MozBRCounts aMozBRCounts = MozBRCounts::yes); + nsresult CheckForEmptyBlock(nsINode* aStartNode, Element* aBodyNode, + Selection* aSelection, + nsIEditor::EDirection aAction, bool* aHandled); + enum class BRLocation { beforeBlock, blockEnd }; + Element* CheckForInvisibleBR(Element& aBlock, BRLocation aWhere, + int32_t aOffset = 0); + nsresult ExpandSelectionForDeletion(Selection& aSelection); + bool IsFirstNode(nsIDOMNode* aNode); + bool IsLastNode(nsIDOMNode* aNode); + nsresult NormalizeSelection(Selection* aSelection); + void GetPromotedPoint(RulesEndpoint aWhere, nsIDOMNode* aNode, + int32_t aOffset, EditAction actionID, + nsCOMPtr<nsIDOMNode>* outNode, int32_t* outOffset); + void GetPromotedRanges(Selection& aSelection, + nsTArray<RefPtr<nsRange>>& outArrayOfRanges, + EditAction inOperationType); + void PromoteRange(nsRange& aRange, EditAction inOperationType); + enum class TouchContent { no, yes }; + nsresult GetNodesForOperation( + nsTArray<RefPtr<nsRange>>& aArrayOfRanges, + nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, + EditAction aOperationType, + TouchContent aTouchContent = TouchContent::yes); + void GetChildNodesForOperation( + nsINode& aNode, + nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes); + nsresult GetNodesFromPoint(EditorDOMPoint aPoint, + EditAction aOperation, + nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, + TouchContent aTouchContent); + nsresult GetNodesFromSelection( + Selection& aSelection, + EditAction aOperation, + nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, + TouchContent aTouchContent = TouchContent::yes); + enum class EntireList { no, yes }; + nsresult GetListActionNodes( + nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, + EntireList aEntireList, + TouchContent aTouchContent = TouchContent::yes); + void GetDefinitionListItemTypes(Element* aElement, bool* aDT, bool* aDD); + nsresult GetParagraphFormatNodes( + nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, + TouchContent aTouchContent = TouchContent::yes); + void LookInsideDivBQandList(nsTArray<OwningNonNull<nsINode>>& aNodeArray); + nsresult BustUpInlinesAtRangeEndpoints(RangeItem& inRange); + nsresult BustUpInlinesAtBRs( + nsIContent& aNode, + nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes); + nsIContent* GetHighestInlineParent(nsINode& aNode); + void MakeTransitionList(nsTArray<OwningNonNull<nsINode>>& aNodeArray, + nsTArray<bool>& aTransitionArray); + nsresult RemoveBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray); + nsresult ApplyBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray, + nsIAtom& aBlockTag); + nsresult MakeBlockquote(nsTArray<OwningNonNull<nsINode>>& aNodeArray); + nsresult SplitAsNeeded(nsIAtom& aTag, OwningNonNull<nsINode>& inOutParent, + int32_t& inOutOffset); + nsresult SplitAsNeeded(nsIAtom& aTag, nsCOMPtr<nsINode>& inOutParent, + int32_t& inOutOffset); + nsresult AddTerminatingBR(nsIDOMNode *aBlock); + EditorDOMPoint JoinNodesSmart(nsIContent& aNodeLeft, + nsIContent& aNodeRight); + Element* GetTopEnclosingMailCite(nsINode& aNode); + nsresult PopListItem(nsIDOMNode* aListItem, bool* aOutOfList); + nsresult RemoveListStructure(Element& aList); + nsresult CacheInlineStyles(nsIDOMNode* aNode); + nsresult ReapplyCachedStyles(); + void ClearCachedStyles(); + void AdjustSpecialBreaks(); + nsresult AdjustWhitespace(Selection* aSelection); + nsresult PinSelectionToNewBlock(Selection* aSelection); + void CheckInterlinePosition(Selection& aSelection); + nsresult AdjustSelection(Selection* aSelection, + nsIEditor::EDirection aAction); + nsresult FindNearSelectableNode(nsIDOMNode* aSelNode, + int32_t aSelOffset, + nsIEditor::EDirection& aDirection, + nsCOMPtr<nsIDOMNode>* outSelectableNode); + /** + * Returns true if aNode1 or aNode2 or both is the descendant of some type of + * table element, but their nearest table element ancestors differ. "Table + * element" here includes not just <table> but also <td>, <tbody>, <tr>, etc. + * The nodes count as being their own descendants for this purpose, so a + * table element is its own nearest table element ancestor. + */ + bool InDifferentTableElements(nsIDOMNode* aNode1, nsIDOMNode* aNode2); + bool InDifferentTableElements(nsINode* aNode1, nsINode* aNode2); + nsresult RemoveEmptyNodes(); + nsresult SelectionEndpointInNode(nsINode* aNode, bool* aResult); + nsresult UpdateDocChangeRange(nsRange* aRange); + nsresult ConfirmSelectionInBody(); + nsresult InsertMozBRIfNeeded(nsINode& aNode); + bool IsEmptyInline(nsINode& aNode); + bool ListIsEmptyLine(nsTArray<OwningNonNull<nsINode>>& arrayOfNodes); + nsresult RemoveAlignment(nsIDOMNode* aNode, const nsAString& aAlignType, + bool aChildrenOnly); + nsresult MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode, bool aStarts); + enum class ContentsOnly { no, yes }; + nsresult AlignBlock(Element& aElement, + const nsAString& aAlignType, ContentsOnly aContentsOnly); + enum class Change { minus, plus }; + nsresult ChangeIndentation(Element& aElement, Change aChange); + void DocumentModifiedWorker(); + +protected: + HTMLEditor* mHTMLEditor; + RefPtr<nsRange> mDocChangeRange; + bool mListenerEnabled; + bool mReturnInEmptyLIKillsList; + bool mDidDeleteSelection; + bool mDidRangedDelete; + bool mRestoreContentEditableCount; + RefPtr<nsRange> mUtilRange; + // Need to remember an int across willJoin/didJoin... + uint32_t mJoinOffset; + nsCOMPtr<Element> mNewBlock; + RefPtr<RangeItem> mRangeItem; + StyleCache mCachedStyles[SIZE_STYLE_TABLE]; +}; + +} // namespace mozilla + +#endif // #ifndef HTMLEditRules_h + diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp new file mode 100644 index 000000000..a701c06ec --- /dev/null +++ b/editor/libeditor/HTMLEditUtils.cpp @@ -0,0 +1,842 @@ +/* -*- 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 "HTMLEditUtils.h" + +#include "TextEditUtils.h" // for TextEditUtils +#include "mozilla/ArrayUtils.h" // for ArrayLength +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc. +#include "mozilla/EditorBase.h" // for EditorBase +#include "mozilla/dom/Element.h" // for Element, nsINode +#include "nsAString.h" // for nsAString_internal::IsEmpty +#include "nsCOMPtr.h" // for nsCOMPtr, operator==, etc. +#include "nsCaseTreatment.h" +#include "nsDebug.h" // for NS_PRECONDITION, etc. +#include "nsError.h" // for NS_SUCCEEDED +#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::a, etc. +#include "nsHTMLTags.h" +#include "nsIAtom.h" // for nsIAtom +#include "nsIDOMHTMLAnchorElement.h" // for nsIDOMHTMLAnchorElement +#include "nsIDOMNode.h" // for nsIDOMNode +#include "nsNameSpaceManager.h" // for kNameSpaceID_None +#include "nsLiteralString.h" // for NS_LITERAL_STRING +#include "nsString.h" // for nsAutoString + +namespace mozilla { + +/** + * IsInlineStyle() returns true if aNode is an inline style. + */ +bool +HTMLEditUtils::IsInlineStyle(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsInlineStyle(node); +} + +bool +HTMLEditUtils::IsInlineStyle(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsAnyOfHTMLElements(nsGkAtoms::b, + nsGkAtoms::i, + nsGkAtoms::u, + nsGkAtoms::tt, + nsGkAtoms::s, + nsGkAtoms::strike, + nsGkAtoms::big, + nsGkAtoms::small, + nsGkAtoms::sub, + nsGkAtoms::sup, + nsGkAtoms::font); +} + +/** + * IsFormatNode() returns true if aNode is a format node. + */ +bool +HTMLEditUtils::IsFormatNode(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsFormatNode(node); +} + +bool +HTMLEditUtils::IsFormatNode(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsAnyOfHTMLElements(nsGkAtoms::p, + nsGkAtoms::pre, + nsGkAtoms::h1, + nsGkAtoms::h2, + nsGkAtoms::h3, + nsGkAtoms::h4, + nsGkAtoms::h5, + nsGkAtoms::h6, + nsGkAtoms::address); +} + +/** + * IsNodeThatCanOutdent() returns true if aNode is a list, list item or + * blockquote. + */ +bool +HTMLEditUtils::IsNodeThatCanOutdent(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsIAtom> nodeAtom = EditorBase::GetTag(aNode); + return (nodeAtom == nsGkAtoms::ul) + || (nodeAtom == nsGkAtoms::ol) + || (nodeAtom == nsGkAtoms::dl) + || (nodeAtom == nsGkAtoms::li) + || (nodeAtom == nsGkAtoms::dd) + || (nodeAtom == nsGkAtoms::dt) + || (nodeAtom == nsGkAtoms::blockquote); +} + +/** + * IsHeader() returns true if aNode is an html header. + */ +bool +HTMLEditUtils::IsHeader(nsINode& aNode) +{ + return aNode.IsAnyOfHTMLElements(nsGkAtoms::h1, + nsGkAtoms::h2, + nsGkAtoms::h3, + nsGkAtoms::h4, + nsGkAtoms::h5, + nsGkAtoms::h6); +} + +bool +HTMLEditUtils::IsHeader(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + MOZ_ASSERT(node); + return IsHeader(*node); +} + +/** + * IsParagraph() returns true if aNode is an html paragraph. + */ +bool +HTMLEditUtils::IsParagraph(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::p); +} + +/** + * IsHR() returns true if aNode is an horizontal rule. + */ +bool +HTMLEditUtils::IsHR(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::hr); +} + +/** + * IsListItem() returns true if aNode is an html list item. + */ +bool +HTMLEditUtils::IsListItem(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsListItem(node); +} + +bool +HTMLEditUtils::IsListItem(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsAnyOfHTMLElements(nsGkAtoms::li, + nsGkAtoms::dd, + nsGkAtoms::dt); +} + +/** + * IsTableElement() returns true if aNode is an html table, td, tr, ... + */ +bool +HTMLEditUtils::IsTableElement(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsTableElement(node); +} + +bool +HTMLEditUtils::IsTableElement(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsAnyOfHTMLElements(nsGkAtoms::table, + nsGkAtoms::tr, + nsGkAtoms::td, + nsGkAtoms::th, + nsGkAtoms::thead, + nsGkAtoms::tfoot, + nsGkAtoms::tbody, + nsGkAtoms::caption); +} + +/** + * IsTableElementButNotTable() returns true if aNode is an html td, tr, ... + * (doesn't include table) + */ +bool +HTMLEditUtils::IsTableElementButNotTable(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsTableElementButNotTable(node); +} + +bool +HTMLEditUtils::IsTableElementButNotTable(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsAnyOfHTMLElements(nsGkAtoms::tr, + nsGkAtoms::td, + nsGkAtoms::th, + nsGkAtoms::thead, + nsGkAtoms::tfoot, + nsGkAtoms::tbody, + nsGkAtoms::caption); +} + +/** + * IsTable() returns true if aNode is an html table. + */ +bool +HTMLEditUtils::IsTable(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::table); +} + +bool +HTMLEditUtils::IsTable(nsINode* aNode) +{ + return aNode && aNode->IsHTMLElement(nsGkAtoms::table); +} + +/** + * IsTableRow() returns true if aNode is an html tr. + */ +bool +HTMLEditUtils::IsTableRow(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::tr); +} + +/** + * IsTableCell() returns true if aNode is an html td or th. + */ +bool +HTMLEditUtils::IsTableCell(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsTableCell(node); +} + +bool +HTMLEditUtils::IsTableCell(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th); +} + +/** + * IsTableCellOrCaption() returns true if aNode is an html td or th or caption. + */ +bool +HTMLEditUtils::IsTableCellOrCaption(nsINode& aNode) +{ + return aNode.IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th, + nsGkAtoms::caption); +} + +/** + * IsList() returns true if aNode is an html list. + */ +bool +HTMLEditUtils::IsList(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsList(node); +} + +bool +HTMLEditUtils::IsList(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsAnyOfHTMLElements(nsGkAtoms::ul, + nsGkAtoms::ol, + nsGkAtoms::dl); +} + +/** + * IsOrderedList() returns true if aNode is an html ordered list. + */ +bool +HTMLEditUtils::IsOrderedList(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::ol); +} + + +/** + * IsUnorderedList() returns true if aNode is an html unordered list. + */ +bool +HTMLEditUtils::IsUnorderedList(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::ul); +} + +/** + * IsBlockquote() returns true if aNode is an html blockquote node. + */ +bool +HTMLEditUtils::IsBlockquote(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::blockquote); +} + +/** + * IsPre() returns true if aNode is an html pre node. + */ +bool +HTMLEditUtils::IsPre(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::pre); +} + +/** + * IsImage() returns true if aNode is an html image node. + */ +bool +HTMLEditUtils::IsImage(nsINode* aNode) +{ + return aNode && aNode->IsHTMLElement(nsGkAtoms::img); +} + +bool +HTMLEditUtils::IsImage(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::img); +} + +bool +HTMLEditUtils::IsLink(nsIDOMNode *aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsLink(node); +} + +bool +HTMLEditUtils::IsLink(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + nsCOMPtr<nsIDOMHTMLAnchorElement> anchor = do_QueryInterface(aNode); + if (anchor) { + nsAutoString tmpText; + if (NS_SUCCEEDED(anchor->GetHref(tmpText)) && !tmpText.IsEmpty()) { + return true; + } + } + return false; +} + +bool +HTMLEditUtils::IsNamedAnchor(nsIDOMNode *aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsNamedAnchor(node); +} + +bool +HTMLEditUtils::IsNamedAnchor(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + if (!aNode->IsHTMLElement(nsGkAtoms::a)) { + return false; + } + + nsAutoString text; + return aNode->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name, + text) && !text.IsEmpty(); +} + +/** + * IsDiv() returns true if aNode is an html div node. + */ +bool +HTMLEditUtils::IsDiv(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::div); +} + +/** + * IsMozDiv() returns true if aNode is an html div node with |type = _moz|. + */ +bool +HTMLEditUtils::IsMozDiv(nsIDOMNode* aNode) +{ + return IsDiv(aNode) && TextEditUtils::HasMozAttr(aNode); +} + +bool +HTMLEditUtils::IsMozDiv(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsHTMLElement(nsGkAtoms::div) && + TextEditUtils::HasMozAttr(GetAsDOMNode(aNode)); +} + +/** + * IsMailCite() returns true if aNode is an html blockquote with |type=cite|. + */ +bool +HTMLEditUtils::IsMailCite(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsMailCite(node); +} + +bool +HTMLEditUtils::IsMailCite(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + // don't ask me why, but our html mailcites are id'd by "type=cite"... + if (aNode->IsElement() && + aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("cite"), + eIgnoreCase)) { + return true; + } + + // ... but our plaintext mailcites by "_moz_quote=true". go figure. + if (aNode->IsElement() && + aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozquote, + NS_LITERAL_STRING("true"), + eIgnoreCase)) { + return true; + } + + return false; +} + +/** + * IsFormWidget() returns true if aNode is a form widget of some kind. + */ +bool +HTMLEditUtils::IsFormWidget(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return node && IsFormWidget(node); +} + +bool +HTMLEditUtils::IsFormWidget(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsAnyOfHTMLElements(nsGkAtoms::textarea, + nsGkAtoms::select, + nsGkAtoms::button, + nsGkAtoms::output, + nsGkAtoms::keygen, + nsGkAtoms::progress, + nsGkAtoms::meter, + nsGkAtoms::input); +} + +bool +HTMLEditUtils::SupportsAlignAttr(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsIAtom> nodeAtom = EditorBase::GetTag(aNode); + return (nodeAtom == nsGkAtoms::hr) + || (nodeAtom == nsGkAtoms::table) + || (nodeAtom == nsGkAtoms::tbody) + || (nodeAtom == nsGkAtoms::tfoot) + || (nodeAtom == nsGkAtoms::thead) + || (nodeAtom == nsGkAtoms::tr) + || (nodeAtom == nsGkAtoms::td) + || (nodeAtom == nsGkAtoms::th) + || (nodeAtom == nsGkAtoms::div) + || (nodeAtom == nsGkAtoms::p) + || (nodeAtom == nsGkAtoms::h1) + || (nodeAtom == nsGkAtoms::h2) + || (nodeAtom == nsGkAtoms::h3) + || (nodeAtom == nsGkAtoms::h4) + || (nodeAtom == nsGkAtoms::h5) + || (nodeAtom == nsGkAtoms::h6); +} + +// We use bitmasks to test containment of elements. Elements are marked to be +// in certain groups by setting the mGroup member of the nsElementInfo struct +// to the corresponding GROUP_ values (OR'ed together). Similarly, elements are +// marked to allow containment of certain groups by setting the +// mCanContainGroups member of the nsElementInfo struct to the corresponding +// GROUP_ values (OR'ed together). +// Testing containment then simply consists of checking whether the +// mCanContainGroups bitmask of an element and the mGroup bitmask of a +// potential child overlap. + +#define GROUP_NONE 0 + +// body, head, html +#define GROUP_TOPLEVEL (1 << 1) + +// base, link, meta, script, style, title +#define GROUP_HEAD_CONTENT (1 << 2) + +// b, big, i, s, small, strike, tt, u +#define GROUP_FONTSTYLE (1 << 3) + +// abbr, acronym, cite, code, datalist, del, dfn, em, ins, kbd, mark, rb, rp +// rt, rtc, ruby, samp, strong, var +#define GROUP_PHRASE (1 << 4) + +// a, applet, basefont, bdo, br, font, iframe, img, map, meter, object, output, +// picture, progress, q, script, span, sub, sup +#define GROUP_SPECIAL (1 << 5) + +// button, form, input, label, select, textarea +#define GROUP_FORMCONTROL (1 << 6) + +// address, applet, article, aside, blockquote, button, center, del, details, +// dir, div, dl, fieldset, figure, footer, form, h1, h2, h3, h4, h5, h6, header, +// hgroup, hr, iframe, ins, main, map, menu, nav, noframes, noscript, object, +// ol, p, pre, table, section, summary, ul +#define GROUP_BLOCK (1 << 7) + +// frame, frameset +#define GROUP_FRAME (1 << 8) + +// col, tbody +#define GROUP_TABLE_CONTENT (1 << 9) + +// tr +#define GROUP_TBODY_CONTENT (1 << 10) + +// td, th +#define GROUP_TR_CONTENT (1 << 11) + +// col +#define GROUP_COLGROUP_CONTENT (1 << 12) + +// param +#define GROUP_OBJECT_CONTENT (1 << 13) + +// li +#define GROUP_LI (1 << 14) + +// area +#define GROUP_MAP_CONTENT (1 << 15) + +// optgroup, option +#define GROUP_SELECT_CONTENT (1 << 16) + +// option +#define GROUP_OPTIONS (1 << 17) + +// dd, dt +#define GROUP_DL_CONTENT (1 << 18) + +// p +#define GROUP_P (1 << 19) + +// text, whitespace, newline, comment +#define GROUP_LEAF (1 << 20) + +// XXX This is because the editor does sublists illegally. +// ol, ul +#define GROUP_OL_UL (1 << 21) + +// h1, h2, h3, h4, h5, h6 +#define GROUP_HEADING (1 << 22) + +// figcaption +#define GROUP_FIGCAPTION (1 << 23) + +// picture members (img, source) +#define GROUP_PICTURE_CONTENT (1 << 24) + +#define GROUP_INLINE_ELEMENT \ + (GROUP_FONTSTYLE | GROUP_PHRASE | GROUP_SPECIAL | GROUP_FORMCONTROL | \ + GROUP_LEAF) + +#define GROUP_FLOW_ELEMENT (GROUP_INLINE_ELEMENT | GROUP_BLOCK) + +struct ElementInfo final +{ +#ifdef DEBUG + eHTMLTags mTag; +#endif + uint32_t mGroup; + uint32_t mCanContainGroups; + bool mIsContainer; + bool mCanContainSelf; +}; + +#ifdef DEBUG +#define ELEM(_tag, _isContainer, _canContainSelf, _group, _canContainGroups) \ + { eHTMLTag_##_tag, _group, _canContainGroups, _isContainer, _canContainSelf } +#else +#define ELEM(_tag, _isContainer, _canContainSelf, _group, _canContainGroups) \ + { _group, _canContainGroups, _isContainer, _canContainSelf } +#endif + +static const ElementInfo kElements[eHTMLTag_userdefined] = { + ELEM(a, true, false, GROUP_SPECIAL, GROUP_INLINE_ELEMENT), + ELEM(abbr, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(acronym, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(address, true, true, GROUP_BLOCK, + GROUP_INLINE_ELEMENT | GROUP_P), + ELEM(applet, true, true, GROUP_SPECIAL | GROUP_BLOCK, + GROUP_FLOW_ELEMENT | GROUP_OBJECT_CONTENT), + ELEM(area, false, false, GROUP_MAP_CONTENT, GROUP_NONE), + ELEM(article, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(aside, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(audio, false, false, GROUP_NONE, GROUP_NONE), + ELEM(b, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT), + ELEM(base, false, false, GROUP_HEAD_CONTENT, GROUP_NONE), + ELEM(basefont, false, false, GROUP_SPECIAL, GROUP_NONE), + ELEM(bdo, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT), + ELEM(bgsound, false, false, GROUP_NONE, GROUP_NONE), + ELEM(big, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT), + ELEM(blockquote, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(body, true, true, GROUP_TOPLEVEL, GROUP_FLOW_ELEMENT), + ELEM(br, false, false, GROUP_SPECIAL, GROUP_NONE), + ELEM(button, true, true, GROUP_FORMCONTROL | GROUP_BLOCK, + GROUP_FLOW_ELEMENT), + ELEM(canvas, false, false, GROUP_NONE, GROUP_NONE), + ELEM(caption, true, true, GROUP_NONE, GROUP_INLINE_ELEMENT), + ELEM(center, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(cite, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(code, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(col, false, false, GROUP_TABLE_CONTENT | GROUP_COLGROUP_CONTENT, + GROUP_NONE), + ELEM(colgroup, true, false, GROUP_NONE, GROUP_COLGROUP_CONTENT), + ELEM(content, true, false, GROUP_NONE, GROUP_INLINE_ELEMENT), + ELEM(data, true, false, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(datalist, true, false, GROUP_PHRASE, + GROUP_OPTIONS | GROUP_INLINE_ELEMENT), + ELEM(dd, true, false, GROUP_DL_CONTENT, GROUP_FLOW_ELEMENT), + ELEM(del, true, true, GROUP_PHRASE | GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(details, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(dfn, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(dir, true, false, GROUP_BLOCK, GROUP_LI), + ELEM(div, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(dl, true, false, GROUP_BLOCK, GROUP_DL_CONTENT), + ELEM(dt, true, true, GROUP_DL_CONTENT, GROUP_INLINE_ELEMENT), + ELEM(em, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(embed, false, false, GROUP_NONE, GROUP_NONE), + ELEM(fieldset, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(figcaption, true, false, GROUP_FIGCAPTION, GROUP_FLOW_ELEMENT), + ELEM(figure, true, true, GROUP_BLOCK, + GROUP_FLOW_ELEMENT | GROUP_FIGCAPTION), + ELEM(font, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT), + ELEM(footer, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(form, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(frame, false, false, GROUP_FRAME, GROUP_NONE), + ELEM(frameset, true, true, GROUP_FRAME, GROUP_FRAME), + ELEM(h1, true, false, GROUP_BLOCK | GROUP_HEADING, + GROUP_INLINE_ELEMENT), + ELEM(h2, true, false, GROUP_BLOCK | GROUP_HEADING, + GROUP_INLINE_ELEMENT), + ELEM(h3, true, false, GROUP_BLOCK | GROUP_HEADING, + GROUP_INLINE_ELEMENT), + ELEM(h4, true, false, GROUP_BLOCK | GROUP_HEADING, + GROUP_INLINE_ELEMENT), + ELEM(h5, true, false, GROUP_BLOCK | GROUP_HEADING, + GROUP_INLINE_ELEMENT), + ELEM(h6, true, false, GROUP_BLOCK | GROUP_HEADING, + GROUP_INLINE_ELEMENT), + ELEM(head, true, false, GROUP_TOPLEVEL, GROUP_HEAD_CONTENT), + ELEM(header, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(hgroup, true, false, GROUP_BLOCK, GROUP_HEADING), + ELEM(hr, false, false, GROUP_BLOCK, GROUP_NONE), + ELEM(html, true, false, GROUP_TOPLEVEL, GROUP_TOPLEVEL), + ELEM(i, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT), + ELEM(iframe, true, true, GROUP_SPECIAL | GROUP_BLOCK, + GROUP_FLOW_ELEMENT), + ELEM(image, false, false, GROUP_NONE, GROUP_NONE), + ELEM(img, false, false, GROUP_SPECIAL | GROUP_PICTURE_CONTENT, GROUP_NONE), + ELEM(input, false, false, GROUP_FORMCONTROL, GROUP_NONE), + ELEM(ins, true, true, GROUP_PHRASE | GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(kbd, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(keygen, false, false, GROUP_FORMCONTROL, GROUP_NONE), + ELEM(label, true, false, GROUP_FORMCONTROL, GROUP_INLINE_ELEMENT), + ELEM(legend, true, true, GROUP_NONE, GROUP_INLINE_ELEMENT), + ELEM(li, true, false, GROUP_LI, GROUP_FLOW_ELEMENT), + ELEM(link, false, false, GROUP_HEAD_CONTENT, GROUP_NONE), + ELEM(listing, false, false, GROUP_NONE, GROUP_NONE), + ELEM(main, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(map, true, true, GROUP_SPECIAL, GROUP_BLOCK | GROUP_MAP_CONTENT), + ELEM(mark, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(marquee, false, false, GROUP_NONE, GROUP_NONE), + ELEM(menu, true, true, GROUP_BLOCK, GROUP_LI | GROUP_FLOW_ELEMENT), + ELEM(menuitem, false, false, GROUP_NONE, GROUP_NONE), + ELEM(meta, false, false, GROUP_HEAD_CONTENT, GROUP_NONE), + ELEM(meter, true, false, GROUP_SPECIAL, GROUP_FLOW_ELEMENT), + ELEM(multicol, false, false, GROUP_NONE, GROUP_NONE), + ELEM(nav, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(nobr, false, false, GROUP_NONE, GROUP_NONE), + ELEM(noembed, false, false, GROUP_NONE, GROUP_NONE), + ELEM(noframes, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(noscript, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(object, true, true, GROUP_SPECIAL | GROUP_BLOCK, + GROUP_FLOW_ELEMENT | GROUP_OBJECT_CONTENT), + // XXX Can contain self and ul because editor does sublists illegally. + ELEM(ol, true, true, GROUP_BLOCK | GROUP_OL_UL, + GROUP_LI | GROUP_OL_UL), + ELEM(optgroup, true, false, GROUP_SELECT_CONTENT, + GROUP_OPTIONS), + ELEM(option, true, false, + GROUP_SELECT_CONTENT | GROUP_OPTIONS, GROUP_LEAF), + ELEM(output, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT), + ELEM(p, true, false, GROUP_BLOCK | GROUP_P, GROUP_INLINE_ELEMENT), + ELEM(param, false, false, GROUP_OBJECT_CONTENT, GROUP_NONE), + ELEM(picture, true, false, GROUP_SPECIAL, GROUP_PICTURE_CONTENT), + ELEM(plaintext, false, false, GROUP_NONE, GROUP_NONE), + ELEM(pre, true, true, GROUP_BLOCK, GROUP_INLINE_ELEMENT), + ELEM(progress, true, false, GROUP_SPECIAL, GROUP_FLOW_ELEMENT), + ELEM(q, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT), + ELEM(rb, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(rp, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(rt, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(rtc, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(ruby, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(s, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT), + ELEM(samp, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(script, true, false, GROUP_HEAD_CONTENT | GROUP_SPECIAL, + GROUP_LEAF), + ELEM(section, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(select, true, false, GROUP_FORMCONTROL, GROUP_SELECT_CONTENT), + ELEM(shadow, true, false, GROUP_NONE, GROUP_INLINE_ELEMENT), + ELEM(small, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT), + ELEM(source, false, false, GROUP_PICTURE_CONTENT, GROUP_NONE), + ELEM(span, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT), + ELEM(strike, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT), + ELEM(strong, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(style, true, false, GROUP_HEAD_CONTENT, GROUP_LEAF), + ELEM(sub, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT), + ELEM(summary, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT), + ELEM(sup, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT), + ELEM(table, true, false, GROUP_BLOCK, GROUP_TABLE_CONTENT), + ELEM(tbody, true, false, GROUP_TABLE_CONTENT, GROUP_TBODY_CONTENT), + ELEM(td, true, false, GROUP_TR_CONTENT, GROUP_FLOW_ELEMENT), + ELEM(textarea, true, false, GROUP_FORMCONTROL, GROUP_LEAF), + ELEM(tfoot, true, false, GROUP_NONE, GROUP_TBODY_CONTENT), + ELEM(th, true, false, GROUP_TR_CONTENT, GROUP_FLOW_ELEMENT), + ELEM(thead, true, false, GROUP_NONE, GROUP_TBODY_CONTENT), + ELEM(template, false, false, GROUP_NONE, GROUP_NONE), + ELEM(time, true, false, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(title, true, false, GROUP_HEAD_CONTENT, GROUP_LEAF), + ELEM(tr, true, false, GROUP_TBODY_CONTENT, GROUP_TR_CONTENT), + ELEM(track, false, false, GROUP_NONE, GROUP_NONE), + ELEM(tt, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT), + ELEM(u, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT), + // XXX Can contain self and ol because editor does sublists illegally. + ELEM(ul, true, true, GROUP_BLOCK | GROUP_OL_UL, + GROUP_LI | GROUP_OL_UL), + ELEM(var, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT), + ELEM(video, false, false, GROUP_NONE, GROUP_NONE), + ELEM(wbr, false, false, GROUP_NONE, GROUP_NONE), + ELEM(xmp, false, false, GROUP_NONE, GROUP_NONE), + + // These aren't elements. + ELEM(text, false, false, GROUP_LEAF, GROUP_NONE), + ELEM(whitespace, false, false, GROUP_LEAF, GROUP_NONE), + ELEM(newline, false, false, GROUP_LEAF, GROUP_NONE), + ELEM(comment, false, false, GROUP_LEAF, GROUP_NONE), + ELEM(entity, false, false, GROUP_NONE, GROUP_NONE), + ELEM(doctypeDecl, false, false, GROUP_NONE, GROUP_NONE), + ELEM(markupDecl, false, false, GROUP_NONE, GROUP_NONE), + ELEM(instruction, false, false, GROUP_NONE, GROUP_NONE), + + ELEM(userdefined, true, false, GROUP_NONE, GROUP_FLOW_ELEMENT) +}; + +bool +HTMLEditUtils::CanContain(int32_t aParent, int32_t aChild) +{ + NS_ASSERTION(aParent > eHTMLTag_unknown && aParent <= eHTMLTag_userdefined, + "aParent out of range!"); + NS_ASSERTION(aChild > eHTMLTag_unknown && aChild <= eHTMLTag_userdefined, + "aChild out of range!"); + +#ifdef DEBUG + static bool checked = false; + if (!checked) { + checked = true; + int32_t i; + for (i = 1; i <= eHTMLTag_userdefined; ++i) { + NS_ASSERTION(kElements[i - 1].mTag == i, + "You need to update kElements (missing tags)."); + } + } +#endif + + // Special-case button. + if (aParent == eHTMLTag_button) { + static const eHTMLTags kButtonExcludeKids[] = { + eHTMLTag_a, + eHTMLTag_fieldset, + eHTMLTag_form, + eHTMLTag_iframe, + eHTMLTag_input, + eHTMLTag_select, + eHTMLTag_textarea + }; + + uint32_t j; + for (j = 0; j < ArrayLength(kButtonExcludeKids); ++j) { + if (kButtonExcludeKids[j] == aChild) { + return false; + } + } + } + + // Deprecated elements. + if (aChild == eHTMLTag_bgsound) { + return false; + } + + // Bug #67007, dont strip userdefined tags. + if (aChild == eHTMLTag_userdefined) { + return true; + } + + const ElementInfo& parent = kElements[aParent - 1]; + if (aParent == aChild) { + return parent.mCanContainSelf; + } + + const ElementInfo& child = kElements[aChild - 1]; + return (parent.mCanContainGroups & child.mGroup) != 0; +} + +bool +HTMLEditUtils::IsContainer(int32_t aTag) +{ + NS_ASSERTION(aTag > eHTMLTag_unknown && aTag <= eHTMLTag_userdefined, + "aTag out of range!"); + + return kElements[aTag - 1].mIsContainer; +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h new file mode 100644 index 000000000..4bbb6fdf3 --- /dev/null +++ b/editor/libeditor/HTMLEditUtils.h @@ -0,0 +1,69 @@ +/* -*- 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/. */ + +#ifndef HTMLEditUtils_h +#define HTMLEditUtils_h + +#include <stdint.h> + +class nsIDOMNode; +class nsINode; + +namespace mozilla { + +class HTMLEditUtils final +{ +public: + // from nsHTMLEditRules: + static bool IsInlineStyle(nsINode* aNode); + static bool IsInlineStyle(nsIDOMNode *aNode); + static bool IsFormatNode(nsINode* aNode); + static bool IsFormatNode(nsIDOMNode* aNode); + static bool IsNodeThatCanOutdent(nsIDOMNode* aNode); + static bool IsHeader(nsINode& aNode); + static bool IsHeader(nsIDOMNode* aNode); + static bool IsParagraph(nsIDOMNode* aNode); + static bool IsHR(nsIDOMNode* aNode); + static bool IsListItem(nsINode* aNode); + static bool IsListItem(nsIDOMNode* aNode); + static bool IsTable(nsIDOMNode* aNode); + static bool IsTable(nsINode* aNode); + static bool IsTableRow(nsIDOMNode* aNode); + static bool IsTableElement(nsINode* aNode); + static bool IsTableElement(nsIDOMNode* aNode); + static bool IsTableElementButNotTable(nsINode* aNode); + static bool IsTableElementButNotTable(nsIDOMNode* aNode); + static bool IsTableCell(nsINode* node); + static bool IsTableCell(nsIDOMNode* aNode); + static bool IsTableCellOrCaption(nsINode& aNode); + static bool IsList(nsINode* aNode); + static bool IsList(nsIDOMNode* aNode); + static bool IsOrderedList(nsIDOMNode* aNode); + static bool IsUnorderedList(nsIDOMNode* aNode); + static bool IsBlockquote(nsIDOMNode* aNode); + static bool IsPre(nsIDOMNode* aNode); + static bool IsAnchor(nsIDOMNode* aNode); + static bool IsImage(nsINode* aNode); + static bool IsImage(nsIDOMNode* aNode); + static bool IsLink(nsIDOMNode* aNode); + static bool IsLink(nsINode* aNode); + static bool IsNamedAnchor(nsINode* aNode); + static bool IsNamedAnchor(nsIDOMNode* aNode); + static bool IsDiv(nsIDOMNode* aNode); + static bool IsMozDiv(nsINode* aNode); + static bool IsMozDiv(nsIDOMNode* aNode); + static bool IsMailCite(nsINode* aNode); + static bool IsMailCite(nsIDOMNode* aNode); + static bool IsFormWidget(nsINode* aNode); + static bool IsFormWidget(nsIDOMNode* aNode); + static bool SupportsAlignAttr(nsIDOMNode* aNode); + static bool CanContain(int32_t aParent, int32_t aChild); + static bool IsContainer(int32_t aTag); +}; + +} // namespace mozilla + +#endif // #ifndef HTMLEditUtils_h + diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp new file mode 100644 index 000000000..dd47ffd3c --- /dev/null +++ b/editor/libeditor/HTMLEditor.cpp @@ -0,0 +1,5289 @@ +/* -*- 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/HTMLEditor.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/EventStates.h" +#include "mozilla/TextEvents.h" + +#include "nsCRT.h" + +#include "nsUnicharUtils.h" + +#include "HTMLEditorEventListener.h" +#include "HTMLEditRules.h" +#include "HTMLEditUtils.h" +#include "HTMLURIRefObject.h" +#include "SetDocumentTitleTransaction.h" +#include "StyleSheetTransactions.h" +#include "TextEditUtils.h" +#include "TypeInState.h" + +#include "nsIDOMText.h" +#include "nsIDOMMozNamedAttrMap.h" +#include "nsIDOMNodeList.h" +#include "nsIDOMDocument.h" +#include "nsIDOMAttr.h" +#include "nsIDocumentInlines.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMMouseEvent.h" +#include "nsIDOMHTMLAnchorElement.h" +#include "nsISelectionController.h" +#include "nsIDOMHTMLDocument.h" +#include "nsILinkHandler.h" +#include "nsIInlineSpellChecker.h" + +#include "mozilla/css/Loader.h" +#include "nsIDOMStyleSheet.h" + +#include "nsIContent.h" +#include "nsIContentIterator.h" +#include "nsIMutableArray.h" +#include "nsContentUtils.h" +#include "nsIDocumentEncoder.h" +#include "nsIPresShell.h" +#include "nsPresContext.h" +#include "nsFocusManager.h" +#include "nsPIDOMWindow.h" + +// netwerk +#include "nsIURI.h" +#include "nsNetUtil.h" + +// Misc +#include "mozilla/EditorUtils.h" +#include "HTMLEditorObjectResizerUtils.h" +#include "TextEditorTest.h" +#include "WSRunObject.h" +#include "nsGkAtoms.h" +#include "nsIWidget.h" + +#include "nsIFrame.h" +#include "nsIParserService.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "nsTextFragment.h" +#include "nsContentList.h" +#include "mozilla/StyleSheet.h" +#include "mozilla/StyleSheetInlines.h" + +namespace mozilla { + +using namespace dom; +using namespace widget; + +// Some utilities to handle overloading of "A" tag for link and named anchor. +static bool +IsLinkTag(const nsString& s) +{ + return s.EqualsIgnoreCase("href"); +} + +static bool +IsNamedAnchorTag(const nsString& s) +{ + return s.EqualsIgnoreCase("anchor") || s.EqualsIgnoreCase("namedanchor"); +} + +HTMLEditor::HTMLEditor() + : mCRInParagraphCreatesParagraph(false) + , mCSSAware(false) + , mSelectedCellIndex(0) + , mIsObjectResizingEnabled(true) + , mIsResizing(false) + , mPreserveRatio(false) + , mResizedObjectIsAnImage(false) + , mIsAbsolutelyPositioningEnabled(true) + , mResizedObjectIsAbsolutelyPositioned(false) + , mGrabberClicked(false) + , mIsMoving(false) + , mSnapToGridEnabled(false) + , mIsInlineTableEditingEnabled(true) + , 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) +{ +} + +HTMLEditor::~HTMLEditor() +{ + // remove the rules as an action listener. Else we get a bad + // ownership loop later on. it's ok if the rules aren't a listener; + // we ignore the error. + nsCOMPtr<nsIEditActionListener> mListener = do_QueryInterface(mRules); + RemoveEditActionListener(mListener); + + //the autopointers will clear themselves up. + //but we need to also remove the listeners or we have a leak + RefPtr<Selection> selection = GetSelection(); + // if we don't get the selection, just skip this + if (selection) { + nsCOMPtr<nsISelectionListener>listener; + listener = do_QueryInterface(mTypeInState); + if (listener) { + selection->RemoveSelectionListener(listener); + } + listener = do_QueryInterface(mSelectionListenerP); + if (listener) { + selection->RemoveSelectionListener(listener); + } + } + + mTypeInState = nullptr; + mSelectionListenerP = nullptr; + + // free any default style propItems + RemoveAllDefaultProperties(); + + if (mLinkHandler && mDocWeak) { + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + + if (ps && ps->GetPresContext()) { + ps->GetPresContext()->SetLinkHandler(mLinkHandler); + } + } + + RemoveEventListeners(); + + HideAnonymousEditingUIs(); +} + +void +HTMLEditor::HideAnonymousEditingUIs() +{ + if (mAbsolutelyPositionedObject) { + HideGrabber(); + } + if (mInlineEditedCell) { + HideInlineTableEditingUI(); + } + if (mResizedObject) { + HideResizers(); + } +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEditor, TextEditor) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTypeInState) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStyleSheets) + + tmp->HideAnonymousEditingUIs(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEditor, TextEditor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTypeInState) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheets) + + 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(mMouseMotionListenerP) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionListenerP) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizeEventListenerP) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObjectResizeEventListeners) + + 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_INHERITED(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(nsIEditorStyleSheets) + NS_INTERFACE_MAP_ENTRY(nsICSSLoaderObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) +NS_INTERFACE_MAP_END_INHERITING(TextEditor) + +NS_IMETHODIMP +HTMLEditor::Init(nsIDOMDocument* aDoc, + nsIContent* aRoot, + nsISelectionController* aSelCon, + uint32_t aFlags, + const nsAString& aInitialValue) +{ + NS_PRECONDITION(aDoc && !aSelCon, "bad arg"); + NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER); + MOZ_ASSERT(aInitialValue.IsEmpty(), "Non-empty initial values not supported"); + + nsresult rulesRv = NS_OK; + + { + // block to scope AutoEditInitRulesTrigger + AutoEditInitRulesTrigger rulesTrigger(this, rulesRv); + + // Init the plaintext editor + nsresult rv = TextEditor::Init(aDoc, aRoot, nullptr, aFlags, aInitialValue); + if (NS_FAILED(rv)) { + return rv; + } + + // Init mutation observer + nsCOMPtr<nsINode> document = do_QueryInterface(aDoc); + document->AddMutationObserverUnlessExists(this); + + if (!mRootElement) { + UpdateRootElement(); + } + + // disable Composer-only features + if (IsMailEditor()) { + SetAbsolutePositioningEnabled(false); + SetSnapToGridEnabled(false); + } + + // Init the HTML-CSS utils + mCSSEditUtils = new CSSEditUtils(this); + + // disable links + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); + nsPresContext *context = presShell->GetPresContext(); + NS_ENSURE_TRUE(context, NS_ERROR_NULL_POINTER); + if (!IsPlaintextEditor() && !IsInteractionAllowed()) { + mLinkHandler = context->GetLinkHandler(); + context->SetLinkHandler(nullptr); + } + + // init the type-in state + mTypeInState = new TypeInState(); + + // init the selection listener for image resizing + mSelectionListenerP = new ResizerSelectionListener(this); + + if (!IsInteractionAllowed()) { + // ignore any errors from this in case the file is missing + AddOverrideStyleSheet(NS_LITERAL_STRING("resource://gre/res/EditorOverride.css")); + } + + RefPtr<Selection> selection = GetSelection(); + if (selection) { + nsCOMPtr<nsISelectionListener>listener; + listener = do_QueryInterface(mTypeInState); + if (listener) { + selection->AddSelectionListener(listener); + } + listener = do_QueryInterface(mSelectionListenerP); + if (listener) { + selection->AddSelectionListener(listener); + } + } + } + NS_ENSURE_SUCCESS(rulesRv, rulesRv); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::PreDestroy(bool aDestroyingFrames) +{ + if (mDidPreDestroy) { + return NS_OK; + } + + nsCOMPtr<nsINode> document = do_QueryReferent(mDocWeak); + if (document) { + document->RemoveMutationObserver(this); + } + + while (!mStyleSheetURLs.IsEmpty()) { + RemoveOverrideStyleSheet(mStyleSheetURLs[0]); + } + + // 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). + HideAnonymousEditingUIs(); + + return TextEditor::PreDestroy(aDestroyingFrames); +} + +void +HTMLEditor::UpdateRootElement() +{ + // Use the HTML documents body element as the editor root if we didn't + // get a root element during initialization. + + nsCOMPtr<nsIDOMElement> rootElement; + nsCOMPtr<nsIDOMHTMLElement> bodyElement; + GetBodyElement(getter_AddRefs(bodyElement)); + if (bodyElement) { + rootElement = bodyElement; + } else { + // If there is no HTML body element, + // we should use the document root element instead. + nsCOMPtr<nsIDOMDocument> doc = do_QueryReferent(mDocWeak); + if (doc) { + doc->GetDocumentElement(getter_AddRefs(rootElement)); + } + } + + mRootElement = do_QueryInterface(rootElement); +} + +already_AddRefed<nsIContent> +HTMLEditor::FindSelectionRoot(nsINode* aNode) +{ + NS_PRECONDITION(aNode->IsNodeOfType(nsINode::eDOCUMENT) || + aNode->IsNodeOfType(nsINode::eCONTENT), + "aNode must be content or document node"); + + nsCOMPtr<nsIDocument> doc = aNode->GetUncomposedDoc(); + if (!doc) { + return nullptr; + } + + nsCOMPtr<nsIContent> content; + if (doc->HasFlag(NODE_IS_EDITABLE) || !aNode->IsContent()) { + content = doc->GetRootElement(); + return content.forget(); + } + content = aNode->AsContent(); + + // 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. + content = do_QueryInterface(GetRoot()); + return content.forget(); + } + + 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_MOZ_READWRITE)) { + return content.forget(); + } + return nullptr; + } + + // For non-readonly editors we want to find the root of the editable subtree + // containing aContent. + content = content->GetEditingHost(); + return content.forget(); +} + +void +HTMLEditor::CreateEventListeners() +{ + // Don't create the handler twice + if (!mEventListener) { + mEventListener = new HTMLEditorEventListener(); + } +} + +nsresult +HTMLEditor::InstallEventListeners() +{ + NS_ENSURE_TRUE(mDocWeak && mEventListener, + 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()); + return listener->Connect(this); +} + +void +HTMLEditor::RemoveEventListeners() +{ + if (!mDocWeak) { + return; + } + + nsCOMPtr<nsIDOMEventTarget> target = GetDOMEventTarget(); + + if (target) { + // Both mMouseMotionListenerP and mResizeEventListenerP can be + // registerd with other targets than the DOM event receiver that + // we can reach from here. But nonetheless, unregister the event + // listeners with the DOM event reveiver (if it's registerd with + // other targets, it'll get unregisterd once the target goes + // away). + + if (mMouseMotionListenerP) { + // mMouseMotionListenerP might be registerd either as bubbling or + // capturing, unregister by both. + target->RemoveEventListener(NS_LITERAL_STRING("mousemove"), + mMouseMotionListenerP, false); + target->RemoveEventListener(NS_LITERAL_STRING("mousemove"), + mMouseMotionListenerP, true); + } + + if (mResizeEventListenerP) { + target->RemoveEventListener(NS_LITERAL_STRING("resize"), + mResizeEventListenerP, false); + } + } + + mMouseMotionListenerP = nullptr; + mResizeEventListenerP = nullptr; + + TextEditor::RemoveEventListeners(); +} + +NS_IMETHODIMP +HTMLEditor::SetFlags(uint32_t aFlags) +{ + nsresult rv = TextEditor::SetFlags(aFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Sets mCSSAware to correspond to aFlags. This toggles whether CSS is + // used to style elements in the editor. Note that the editor is only CSS + // aware by default in Composer and in the mail editor. + mCSSAware = !NoCSS() && !IsMailEditor(); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::InitRules() +{ + if (!mRules) { + // instantiate the rules for the html editor + mRules = new HTMLEditRules(); + } + return mRules->Init(static_cast<TextEditor*>(this)); +} + +NS_IMETHODIMP +HTMLEditor::BeginningOfDocument() +{ + if (!mDocWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Get the selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + + // Get the root element. + nsCOMPtr<Element> rootElement = GetRoot(); + if (!rootElement) { + NS_WARNING("GetRoot() returned a null pointer (mRootElement is null)"); + return NS_OK; + } + + // Find first editable thingy + bool done = false; + nsCOMPtr<nsINode> curNode = rootElement.get(), selNode; + int32_t curOffset = 0, selOffset = 0; + while (!done) { + WSRunObject wsObj(this, curNode, curOffset); + int32_t visOffset = 0; + WSType visType; + nsCOMPtr<nsINode> visNode; + wsObj.NextVisibleNode(curNode, curOffset, address_of(visNode), &visOffset, + &visType); + if (visType == WSType::normalWS || visType == WSType::text) { + selNode = visNode; + selOffset = visOffset; + done = true; + } else if (visType == WSType::br || visType == WSType::special) { + selNode = visNode->GetParentNode(); + selOffset = selNode ? selNode->IndexOf(visNode) : -1; + done = true; + } else if (visType == WSType::otherBlock) { + // By definition of WSRunObject, a block element terminates a + // whitespace run. That is, although we are calling a method that is + // named "NextVisibleNode", the node returned might not be + // visible/editable! + // + // If the given block does not contain any visible/editable items, we + // want to skip it and continue our search. + + if (!IsContainer(visNode)) { + // However, we were given a block that is not a container. Since the + // block can not contain anything that's visible, such a block only + // makes sense if it is visible by itself, like a <hr>. We want to + // place the caret in front of that block. + selNode = visNode->GetParentNode(); + selOffset = selNode ? selNode->IndexOf(visNode) : -1; + done = true; + } else { + bool isEmptyBlock; + if (NS_SUCCEEDED(IsEmptyNode(visNode, &isEmptyBlock)) && + isEmptyBlock) { + // Skip the empty block + curNode = visNode->GetParentNode(); + curOffset = curNode ? curNode->IndexOf(visNode) : -1; + curOffset++; + } else { + curNode = visNode; + curOffset = 0; + } + // Keep looping + } + } else { + // Else we found nothing useful + selNode = curNode; + selOffset = curOffset; + done = true; + } + } + return selection->Collapse(selNode, selOffset); +} + +nsresult +HTMLEditor::HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) +{ + // NOTE: When you change this method, you should also change: + // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html + + if (IsReadonly() || IsDisabled()) { + // When we're not editable, the events are handled on EditorBase, so, we can + // bypass TextEditor. + return EditorBase::HandleKeyPressEvent(aKeyEvent); + } + + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent(); + NS_ENSURE_TRUE(nativeKeyEvent, NS_ERROR_UNEXPECTED); + NS_ASSERTION(nativeKeyEvent->mMessage == eKeyPress, + "HandleKeyPressEvent gets non-keypress event"); + + switch (nativeKeyEvent->mKeyCode) { + case NS_VK_META: + case NS_VK_WIN: + case NS_VK_SHIFT: + case NS_VK_CONTROL: + case NS_VK_ALT: + case NS_VK_BACK: + case NS_VK_DELETE: + // These keys are handled on EditorBase, so, we can bypass + // TextEditor. + return EditorBase::HandleKeyPressEvent(aKeyEvent); + case NS_VK_TAB: { + if (IsPlaintextEditor()) { + // If this works as plain text editor, e.g., mail editor for plain + // text, should be handled on TextEditor. + return TextEditor::HandleKeyPressEvent(aKeyEvent); + } + + if (IsTabbable()) { + return NS_OK; // let it be used for focus switching + } + + if (nativeKeyEvent->IsControl() || nativeKeyEvent->IsAlt() || + nativeKeyEvent->IsMeta() || nativeKeyEvent->IsOS()) { + return NS_OK; + } + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection && selection->RangeCount(), NS_ERROR_FAILURE); + + nsCOMPtr<nsINode> node = selection->GetRangeAt(0)->GetStartParent(); + MOZ_ASSERT(node); + + nsCOMPtr<Element> blockParent = GetBlock(*node); + + if (!blockParent) { + break; + } + + bool handled = false; + nsresult rv = NS_OK; + if (HTMLEditUtils::IsTableElement(blockParent)) { + rv = TabInTable(nativeKeyEvent->IsShift(), &handled); + if (handled) { + ScrollSelectionIntoView(false); + } + } else if (HTMLEditUtils::IsListItem(blockParent)) { + rv = Indent(nativeKeyEvent->IsShift() + ? NS_LITERAL_STRING("outdent") + : NS_LITERAL_STRING("indent")); + handled = true; + } + NS_ENSURE_SUCCESS(rv, rv); + if (handled) { + return aKeyEvent->AsEvent()->PreventDefault(); // consumed + } + if (nativeKeyEvent->IsShift()) { + return NS_OK; // don't type text for shift tabs + } + aKeyEvent->AsEvent()->PreventDefault(); + return TypedText(NS_LITERAL_STRING("\t"), eTypedText); + } + case NS_VK_RETURN: + if (nativeKeyEvent->IsControl() || nativeKeyEvent->IsAlt() || + nativeKeyEvent->IsMeta() || nativeKeyEvent->IsOS()) { + return NS_OK; + } + aKeyEvent->AsEvent()->PreventDefault(); // consumed + if (nativeKeyEvent->IsShift() && !IsPlaintextEditor()) { + // only inserts a br node + return TypedText(EmptyString(), eTypedBR); + } + // uses rules to figure out what to insert + return TypedText(EmptyString(), eTypedBreak); + } + + // NOTE: On some keyboard layout, some characters are inputted with Control + // key or Alt key, but at that time, widget sets FALSE to these keys. + if (!nativeKeyEvent->mCharCode || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + // we don't PreventDefault() here or keybindings like control-x won't work + return NS_OK; + } + aKeyEvent->AsEvent()->PreventDefault(); + nsAutoString str(nativeKeyEvent->mCharCode); + return TypedText(str, eTypedText); +} + +static void +AssertParserServiceIsCorrect(nsIAtom* aTag, bool aIsBlock) +{ +#ifdef DEBUG + // Check this against what we would have said with the old code: + if (aTag == nsGkAtoms::p || + aTag == nsGkAtoms::div || + aTag == nsGkAtoms::blockquote || + aTag == nsGkAtoms::h1 || + aTag == nsGkAtoms::h2 || + aTag == nsGkAtoms::h3 || + aTag == nsGkAtoms::h4 || + aTag == nsGkAtoms::h5 || + aTag == nsGkAtoms::h6 || + aTag == nsGkAtoms::ul || + aTag == nsGkAtoms::ol || + aTag == nsGkAtoms::dl || + aTag == nsGkAtoms::noscript || + aTag == nsGkAtoms::form || + aTag == nsGkAtoms::hr || + aTag == nsGkAtoms::table || + aTag == nsGkAtoms::fieldset || + aTag == nsGkAtoms::address || + aTag == nsGkAtoms::col || + aTag == nsGkAtoms::colgroup || + aTag == nsGkAtoms::li || + aTag == nsGkAtoms::dt || + aTag == nsGkAtoms::dd || + aTag == nsGkAtoms::legend) { + if (!aIsBlock) { + nsAutoString assertmsg (NS_LITERAL_STRING("Parser and editor disagree on blockness: ")); + + nsAutoString tagName; + aTag->ToString(tagName); + assertmsg.Append(tagName); + char* assertstr = ToNewCString(assertmsg); + NS_ASSERTION(aIsBlock, assertstr); + free(assertstr); + } + } +#endif // DEBUG +} + +/** + * Returns true if the id represents an element of block type. + * Can be used to determine if a new paragraph should be started. + */ +bool +HTMLEditor::NodeIsBlockStatic(const nsINode* aElement) +{ + MOZ_ASSERT(aElement); + + // Nodes we know we want to treat as block + // even though the parser says they're not: + if (aElement->IsAnyOfHTMLElements(nsGkAtoms::body, + nsGkAtoms::head, + nsGkAtoms::tbody, + nsGkAtoms::thead, + nsGkAtoms::tfoot, + nsGkAtoms::tr, + nsGkAtoms::th, + nsGkAtoms::td, + nsGkAtoms::li, + nsGkAtoms::dt, + nsGkAtoms::dd, + nsGkAtoms::pre)) { + return true; + } + + bool isBlock; +#ifdef DEBUG + // XXX we can't use DebugOnly here because VC++ is stupid (bug 802884) + nsresult rv = +#endif + nsContentUtils::GetParserService()-> + IsBlock(nsContentUtils::GetParserService()->HTMLAtomTagToId( + aElement->NodeInfo()->NameAtom()), + isBlock); + MOZ_ASSERT(rv == NS_OK); + + AssertParserServiceIsCorrect(aElement->NodeInfo()->NameAtom(), isBlock); + + return isBlock; +} + +nsresult +HTMLEditor::NodeIsBlockStatic(nsIDOMNode* aNode, + bool* aIsBlock) +{ + if (!aNode || !aIsBlock) { + return NS_ERROR_NULL_POINTER; + } + + nsCOMPtr<dom::Element> element = do_QueryInterface(aNode); + *aIsBlock = element && NodeIsBlockStatic(element); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::NodeIsBlock(nsIDOMNode* aNode, + bool* aIsBlock) +{ + return NodeIsBlockStatic(aNode, aIsBlock); +} + +bool +HTMLEditor::IsBlockNode(nsINode* aNode) +{ + return aNode && NodeIsBlockStatic(aNode); +} + +// Non-static version for the nsIEditor interface and JavaScript +NS_IMETHODIMP +HTMLEditor::SetDocumentTitle(const nsAString& aTitle) +{ + RefPtr<SetDocumentTitleTransaction> transaction = + new SetDocumentTitleTransaction(); + NS_ENSURE_TRUE(transaction, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = transaction->Init(this, &aTitle); + NS_ENSURE_SUCCESS(rv, rv); + + //Don't let Rules System change the selection + AutoTransactionsConserveSelection dontChangeSelection(this); + return EditorBase::DoTransaction(transaction); +} + +/** + * GetBlockNodeParent returns enclosing block level ancestor, if any. + */ +Element* +HTMLEditor::GetBlockNodeParent(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + nsCOMPtr<nsINode> p = aNode->GetParentNode(); + + while (p) { + if (NodeIsBlockStatic(p)) { + return p->AsElement(); + } + p = p->GetParentNode(); + } + + return nullptr; +} + +nsIDOMNode* +HTMLEditor::GetBlockNodeParent(nsIDOMNode* aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + + if (!node) { + NS_NOTREACHED("null node passed to GetBlockNodeParent()"); + return nullptr; + } + + return GetAsDOMNode(GetBlockNodeParent(node)); +} + +/** + * Returns the node if it's a block, otherwise GetBlockNodeParent + */ +Element* +HTMLEditor::GetBlock(nsINode& aNode) +{ + if (NodeIsBlockStatic(&aNode)) { + return aNode.AsElement(); + } + return GetBlockNodeParent(&aNode); +} + +/** + * IsNextCharInNodeWhitespace() checks the adjacent content in the same node to + * see if following selection is whitespace or nbsp. + */ +void +HTMLEditor::IsNextCharInNodeWhitespace(nsIContent* aContent, + int32_t aOffset, + bool* outIsSpace, + bool* outIsNBSP, + nsIContent** outNode, + int32_t* outOffset) +{ + MOZ_ASSERT(aContent && outIsSpace && outIsNBSP); + MOZ_ASSERT((outNode && outOffset) || (!outNode && !outOffset)); + *outIsSpace = false; + *outIsNBSP = false; + if (outNode && outOffset) { + *outNode = nullptr; + *outOffset = -1; + } + + if (aContent->IsNodeOfType(nsINode::eTEXT) && + (uint32_t)aOffset < aContent->Length()) { + char16_t ch = aContent->GetText()->CharAt(aOffset); + *outIsSpace = nsCRT::IsAsciiSpace(ch); + *outIsNBSP = (ch == kNBSP); + if (outNode && outOffset) { + NS_IF_ADDREF(*outNode = aContent); + // yes, this is _past_ the character + *outOffset = aOffset + 1; + } + } +} + + +/** + * IsPrevCharInNodeWhitespace() checks the adjacent content in the same node to + * see if following selection is whitespace. + */ +void +HTMLEditor::IsPrevCharInNodeWhitespace(nsIContent* aContent, + int32_t aOffset, + bool* outIsSpace, + bool* outIsNBSP, + nsIContent** outNode, + int32_t* outOffset) +{ + MOZ_ASSERT(aContent && outIsSpace && outIsNBSP); + MOZ_ASSERT((outNode && outOffset) || (!outNode && !outOffset)); + *outIsSpace = false; + *outIsNBSP = false; + if (outNode && outOffset) { + *outNode = nullptr; + *outOffset = -1; + } + + if (aContent->IsNodeOfType(nsINode::eTEXT) && aOffset > 0) { + char16_t ch = aContent->GetText()->CharAt(aOffset - 1); + *outIsSpace = nsCRT::IsAsciiSpace(ch); + *outIsNBSP = (ch == kNBSP); + if (outNode && outOffset) { + NS_IF_ADDREF(*outNode = aContent); + *outOffset = aOffset - 1; + } + } +} + +bool +HTMLEditor::IsVisBreak(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + if (!TextEditUtils::IsBreak(aNode)) { + return false; + } + // Check if there is a later node in block after br + nsCOMPtr<nsINode> nextNode = GetNextHTMLNode(aNode, true); + if (nextNode && TextEditUtils::IsBreak(nextNode)) { + return true; + } + + // A single line break before a block boundary is not displayed, so e.g. + // foo<p>bar<br></p> and foo<br><p>bar</p> display the same as foo<p>bar</p>. + // But if there are multiple <br>s in a row, all but the last are visible. + if (!nextNode) { + // This break is trailer in block, it's not visible + return false; + } + if (IsBlockNode(nextNode)) { + // Break is right before a block, it's not visible + return false; + } + + // If there's an inline node after this one that's not a break, and also a + // prior break, this break must be visible. + nsCOMPtr<nsINode> priorNode = GetPriorHTMLNode(aNode, true); + if (priorNode && TextEditUtils::IsBreak(priorNode)) { + return true; + } + + // Sigh. We have to use expensive whitespace calculation code to + // determine what is going on + int32_t selOffset; + nsCOMPtr<nsINode> selNode = GetNodeLocation(aNode, &selOffset); + // Let's look after the break + selOffset++; + WSRunObject wsObj(this, selNode, selOffset); + nsCOMPtr<nsINode> unused; + int32_t visOffset = 0; + WSType visType; + wsObj.NextVisibleNode(selNode, selOffset, address_of(unused), + &visOffset, &visType); + if (visType & WSType::block) { + return false; + } + + return true; +} + +NS_IMETHODIMP +HTMLEditor::GetIsDocumentEditable(bool* aIsDocumentEditable) +{ + NS_ENSURE_ARG_POINTER(aIsDocumentEditable); + + nsCOMPtr<nsIDOMDocument> doc = GetDOMDocument(); + *aIsDocumentEditable = doc && IsModifiable(); + + return NS_OK; +} + +bool +HTMLEditor::IsModifiable() +{ + return !IsReadonly(); +} + +NS_IMETHODIMP +HTMLEditor::UpdateBaseURL() +{ + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + // Look for an HTML <base> tag + RefPtr<nsContentList> nodeList = + doc->GetElementsByTagName(NS_LITERAL_STRING("base")); + + // 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 (!nodeList || !nodeList->Item(0)) { + doc->SetBaseURI(doc->GetDocumentURI()); + } + return NS_OK; +} + +/** + * This routine is needed to provide a bottleneck for typing for logging + * purposes. Can't use HandleKeyPress() (above) for that since it takes + * a nsIDOMKeyEvent* parameter. So instead we pass enough info through + * to TypedText() to determine what action to take, but without passing + * an event. + */ +NS_IMETHODIMP +HTMLEditor::TypedText(const nsAString& aString, + ETypingAction aAction) +{ + AutoPlaceHolderBatch batch(this, nsGkAtoms::TypingTxnName); + + if (aAction == eTypedBR) { + // only inserts a br node + nsCOMPtr<nsIDOMNode> brNode; + return InsertBR(address_of(brNode)); + } + + return TextEditor::TypedText(aString, aAction); +} + +NS_IMETHODIMP +HTMLEditor::TabInTable(bool inIsShift, + bool* outHandled) +{ + NS_ENSURE_TRUE(outHandled, NS_ERROR_NULL_POINTER); + *outHandled = false; + + // Find enclosing table cell from selection (cell may be selected element) + nsCOMPtr<Element> cellElement = + GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr); + // Do nothing -- we didn't find a table cell + NS_ENSURE_TRUE(cellElement, NS_OK); + + // find enclosing table + nsCOMPtr<Element> table = GetEnclosingTable(cellElement); + NS_ENSURE_TRUE(table, NS_OK); + + // advance to next cell + // first create an iterator over the table + nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator(); + nsresult rv = iter->Init(table); + NS_ENSURE_SUCCESS(rv, rv); + // position iter at block + rv = iter->PositionAt(cellElement); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINode> node; + do { + if (inIsShift) { + iter->Prev(); + } else { + iter->Next(); + } + + node = iter->GetCurrentNode(); + + if (node && HTMLEditUtils::IsTableCell(node) && + GetEnclosingTable(node) == table) { + CollapseSelectionToDeepestNonTableFirstChild(nullptr, node); + *outHandled = true; + return NS_OK; + } + } while (!iter->IsDone()); + + if (!(*outHandled) && !inIsShift) { + // If we haven't handled it yet, then we must have run off the end of the + // table. Insert a new row. + rv = InsertTableRow(1, true); + NS_ENSURE_SUCCESS(rv, rv); + *outHandled = true; + // Put selection in right place. Use table code to get selection and index + // to new row... + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> tblElement, cell; + int32_t row; + rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(tblElement), + getter_AddRefs(cell), + nullptr, nullptr, + &row, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + // ...so that we can ask for first cell in that row... + rv = GetCellAt(tblElement, row, 0, getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + // ...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) { + selection->Collapse(cell, 0); + } + } + + return NS_OK; +} + +already_AddRefed<Element> +HTMLEditor::CreateBR(nsINode* aNode, + int32_t aOffset, + EDirection aSelect) +{ + nsCOMPtr<nsIDOMNode> parent = GetAsDOMNode(aNode); + int32_t offset = aOffset; + nsCOMPtr<nsIDOMNode> outBRNode; + // We assume everything is fine if the br is not null, irrespective of retval + CreateBRImpl(address_of(parent), &offset, address_of(outBRNode), aSelect); + nsCOMPtr<Element> ret = do_QueryInterface(outBRNode); + return ret.forget(); +} + +NS_IMETHODIMP +HTMLEditor::CreateBR(nsIDOMNode* aNode, + int32_t aOffset, + nsCOMPtr<nsIDOMNode>* outBRNode, + EDirection aSelect) +{ + nsCOMPtr<nsIDOMNode> parent = aNode; + int32_t offset = aOffset; + return CreateBRImpl(address_of(parent), &offset, outBRNode, aSelect); +} + +void +HTMLEditor::CollapseSelectionToDeepestNonTableFirstChild(Selection* aSelection, + nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + RefPtr<Selection> selection = aSelection; + if (!selection) { + selection = GetSelection(); + } + if (!selection) { + // Nothing to do + return; + } + + nsCOMPtr<nsINode> node = aNode; + + for (nsCOMPtr<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) || !IsContainer(child)) { + break; + } + node = child; + } + + selection->Collapse(node, 0); +} + + +/** + * This is mostly like InsertHTMLWithCharsetAndContext, but we can't use that + * because it is selection-based and the rules code won't let us edit under the + * <head> node + */ +NS_IMETHODIMP +HTMLEditor::ReplaceHeadContentsWithHTML(const nsAString& aSourceToInsert) +{ + // don't do any post processing, rules get confused + AutoRules beginRulesSniffing(this, EditAction::ignore, nsIEditor::eNone); + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + ForceCompositionEnd(); + + // Do not use AutoRules -- rules code won't let us insert in <head>. Use + // the head node as a parent and delete/insert directly. + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + + RefPtr<nsContentList> nodeList = + doc->GetElementsByTagName(NS_LITERAL_STRING("head")); + NS_ENSURE_TRUE(nodeList, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIContent> headNode = nodeList->Item(0); + NS_ENSURE_TRUE(headNode, NS_ERROR_NULL_POINTER); + + // 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); // hope this does copy-on-write + + // Windows linebreaks: Map CRLF to LF: + inputString.ReplaceSubstring(u"\r\n", u"\n"); + + // Mac linebreaks: Map any remaining CR to LF: + inputString.ReplaceSubstring(u"\r", u"\n"); + + AutoEditBatch beginBatching(this); + + // Get the first range in the selection, for context: + RefPtr<nsRange> range = selection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER); + + ErrorResult err; + RefPtr<DocumentFragment> docfrag = + range->CreateContextualFragment(inputString, err); + + // 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 (err.Failed()) { +#ifdef DEBUG + printf("Couldn't create contextual fragment: error was %X\n", + err.ErrorCodeAsInt()); +#endif + return err.StealNSResult(); + } + NS_ENSURE_TRUE(docfrag, NS_ERROR_NULL_POINTER); + + // First delete all children in head + while (nsCOMPtr<nsIContent> child = headNode->GetFirstChild()) { + nsresult rv = DeleteNode(child); + NS_ENSURE_SUCCESS(rv, 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 = docfrag->GetFirstChild()) { + nsresult rv = InsertNode(*child, *headNode, offsetOfNewNode++); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RebuildDocumentFromSource(const nsAString& aSourceString) +{ + ForceCompositionEnd(); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + nsCOMPtr<Element> bodyElement = GetRoot(); + NS_ENSURE_TRUE(bodyElement, 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(NS_LITERAL_STRING("<body"), + beginbody, endbody); + + nsReadingIterator<char16_t> beginhead; + nsReadingIterator<char16_t> endhead; + aSourceString.BeginReading(beginhead); + aSourceString.EndReading(endhead); + bool foundhead = CaseInsensitiveFindInReadable(NS_LITERAL_STRING("<head"), + 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( + NS_LITERAL_STRING("</head>"), 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 + AutoEditBatch beginBatching(this); + + nsReadingIterator<char16_t> endtotal; + aSourceString.EndReading(endtotal); + + if (foundhead) { + if (foundclosehead) { + nsresult rv = + ReplaceHeadContentsWithHTML(Substring(beginhead, beginclosehead)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (foundbody) { + nsresult rv = + ReplaceHeadContentsWithHTML(Substring(beginhead, beginbody)); + if (NS_WARN_IF(NS_FAILED(rv))) { + 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 = ReplaceHeadContentsWithHTML(Substring(beginhead, endtotal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } else { + nsReadingIterator<char16_t> begintotal; + aSourceString.BeginReading(begintotal); + NS_NAMED_LITERAL_STRING(head, "<head>"); + if (foundclosehead) { + nsresult rv = + ReplaceHeadContentsWithHTML(head + Substring(begintotal, + beginclosehead)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (foundbody) { + nsresult rv = ReplaceHeadContentsWithHTML(head + Substring(begintotal, + beginbody)); + if (NS_WARN_IF(NS_FAILED(rv))) { + 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 = ReplaceHeadContentsWithHTML(head); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + nsresult rv = SelectAll(); + NS_ENSURE_SUCCESS(rv, rv); + + if (!foundbody) { + NS_NAMED_LITERAL_STRING(body, "<body>"); + // XXX Without recourse to some parser/content sink/docshell hackery we + // don't really know where the head ends and the body begins + if (foundclosehead) { + // assume body starts after the head ends + nsresult rv = LoadHTML(body + Substring(endclosehead, endtotal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (foundhead) { + // assume there is no body + nsresult rv = LoadHTML(body); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // assume there is no head, the entire source is body + nsresult rv = LoadHTML(body + aSourceString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsCOMPtr<Element> divElement = + CreateElementWithDefaults(NS_LITERAL_STRING("div")); + NS_ENSURE_TRUE(divElement, NS_ERROR_FAILURE); + + CloneAttributes(bodyElement, divElement); + + return BeginningOfDocument(); + } + + rv = LoadHTML(Substring(beginbody, endtotal)); + NS_ENSURE_SUCCESS(rv, rv); + + // Now we must copy attributes user might have edited on the <body> tag + // because InsertHTML (actually, CreateContextualFragment()) will never + // return a body node in the DOM fragment + + // We already know where "<body" begins + nsReadingIterator<char16_t> beginclosebody = beginbody; + nsReadingIterator<char16_t> endclosebody; + aSourceString.EndReading(endclosebody); + if (!FindInReadable(NS_LITERAL_STRING(">"), beginclosebody, endclosebody)) { + return NS_ERROR_FAILURE; + } + + // Truncate at the end of the body tag. Kludge of the year: fool the parser + // by replacing "body" with "div" so we get a node + nsAutoString bodyTag; + bodyTag.AssignLiteral("<div "); + bodyTag.Append(Substring(endbody, endclosebody)); + + RefPtr<nsRange> range = selection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + ErrorResult erv; + RefPtr<DocumentFragment> docfrag = + range->CreateContextualFragment(bodyTag, erv); + NS_ENSURE_TRUE(!erv.Failed(), erv.StealNSResult()); + NS_ENSURE_TRUE(docfrag, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIContent> child = docfrag->GetFirstChild(); + NS_ENSURE_TRUE(child && child->IsElement(), NS_ERROR_NULL_POINTER); + + // Copy all attributes from the div child to current body element + CloneAttributes(bodyElement, child->AsElement()); + + // place selection at first editable content + return BeginningOfDocument(); +} + +void +HTMLEditor::NormalizeEOLInsertPosition(nsINode* firstNodeToInsert, + nsCOMPtr<nsIDOMNode>* insertParentNode, + int32_t* insertOffset) +{ + /* + This function will either correct the position passed in, + or leave the position unchanged. + + When the (first) item to insert is a block level element, + and our insertion position is after the last visible item in a line, + i.e. the insertion position is just before a visible line break <br>, + we want to skip to the position just after the line break (see bug 68767) + + However, our logic to detect whether we should skip or not + needs to be more clever. + We must not skip when the caret appears to be positioned at the beginning + of a block, in that case skipping the <br> would not insert the <br> + at the caret position, but after the current empty line. + + So we have several cases to test: + + 1) We only ever want to skip, if the next visible thing after the current position is a break + + 2) We do not want to skip if there is no previous visible thing at all + That is detected if the call to PriorVisibleNode gives us an offset of zero. + Because PriorVisibleNode always positions after the prior node, we would + see an offset > 0, if there were a prior node. + + 3) We do not want to skip, if both the next and the previous visible things are breaks. + + 4) We do not want to skip if the previous visible thing is in a different block + than the insertion position. + */ + + if (!IsBlockNode(firstNodeToInsert)) { + return; + } + + WSRunObject wsObj(this, *insertParentNode, *insertOffset); + nsCOMPtr<nsINode> nextVisNode, prevVisNode; + int32_t nextVisOffset=0; + WSType nextVisType; + int32_t prevVisOffset=0; + WSType prevVisType; + + nsCOMPtr<nsINode> parent(do_QueryInterface(*insertParentNode)); + wsObj.NextVisibleNode(parent, *insertOffset, address_of(nextVisNode), &nextVisOffset, &nextVisType); + if (!nextVisNode) { + return; + } + + if (!(nextVisType & WSType::br)) { + return; + } + + wsObj.PriorVisibleNode(parent, *insertOffset, address_of(prevVisNode), &prevVisOffset, &prevVisType); + if (!prevVisNode) { + return; + } + + if (prevVisType & WSType::br) { + return; + } + + if (prevVisType & WSType::thisBlock) { + return; + } + + int32_t brOffset=0; + nsCOMPtr<nsIDOMNode> brNode = GetNodeLocation(GetAsDOMNode(nextVisNode), &brOffset); + + *insertParentNode = brNode; + *insertOffset = brOffset + 1; +} + +NS_IMETHODIMP +HTMLEditor::InsertElementAtSelection(nsIDOMElement* aElement, + bool aDeleteSelection) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMNode> node = do_QueryInterface(aElement); + + ForceCompositionEnd(); + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertElement, + nsIEditor::eNext); + + RefPtr<Selection> selection = GetSelection(); + if (!selection) { + return NS_ERROR_FAILURE; + } + + // hand off to the rules system, see if it has anything to say about this + bool cancel, handled; + TextRulesInfo ruleInfo(EditAction::insertElement); + ruleInfo.insertElement = aElement; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + + if (!handled) { + if (aDeleteSelection) { + if (!IsBlockNode(element)) { + // E.g., inserting an image. In this case we don't need to delete any + // inline wrappers before we do the insertion. Otherwise we let + // DeleteSelectionAndPrepareToCreateNode do the deletion for us, which + // calls DeleteSelection with aStripWrappers = eStrip. + rv = DeleteSelection(nsIEditor::eNone, nsIEditor::eNoStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsresult rv = DeleteSelectionAndPrepareToCreateNode(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // If deleting, selection will be collapsed. + // so if not, we collapse it + if (!aDeleteSelection) { + // Named Anchor is a special case, + // We collapse to insert element BEFORE the selection + // For all other tags, we insert AFTER the selection + if (HTMLEditUtils::IsNamedAnchor(node)) { + selection->CollapseToStart(); + } else { + selection->CollapseToEnd(); + } + } + + nsCOMPtr<nsIDOMNode> parentSelectedNode; + int32_t offsetForInsert; + rv = selection->GetAnchorNode(getter_AddRefs(parentSelectedNode)); + // XXX: ERROR_HANDLING bad XPCOM usage + if (NS_SUCCEEDED(rv) && + NS_SUCCEEDED(selection->GetAnchorOffset(&offsetForInsert)) && + parentSelectedNode) { + // Adjust position based on the node we are going to insert. + NormalizeEOLInsertPosition(element, address_of(parentSelectedNode), + &offsetForInsert); + + rv = InsertNodeAtPoint(node, address_of(parentSelectedNode), + &offsetForInsert, false); + NS_ENSURE_SUCCESS(rv, rv); + // Set caret after element, but check for special case + // of inserting table-related elements: set in first cell instead + if (!SetCaretInTableCell(aElement)) { + rv = SetCaretAfterElement(aElement); + NS_ENSURE_SUCCESS(rv, rv); + } + // check for inserting a whole table at the end of a block. If so insert a br after it. + if (HTMLEditUtils::IsTable(node)) { + bool isLast; + rv = IsLastEditableChild(node, &isLast); + NS_ENSURE_SUCCESS(rv, rv); + if (isLast) { + nsCOMPtr<nsIDOMNode> brNode; + rv = CreateBR(parentSelectedNode, offsetForInsert + 1, + address_of(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + selection->Collapse(parentSelectedNode, offsetForInsert+1); + } + } + } + } + rv = rules->DidDoAction(selection, &ruleInfo, rv); + return rv; +} + + +/** + * InsertNodeAtPoint() attempts to insert aNode into the document, at a point + * specified by {*ioParent,*ioOffset}. Checks with strict dtd to see if + * containment is allowed. If not allowed, will attempt to find a parent in + * the parent hierarchy of *ioParent that will accept aNode as a child. If + * such a parent is found, will split the document tree from + * {*ioParent,*ioOffset} up to parent, and then insert aNode. + * ioParent & ioOffset are then adjusted to point to the actual location that + * aNode was inserted at. aNoEmptyNodes specifies if the splitting process + * is allowed to reslt in empty nodes. + * + * @param aNode Node to insert. + * @param ioParent Insertion parent. + * @param ioOffset Insertion offset. + * @param aNoEmptyNodes Splitting can result in empty nodes? + */ +nsresult +HTMLEditor::InsertNodeAtPoint(nsIDOMNode* aNode, + nsCOMPtr<nsIDOMNode>* ioParent, + int32_t* ioOffset, + bool aNoEmptyNodes) +{ + nsCOMPtr<nsIContent> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(ioParent, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(*ioParent, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(ioOffset, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIContent> parent = do_QueryInterface(*ioParent); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIContent> topChild = parent; + nsCOMPtr<nsIContent> origParent = parent; + + // Search up the parent chain to find a suitable container + while (!CanContain(*parent, *node)) { + // If the current parent is a root (body or table element) + // then go no further - we can't insert + if (parent->IsHTMLElement(nsGkAtoms::body) || + HTMLEditUtils::IsTableElement(parent)) { + return NS_ERROR_FAILURE; + } + // Get the next parent + NS_ENSURE_TRUE(parent->GetParentNode(), NS_ERROR_FAILURE); + if (!IsEditable(parent->GetParentNode())) { + // There's no suitable place to put the node in this editing host. Maybe + // someone is trying to put block content in a span. So just put it + // where we were originally asked. + parent = topChild = origParent; + break; + } + topChild = parent; + parent = parent->GetParent(); + } + if (parent != topChild) { + // we need to split some levels above the original selection parent + int32_t offset = SplitNodeDeep(*topChild, *origParent, *ioOffset, + aNoEmptyNodes ? EmptyContainers::no + : EmptyContainers::yes); + NS_ENSURE_STATE(offset != -1); + *ioParent = GetAsDOMNode(parent); + *ioOffset = offset; + } + // Now we can insert the new node + return InsertNode(*node, *parent, *ioOffset); +} + +NS_IMETHODIMP +HTMLEditor::SelectElement(nsIDOMElement* aElement) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_STATE(element || !aElement); + + // Must be sure that element is contained in the document body + if (!IsDescendantOfEditorRoot(element)) { + return NS_ERROR_NULL_POINTER; + } + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIDOMNode>parent; + nsresult rv = aElement->GetParentNode(getter_AddRefs(parent)); + if (NS_SUCCEEDED(rv) && parent) { + int32_t offsetInParent = GetChildOffset(aElement, parent); + + // Collapse selection to just before desired element, + rv = selection->Collapse(parent, offsetInParent); + if (NS_SUCCEEDED(rv)) { + // then extend it to just after + rv = selection->Extend(parent, offsetInParent + 1); + } + } + return rv; +} + +NS_IMETHODIMP +HTMLEditor::SetCaretAfterElement(nsIDOMElement* aElement) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_STATE(element || !aElement); + + // Be sure the element is contained in the document body + if (!aElement || !IsDescendantOfEditorRoot(element)) { + return NS_ERROR_NULL_POINTER; + } + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIDOMNode>parent; + nsresult rv = aElement->GetParentNode(getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + int32_t offsetInParent = GetChildOffset(aElement, parent); + // Collapse selection to just after desired element, + return selection->Collapse(parent, offsetInParent + 1); +} + +NS_IMETHODIMP +HTMLEditor::SetParagraphFormat(const nsAString& aParagraphFormat) +{ + nsAutoString tag; tag.Assign(aParagraphFormat); + ToLowerCase(tag); + if (tag.EqualsLiteral("dd") || tag.EqualsLiteral("dt")) { + return MakeDefinitionItem(tag); + } + return InsertBasicBlock(tag); +} + +NS_IMETHODIMP +HTMLEditor::GetParagraphState(bool* aMixed, + nsAString& outFormat) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER); + RefPtr<HTMLEditRules> htmlRules = + static_cast<HTMLEditRules*>(mRules.get()); + + return htmlRules->GetParagraphState(aMixed, outFormat); +} + +NS_IMETHODIMP +HTMLEditor::GetBackgroundColorState(bool* aMixed, + nsAString& aOutColor) +{ + if (IsCSSEnabled()) { + // if we are in CSS mode, we have to check if the containing block defines + // a background color + return GetCSSBackgroundColorState(aMixed, aOutColor, true); + } + // in HTML mode, we look only at page's background + return GetHTMLBackgroundColorState(aMixed, aOutColor); +} + +NS_IMETHODIMP +HTMLEditor::GetHighlightColorState(bool* aMixed, + nsAString& aOutColor) +{ + *aMixed = false; + aOutColor.AssignLiteral("transparent"); + if (!IsCSSEnabled()) { + return NS_OK; + } + + // in CSS mode, text background can be added by the Text Highlight button + // we need to query the background of the selection without looking for + // the block container of the ranges in the selection + return GetCSSBackgroundColorState(aMixed, aOutColor, false); +} + +nsresult +HTMLEditor::GetCSSBackgroundColorState(bool* aMixed, + nsAString& aOutColor, + bool aBlockLevel) +{ + NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER); + *aMixed = false; + // the default background color is transparent + aOutColor.AssignLiteral("transparent"); + + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection && selection->GetRangeAt(0)); + + // get selection location + nsCOMPtr<nsINode> parent = selection->GetRangeAt(0)->GetStartParent(); + int32_t offset = selection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + + // is the selection collapsed? + nsCOMPtr<nsINode> nodeToExamine; + if (selection->Collapsed() || IsTextNode(parent)) { + // we want to look at the parent and ancestors + nodeToExamine = parent; + } else { + // otherwise we want to look at the first editable node after + // {parent,offset} and its ancestors for divs with alignment on them + nodeToExamine = parent->GetChildAt(offset); + //GetNextNode(parent, offset, true, address_of(nodeToExamine)); + } + + NS_ENSURE_TRUE(nodeToExamine, NS_ERROR_NULL_POINTER); + + if (aBlockLevel) { + // we are querying the block background (and not the text background), let's + // climb to the block container + nsCOMPtr<Element> blockParent = GetBlock(*nodeToExamine); + NS_ENSURE_TRUE(blockParent, NS_OK); + + // Make sure to not walk off onto the Document node + do { + // retrieve the computed style of background-color for blockParent + mCSSEditUtils->GetComputedProperty(*blockParent, + *nsGkAtoms::backgroundColor, + aOutColor); + blockParent = blockParent->GetParentElement(); + // look at parent if the queried color is transparent and if the node to + // examine is not the root of the document + } while (aOutColor.EqualsLiteral("transparent") && blockParent); + if (aOutColor.EqualsLiteral("transparent")) { + // we have hit the root of the document and the color is still transparent ! + // Grumble... Let's look at the default background color because that's the + // color we are looking for + mCSSEditUtils->GetDefaultBackgroundColor(aOutColor); + } + } + else { + // no, we are querying the text background for the Text Highlight button + if (IsTextNode(nodeToExamine)) { + // if the node of interest is a text node, let's climb a level + nodeToExamine = nodeToExamine->GetParentNode(); + } + do { + // is the node to examine a block ? + if (NodeIsBlockStatic(nodeToExamine)) { + // yes it is a block; in that case, the text background color is transparent + aOutColor.AssignLiteral("transparent"); + break; + } else { + // no, it's not; let's retrieve the computed style of background-color for the + // node to examine + mCSSEditUtils->GetComputedProperty(*nodeToExamine, + *nsGkAtoms::backgroundColor, + aOutColor); + if (!aOutColor.EqualsLiteral("transparent")) { + break; + } + } + nodeToExamine = nodeToExamine->GetParentNode(); + } while ( aOutColor.EqualsLiteral("transparent") && nodeToExamine ); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetHTMLBackgroundColorState(bool* aMixed, + nsAString& aOutColor) +{ + //TODO: We don't handle "mixed" correctly! + NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER); + *aMixed = false; + aOutColor.Truncate(); + + nsCOMPtr<nsIDOMElement> domElement; + int32_t selectedCount; + nsAutoString tagName; + nsresult rv = GetSelectedOrParentTableElement(tagName, + &selectedCount, + getter_AddRefs(domElement)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<dom::Element> element = do_QueryInterface(domElement); + + while (element) { + // We are in a cell or selected table + element->GetAttr(kNameSpaceID_None, nsGkAtoms::bgcolor, aOutColor); + + // Done if we have a color explicitly set + if (!aOutColor.IsEmpty()) { + return NS_OK; + } + + // Once we hit the body, we're done + if (element->IsHTMLElement(nsGkAtoms::body)) { + return NS_OK; + } + + // No color is set, but we need to report visible color inherited + // from nested cells/tables, so search up parent chain + element = element->GetParentElement(); + } + + // If no table or cell found, get page body + dom::Element* bodyElement = GetRoot(); + NS_ENSURE_TRUE(bodyElement, NS_ERROR_NULL_POINTER); + + bodyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::bgcolor, aOutColor); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetListState(bool* aMixed, + bool* aOL, + bool* aUL, + bool* aDL) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + NS_ENSURE_TRUE(aMixed && aOL && aUL && aDL, NS_ERROR_NULL_POINTER); + RefPtr<HTMLEditRules> htmlRules = + static_cast<HTMLEditRules*>(mRules.get()); + + return htmlRules->GetListState(aMixed, aOL, aUL, aDL); +} + +NS_IMETHODIMP +HTMLEditor::GetListItemState(bool* aMixed, + bool* aLI, + bool* aDT, + bool* aDD) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + NS_ENSURE_TRUE(aMixed && aLI && aDT && aDD, NS_ERROR_NULL_POINTER); + + RefPtr<HTMLEditRules> htmlRules = + static_cast<HTMLEditRules*>(mRules.get()); + + return htmlRules->GetListItemState(aMixed, aLI, aDT, aDD); +} + +NS_IMETHODIMP +HTMLEditor::GetAlignment(bool* aMixed, + nsIHTMLEditor::EAlignment* aAlign) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + NS_ENSURE_TRUE(aMixed && aAlign, NS_ERROR_NULL_POINTER); + RefPtr<HTMLEditRules> htmlRules = + static_cast<HTMLEditRules*>(mRules.get()); + + return htmlRules->GetAlignment(aMixed, aAlign); +} + +NS_IMETHODIMP +HTMLEditor::GetIndentState(bool* aCanIndent, + bool* aCanOutdent) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + NS_ENSURE_TRUE(aCanIndent && aCanOutdent, NS_ERROR_NULL_POINTER); + + RefPtr<HTMLEditRules> htmlRules = + static_cast<HTMLEditRules*>(mRules.get()); + + return htmlRules->GetIndentState(aCanIndent, aCanOutdent); +} + +NS_IMETHODIMP +HTMLEditor::MakeOrChangeList(const nsAString& aListType, + bool entireList, + const nsAString& aBulletType) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + bool cancel, handled; + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::makeList, nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + TextRulesInfo ruleInfo(EditAction::makeList); + ruleInfo.blockType = &aListType; + ruleInfo.entireList = entireList; + ruleInfo.bulletType = &aBulletType; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + + if (!handled) { + // Find out if the selection is collapsed: + bool isCollapsed = selection->Collapsed(); + + NS_ENSURE_TRUE(selection->GetRangeAt(0) && + selection->GetRangeAt(0)->GetStartParent() && + selection->GetRangeAt(0)->GetStartParent()->IsContent(), + NS_ERROR_FAILURE); + OwningNonNull<nsIContent> node = + *selection->GetRangeAt(0)->GetStartParent()->AsContent(); + int32_t offset = selection->GetRangeAt(0)->StartOffset(); + + if (isCollapsed) { + // have to find a place to put the list + nsCOMPtr<nsIContent> parent = node; + nsCOMPtr<nsIContent> topChild = node; + + nsCOMPtr<nsIAtom> listAtom = NS_Atomize(aListType); + while (!CanContainTag(*parent, *listAtom)) { + topChild = parent; + parent = parent->GetParent(); + } + + if (parent != node) { + // we need to split up to the child of parent + offset = SplitNodeDeep(*topChild, *node, offset); + NS_ENSURE_STATE(offset != -1); + } + + // make a list + nsCOMPtr<Element> newList = CreateNode(listAtom, parent, offset); + NS_ENSURE_STATE(newList); + // make a list item + nsCOMPtr<Element> newItem = CreateNode(nsGkAtoms::li, newList, 0); + NS_ENSURE_STATE(newItem); + rv = selection->Collapse(newItem, 0); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +HTMLEditor::RemoveList(const nsAString& aListType) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + bool cancel, handled; + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::removeList, nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + TextRulesInfo ruleInfo(EditAction::removeList); + if (aListType.LowerCaseEqualsLiteral("ol")) { + ruleInfo.bOrdered = true; + } else { + ruleInfo.bOrdered = false; + } + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + + // no default behavior for this yet. what would it mean? + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +nsresult +HTMLEditor::MakeDefinitionItem(const nsAString& aItemType) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + bool cancel, handled; + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::makeDefListItem, + nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + TextRulesInfo ruleInfo(EditAction::makeDefListItem); + ruleInfo.blockType = &aItemType; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + + if (!handled) { + // todo: no default for now. we count on rules to handle it. + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +nsresult +HTMLEditor::InsertBasicBlock(const nsAString& aBlockType) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + bool cancel, handled; + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::makeBasicBlock, + nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + TextRulesInfo ruleInfo(EditAction::makeBasicBlock); + ruleInfo.blockType = &aBlockType; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + + if (!handled) { + // Find out if the selection is collapsed: + bool isCollapsed = selection->Collapsed(); + + NS_ENSURE_TRUE(selection->GetRangeAt(0) && + selection->GetRangeAt(0)->GetStartParent() && + selection->GetRangeAt(0)->GetStartParent()->IsContent(), + NS_ERROR_FAILURE); + OwningNonNull<nsIContent> node = + *selection->GetRangeAt(0)->GetStartParent()->AsContent(); + int32_t offset = selection->GetRangeAt(0)->StartOffset(); + + if (isCollapsed) { + // have to find a place to put the block + nsCOMPtr<nsIContent> parent = node; + nsCOMPtr<nsIContent> topChild = node; + + nsCOMPtr<nsIAtom> blockAtom = NS_Atomize(aBlockType); + while (!CanContainTag(*parent, *blockAtom)) { + NS_ENSURE_TRUE(parent->GetParent(), NS_ERROR_FAILURE); + topChild = parent; + parent = parent->GetParent(); + } + + if (parent != node) { + // we need to split up to the child of parent + offset = SplitNodeDeep(*topChild, *node, offset); + NS_ENSURE_STATE(offset != -1); + } + + // make a block + nsCOMPtr<Element> newBlock = CreateNode(blockAtom, parent, offset); + NS_ENSURE_STATE(newBlock); + + // reposition selection to inside the block + rv = selection->Collapse(newBlock, 0); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +HTMLEditor::Indent(const nsAString& aIndent) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + bool cancel, handled; + EditAction opID = EditAction::indent; + if (aIndent.LowerCaseEqualsLiteral("outdent")) { + opID = EditAction::outdent; + } + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, opID, nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + TextRulesInfo ruleInfo(opID); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + + if (!handled) { + // Do default - insert a blockquote node if selection collapsed + bool isCollapsed = selection->Collapsed(); + + NS_ENSURE_TRUE(selection->GetRangeAt(0) && + selection->GetRangeAt(0)->GetStartParent() && + selection->GetRangeAt(0)->GetStartParent()->IsContent(), + NS_ERROR_FAILURE); + OwningNonNull<nsIContent> node = + *selection->GetRangeAt(0)->GetStartParent()->AsContent(); + int32_t offset = selection->GetRangeAt(0)->StartOffset(); + + if (aIndent.EqualsLiteral("indent")) { + if (isCollapsed) { + // have to find a place to put the blockquote + nsCOMPtr<nsIContent> parent = node; + nsCOMPtr<nsIContent> topChild = node; + while (!CanContainTag(*parent, *nsGkAtoms::blockquote)) { + NS_ENSURE_TRUE(parent->GetParent(), NS_ERROR_FAILURE); + topChild = parent; + parent = parent->GetParent(); + } + + if (parent != node) { + // we need to split up to the child of parent + offset = SplitNodeDeep(*topChild, *node, offset); + NS_ENSURE_STATE(offset != -1); + } + + // make a blockquote + nsCOMPtr<Element> newBQ = CreateNode(nsGkAtoms::blockquote, parent, offset); + NS_ENSURE_STATE(newBQ); + // put a space in it so layout will draw the list item + rv = selection->Collapse(newBQ, 0); + NS_ENSURE_SUCCESS(rv, rv); + rv = InsertText(NS_LITERAL_STRING(" ")); + NS_ENSURE_SUCCESS(rv, rv); + // reposition selection to before the space character + NS_ENSURE_STATE(selection->GetRangeAt(0)); + rv = selection->Collapse(selection->GetRangeAt(0)->GetStartParent(), 0); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +//TODO: IMPLEMENT ALIGNMENT! + +NS_IMETHODIMP +HTMLEditor::Align(const nsAString& aAlignType) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::align, nsIEditor::eNext); + + bool cancel, handled; + + // Find out if the selection is collapsed: + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + TextRulesInfo ruleInfo(EditAction::align); + ruleInfo.alignType = &aAlignType; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +already_AddRefed<Element> +HTMLEditor::GetElementOrParentByTagName(const nsAString& aTagName, + nsINode* aNode) +{ + MOZ_ASSERT(!aTagName.IsEmpty()); + + nsCOMPtr<nsINode> node = aNode; + if (!node) { + // If no node supplied, get it from anchor node of current selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, nullptr); + + nsCOMPtr<nsINode> anchorNode = selection->GetAnchorNode(); + NS_ENSURE_TRUE(anchorNode, nullptr); + + // Try to get the actual selected node + if (anchorNode->HasChildNodes() && anchorNode->IsContent()) { + node = anchorNode->GetChildAt(selection->AnchorOffset()); + } + // Anchor node is probably a text node - just use that + if (!node) { + node = anchorNode; + } + } + + nsCOMPtr<Element> current; + if (node->IsElement()) { + current = node->AsElement(); + } else if (node->GetParentElement()) { + current = node->GetParentElement(); + } else { + // Neither aNode nor its parent is an element, so no ancestor is + MOZ_ASSERT(!node->GetParentNode() || + !node->GetParentNode()->GetParentNode()); + return nullptr; + } + + nsAutoString tagName(aTagName); + ToLowerCase(tagName); + bool getLink = IsLinkTag(tagName); + bool getNamedAnchor = IsNamedAnchorTag(tagName); + if (getLink || getNamedAnchor) { + tagName.Assign('a'); + } + bool findTableCell = tagName.EqualsLiteral("td"); + bool findList = tagName.EqualsLiteral("list"); + + for (; current; current = current->GetParentElement()) { + // Test if we have a link (an anchor with href set) + if ((getLink && HTMLEditUtils::IsLink(current)) || + (getNamedAnchor && HTMLEditUtils::IsNamedAnchor(current))) { + return current.forget(); + } + if (findList) { + // Match "ol", "ul", or "dl" for lists + if (HTMLEditUtils::IsList(current)) { + return current.forget(); + } + } else if (findTableCell) { + // Table cells are another special case: match either "td" or "th" + if (HTMLEditUtils::IsTableCell(current)) { + return current.forget(); + } + } else if (current->NodeName().Equals(tagName, + nsCaseInsensitiveStringComparator())) { + return current.forget(); + } + + // Stop searching if parent is a body tag. Note: Originally used IsRoot to + // stop at table cells, but that's too messy when you are trying to find + // the parent table + if (current->GetParentElement() && + current->GetParentElement()->IsHTMLElement(nsGkAtoms::body)) { + break; + } + } + + return nullptr; +} + +NS_IMETHODIMP +HTMLEditor::GetElementOrParentByTagName(const nsAString& aTagName, + nsIDOMNode* aNode, + nsIDOMElement** aReturn) +{ + NS_ENSURE_TRUE(!aTagName.IsEmpty(), NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(aReturn, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + nsCOMPtr<Element> parent = + GetElementOrParentByTagName(aTagName, node); + nsCOMPtr<nsIDOMElement> ret = do_QueryInterface(parent); + + if (!ret) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + ret.forget(aReturn); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetSelectedElement(const nsAString& aTagName, + nsIDOMElement** aReturn) +{ + NS_ENSURE_TRUE(aReturn , NS_ERROR_NULL_POINTER); + + // default is null - no element found + *aReturn = nullptr; + + // First look for a single element in selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + bool bNodeFound = false; + bool isCollapsed = selection->Collapsed(); + + nsAutoString domTagName; + nsAutoString TagName(aTagName); + ToLowerCase(TagName); + // Empty string indicates we should match any element tag + bool anyTag = (TagName.IsEmpty()); + bool isLinkTag = IsLinkTag(TagName); + bool isNamedAnchorTag = IsNamedAnchorTag(TagName); + + nsCOMPtr<nsIDOMElement> selectedElement; + RefPtr<nsRange> range = selection->GetRangeAt(0); + NS_ENSURE_STATE(range); + + nsCOMPtr<nsIDOMNode> startParent; + int32_t startOffset, endOffset; + nsresult rv = range->GetStartContainer(getter_AddRefs(startParent)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetStartOffset(&startOffset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> endParent; + rv = range->GetEndContainer(getter_AddRefs(endParent)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->GetEndOffset(&endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // Optimization for a single selected element + if (startParent && startParent == endParent && endOffset - startOffset == 1) { + nsCOMPtr<nsIDOMNode> selectedNode = GetChildAt(startParent, startOffset); + NS_ENSURE_SUCCESS(rv, NS_OK); + if (selectedNode) { + selectedNode->GetNodeName(domTagName); + ToLowerCase(domTagName); + + // Test for appropriate node type requested + if (anyTag || (TagName == domTagName) || + (isLinkTag && HTMLEditUtils::IsLink(selectedNode)) || + (isNamedAnchorTag && HTMLEditUtils::IsNamedAnchor(selectedNode))) { + bNodeFound = true; + selectedElement = do_QueryInterface(selectedNode); + } + } + } + + if (!bNodeFound) { + if (isLinkTag) { + // Link tag is a special case - we return the anchor node + // found for any selection that is totally within a link, + // included a collapsed selection (just a caret in a link) + nsCOMPtr<nsIDOMNode> anchorNode; + rv = selection->GetAnchorNode(getter_AddRefs(anchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + int32_t anchorOffset = -1; + if (anchorNode) { + selection->GetAnchorOffset(&anchorOffset); + } + + nsCOMPtr<nsIDOMNode> focusNode; + rv = selection->GetFocusNode(getter_AddRefs(focusNode)); + NS_ENSURE_SUCCESS(rv, rv); + int32_t focusOffset = -1; + if (focusNode) { + selection->GetFocusOffset(&focusOffset); + } + + // Link node must be the same for both ends of selection + if (NS_SUCCEEDED(rv) && anchorNode) { + nsCOMPtr<nsIDOMElement> parentLinkOfAnchor; + rv = GetElementOrParentByTagName(NS_LITERAL_STRING("href"), + anchorNode, + getter_AddRefs(parentLinkOfAnchor)); + // XXX: ERROR_HANDLING can parentLinkOfAnchor be null? + if (NS_SUCCEEDED(rv) && parentLinkOfAnchor) { + if (isCollapsed) { + // We have just a caret in the link + bNodeFound = true; + } else if (focusNode) { + // Link node must be the same for both ends of selection. + nsCOMPtr<nsIDOMElement> parentLinkOfFocus; + rv = GetElementOrParentByTagName(NS_LITERAL_STRING("href"), + focusNode, + getter_AddRefs(parentLinkOfFocus)); + if (NS_SUCCEEDED(rv) && parentLinkOfFocus == parentLinkOfAnchor) { + bNodeFound = true; + } + } + + // We found a link node parent + if (bNodeFound) { + // GetElementOrParentByTagName addref'd this, so we don't need to do it here + *aReturn = parentLinkOfAnchor; + NS_IF_ADDREF(*aReturn); + return NS_OK; + } + } else if (anchorOffset >= 0) { + // Check if link node is the only thing selected + nsCOMPtr<nsIDOMNode> anchorChild; + anchorChild = GetChildAt(anchorNode,anchorOffset); + if (anchorChild && HTMLEditUtils::IsLink(anchorChild) && + anchorNode == focusNode && focusOffset == anchorOffset + 1) { + selectedElement = do_QueryInterface(anchorChild); + bNodeFound = true; + } + } + } + } + + if (!isCollapsed) { + RefPtr<nsRange> currange = selection->GetRangeAt(0); + if (currange) { + nsCOMPtr<nsIContentIterator> iter = + do_CreateInstance("@mozilla.org/content/post-content-iterator;1", + &rv); + NS_ENSURE_SUCCESS(rv, rv); + + iter->Init(currange); + // loop through the content iterator for each content node + while (!iter->IsDone()) { + // Query interface to cast nsIContent to nsIDOMNode + // then get tagType to compare to aTagName + // Clone node of each desired type and append it to the aDomFrag + selectedElement = do_QueryInterface(iter->GetCurrentNode()); + if (selectedElement) { + // If we already found a node, then we have another element, + // thus there's not just one element selected + if (bNodeFound) { + bNodeFound = false; + break; + } + + selectedElement->GetNodeName(domTagName); + ToLowerCase(domTagName); + + if (anyTag) { + // Get name of first selected element + selectedElement->GetTagName(TagName); + ToLowerCase(TagName); + anyTag = false; + } + + // The "A" tag is a pain, + // used for both link(href is set) and "Named Anchor" + nsCOMPtr<nsIDOMNode> selectedNode = do_QueryInterface(selectedElement); + if ((isLinkTag && + HTMLEditUtils::IsLink(selectedNode)) || + (isNamedAnchorTag && + HTMLEditUtils::IsNamedAnchor(selectedNode))) { + bNodeFound = true; + } else if (TagName == domTagName) { // All other tag names are handled here + bNodeFound = true; + } + if (!bNodeFound) { + // Check if node we have is really part of the selection??? + break; + } + } + iter->Next(); + } + } else { + // Should never get here? + isCollapsed = true; + NS_WARNING("isCollapsed was FALSE, but no elements found in selection\n"); + } + } + } + + if (!bNodeFound) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + *aReturn = selectedElement; + if (selectedElement) { + // Getters must addref + NS_ADDREF(*aReturn); + } + return rv; +} + +already_AddRefed<Element> +HTMLEditor::CreateElementWithDefaults(const nsAString& aTagName) +{ + MOZ_ASSERT(!aTagName.IsEmpty()); + + nsAutoString tagName(aTagName); + ToLowerCase(tagName); + nsAutoString realTagName; + + if (IsLinkTag(tagName) || IsNamedAnchorTag(tagName)) { + realTagName.Assign('a'); + } else { + realTagName = tagName; + } + // We don't use editor's CreateElement because we don't want to go through + // the transaction system + + // New call to use instead to get proper HTML element, bug 39919 + nsCOMPtr<nsIAtom> realTagAtom = NS_Atomize(realTagName); + nsCOMPtr<Element> newElement = CreateHTMLContent(realTagAtom); + if (!newElement) { + return nullptr; + } + + // Mark the new element dirty, so it will be formatted + ErrorResult rv; + newElement->SetAttribute(NS_LITERAL_STRING("_moz_dirty"), EmptyString(), rv); + + // Set default values for new elements + if (tagName.EqualsLiteral("table")) { + newElement->SetAttribute(NS_LITERAL_STRING("cellpadding"), + NS_LITERAL_STRING("2"), rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return nullptr; + } + newElement->SetAttribute(NS_LITERAL_STRING("cellspacing"), + NS_LITERAL_STRING("2"), rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return nullptr; + } + newElement->SetAttribute(NS_LITERAL_STRING("border"), + NS_LITERAL_STRING("1"), rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return nullptr; + } + } else if (tagName.EqualsLiteral("td")) { + nsresult rv = + SetAttributeOrEquivalent( + static_cast<nsIDOMElement*>(newElement->AsDOMNode()), + NS_LITERAL_STRING("valign"), NS_LITERAL_STRING("top"), true); + NS_ENSURE_SUCCESS(rv, nullptr); + } + // ADD OTHER TAGS HERE + + return newElement.forget(); +} + +NS_IMETHODIMP +HTMLEditor::CreateElementWithDefaults(const nsAString& aTagName, + nsIDOMElement** aReturn) +{ + NS_ENSURE_TRUE(!aTagName.IsEmpty() && aReturn, NS_ERROR_NULL_POINTER); + *aReturn = nullptr; + + nsCOMPtr<Element> newElement = CreateElementWithDefaults(aTagName); + nsCOMPtr<nsIDOMElement> ret = do_QueryInterface(newElement); + NS_ENSURE_TRUE(ret, NS_ERROR_FAILURE); + + ret.forget(aReturn); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::InsertLinkAroundSelection(nsIDOMElement* aAnchorElement) +{ + NS_ENSURE_TRUE(aAnchorElement, NS_ERROR_NULL_POINTER); + + // We must have a real selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + if (selection->Collapsed()) { + NS_WARNING("InsertLinkAroundSelection called but there is no selection!!!"); + return NS_OK; + } + + // Be sure we were given an anchor element + nsCOMPtr<nsIDOMHTMLAnchorElement> anchor = do_QueryInterface(aAnchorElement); + if (!anchor) { + return NS_OK; + } + + nsAutoString href; + nsresult rv = anchor->GetHref(href); + NS_ENSURE_SUCCESS(rv, rv); + if (href.IsEmpty()) { + return NS_OK; + } + + AutoEditBatch beginBatching(this); + + // Set all attributes found on the supplied anchor element + nsCOMPtr<nsIDOMMozNamedAttrMap> attrMap; + aAnchorElement->GetAttributes(getter_AddRefs(attrMap)); + NS_ENSURE_TRUE(attrMap, NS_ERROR_FAILURE); + + uint32_t count; + attrMap->GetLength(&count); + nsAutoString name, value; + + for (uint32_t i = 0; i < count; ++i) { + nsCOMPtr<nsIDOMAttr> attribute; + rv = attrMap->Item(i, getter_AddRefs(attribute)); + NS_ENSURE_SUCCESS(rv, rv); + + if (attribute) { + // We must clear the string buffers + // because GetName, GetValue appends to previous string! + name.Truncate(); + value.Truncate(); + + rv = attribute->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + rv = attribute->GetValue(value); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetInlineProperty(nsGkAtoms::a, name, value); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +nsresult +HTMLEditor::SetHTMLBackgroundColor(const nsAString& aColor) +{ + NS_PRECONDITION(mDocWeak, "Missing Editor DOM Document"); + + // Find a selected or enclosing table element to set background on + nsCOMPtr<nsIDOMElement> element; + int32_t selectedCount; + nsAutoString tagName; + nsresult rv = GetSelectedOrParentTableElement(tagName, &selectedCount, + getter_AddRefs(element)); + NS_ENSURE_SUCCESS(rv, rv); + + bool setColor = !aColor.IsEmpty(); + + NS_NAMED_LITERAL_STRING(bgcolor, "bgcolor"); + if (element) { + if (selectedCount > 0) { + // Traverse all selected cells + nsCOMPtr<nsIDOMElement> cell; + rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell)); + if (NS_SUCCEEDED(rv) && cell) { + while (cell) { + rv = setColor ? SetAttribute(cell, bgcolor, aColor) : + RemoveAttribute(cell, bgcolor); + if (NS_FAILED(rv)) { + return rv; + } + + GetNextSelectedCell(nullptr, getter_AddRefs(cell)); + } + return NS_OK; + } + } + // If we failed to find a cell, fall through to use originally-found element + } else { + // No table element -- set the background color on the body tag + element = do_QueryInterface(GetRoot()); + NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER); + } + // Use the editor method that goes through the transaction system + return setColor ? SetAttribute(element, bgcolor, aColor) : + RemoveAttribute(element, bgcolor); +} + +NS_IMETHODIMP +HTMLEditor::SetBodyAttribute(const nsAString& aAttribute, + const nsAString& aValue) +{ + // TODO: Check selection for Cell, Row, Column or table and do color on appropriate level + + NS_ASSERTION(mDocWeak, "Missing Editor DOM Document"); + + // Set the background color attribute on the body tag + nsCOMPtr<nsIDOMElement> bodyElement = do_QueryInterface(GetRoot()); + NS_ENSURE_TRUE(bodyElement, NS_ERROR_NULL_POINTER); + + // Use the editor method that goes through the transaction system + return SetAttribute(bodyElement, aAttribute, aValue); +} + +NS_IMETHODIMP +HTMLEditor::GetLinkedObjects(nsIArray** aNodeList) +{ + NS_ENSURE_TRUE(aNodeList, NS_ERROR_NULL_POINTER); + + nsresult rv; + nsCOMPtr<nsIMutableArray> nodes = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIContentIterator> iter = + do_CreateInstance("@mozilla.org/content/post-content-iterator;1", &rv); + NS_ENSURE_TRUE(iter, NS_ERROR_NULL_POINTER); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED); + + iter->Init(doc->GetRootElement()); + + // loop through the content iterator for each content node + while (!iter->IsDone()) { + nsCOMPtr<nsIDOMNode> node (do_QueryInterface(iter->GetCurrentNode())); + if (node) { + // Let nsURIRefObject make the hard decisions: + nsCOMPtr<nsIURIRefObject> refObject; + rv = NS_NewHTMLURIRefObject(getter_AddRefs(refObject), node); + if (NS_SUCCEEDED(rv)) { + nodes->AppendElement(refObject, false); + } + } + iter->Next(); + } + } + + nodes.forget(aNodeList); + return NS_OK; +} + + +NS_IMETHODIMP +HTMLEditor::AddStyleSheet(const nsAString& aURL) +{ + // Enable existing sheet if already loaded. + if (EnableExistingStyleSheet(aURL)) { + return NS_OK; + } + + // Lose the previously-loaded sheet so there's nothing to replace + // This pattern is different from Override methods because + // we must wait to remove mLastStyleSheetURL and add new sheet + // at the same time (in StyleSheetLoaded callback) so they are undoable together + mLastStyleSheetURL.Truncate(); + return ReplaceStyleSheet(aURL); +} + +NS_IMETHODIMP +HTMLEditor::ReplaceStyleSheet(const nsAString& aURL) +{ + // Enable existing sheet if already loaded. + if (EnableExistingStyleSheet(aURL)) { + // Disable last sheet if not the same as new one + if (!mLastStyleSheetURL.IsEmpty() && !mLastStyleSheetURL.Equals(aURL)) { + return EnableStyleSheet(mLastStyleSheetURL, false); + } + return NS_OK; + } + + // Make sure the pres shell doesn't disappear during the load. + NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED); + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsIURI> uaURI; + nsresult rv = NS_NewURI(getter_AddRefs(uaURI), aURL); + NS_ENSURE_SUCCESS(rv, rv); + + return ps->GetDocument()->CSSLoader()-> + LoadSheet(uaURI, false, nullptr, EmptyCString(), this); +} + +NS_IMETHODIMP +HTMLEditor::RemoveStyleSheet(const nsAString& aURL) +{ + RefPtr<StyleSheet> sheet = GetStyleSheetForURL(aURL); + NS_ENSURE_TRUE(sheet, NS_ERROR_UNEXPECTED); + + RefPtr<RemoveStyleSheetTransaction> transaction; + nsresult rv = + CreateTxnForRemoveStyleSheet(sheet, getter_AddRefs(transaction)); + if (!transaction) { + rv = NS_ERROR_NULL_POINTER; + } + if (NS_SUCCEEDED(rv)) { + rv = DoTransaction(transaction); + if (NS_SUCCEEDED(rv)) { + mLastStyleSheetURL.Truncate(); // forget it + } + // Remove it from our internal list + rv = RemoveStyleSheetFromList(aURL); + } + + return rv; +} + + +NS_IMETHODIMP +HTMLEditor::AddOverrideStyleSheet(const nsAString& aURL) +{ + // Enable existing sheet if already loaded. + if (EnableExistingStyleSheet(aURL)) { + return NS_OK; + } + + // Make sure the pres shell doesn't disappear during the load. + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsIURI> uaURI; + nsresult rv = NS_NewURI(getter_AddRefs(uaURI), aURL); + NS_ENSURE_SUCCESS(rv, rv); + + // We MUST ONLY load synchronous local files (no @import) + // XXXbz Except this will actually try to load remote files + // synchronously, of course.. + RefPtr<StyleSheet> sheet; + // Editor override style sheets may want to style Gecko anonymous boxes + rv = ps->GetDocument()->CSSLoader()-> + LoadSheetSync(uaURI, mozilla::css::eAgentSheetFeatures, true, + &sheet); + + // Synchronous loads should ALWAYS return completed + NS_ENSURE_TRUE(sheet, NS_ERROR_NULL_POINTER); + + // Add the override style sheet + // (This checks if already exists) + ps->AddOverrideStyleSheet(sheet); + + ps->RestyleForCSSRuleChanges(); + + // Save as the last-loaded sheet + mLastOverrideStyleSheetURL = aURL; + + //Add URL and style sheet to our lists + return AddNewStyleSheetToList(aURL, sheet); +} + +NS_IMETHODIMP +HTMLEditor::ReplaceOverrideStyleSheet(const nsAString& aURL) +{ + // Enable existing sheet if already loaded. + if (EnableExistingStyleSheet(aURL)) { + // Disable last sheet if not the same as new one + if (!mLastOverrideStyleSheetURL.IsEmpty() && + !mLastOverrideStyleSheetURL.Equals(aURL)) { + return EnableStyleSheet(mLastOverrideStyleSheetURL, false); + } + return NS_OK; + } + // Remove the previous sheet + if (!mLastOverrideStyleSheetURL.IsEmpty()) { + RemoveOverrideStyleSheet(mLastOverrideStyleSheetURL); + } + return AddOverrideStyleSheet(aURL); +} + +// Do NOT use transaction system for override style sheets +NS_IMETHODIMP +HTMLEditor::RemoveOverrideStyleSheet(const nsAString& aURL) +{ + RefPtr<StyleSheet> sheet = GetStyleSheetForURL(aURL); + + // Make sure we remove the stylesheet from our internal list in all + // cases. + nsresult rv = RemoveStyleSheetFromList(aURL); + + NS_ENSURE_TRUE(sheet, NS_OK); /// Don't fail if sheet not found + + NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED); + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + ps->RemoveOverrideStyleSheet(sheet); + ps->RestyleForCSSRuleChanges(); + + // Remove it from our internal list + return rv; +} + +NS_IMETHODIMP +HTMLEditor::EnableStyleSheet(const nsAString& aURL, + bool aEnable) +{ + RefPtr<StyleSheet> sheet = GetStyleSheetForURL(aURL); + NS_ENSURE_TRUE(sheet, NS_OK); // Don't fail if sheet not found + + // Ensure the style sheet is owned by our document. + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + sheet->SetOwningDocument(doc); + + if (sheet->IsServo()) { + // XXXheycam ServoStyleSheets don't support being enabled/disabled yet. + NS_ERROR("stylo: ServoStyleSheets can't be disabled yet"); + return NS_ERROR_FAILURE; + } + return sheet->AsGecko()->SetDisabled(!aEnable); +} + +bool +HTMLEditor::EnableExistingStyleSheet(const nsAString& aURL) +{ + RefPtr<StyleSheet> sheet = GetStyleSheetForURL(aURL); + + // Enable sheet if already loaded. + if (!sheet) { + return false; + } + + // Ensure the style sheet is owned by our document. + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + sheet->SetOwningDocument(doc); + + if (sheet->IsServo()) { + // XXXheycam ServoStyleSheets don't support being enabled/disabled yet. + NS_ERROR("stylo: ServoStyleSheets can't be disabled yet"); + return true; + } + sheet->AsGecko()->SetDisabled(false); + return true; +} + +nsresult +HTMLEditor::AddNewStyleSheetToList(const nsAString& aURL, + StyleSheet* aStyleSheet) +{ + uint32_t countSS = mStyleSheets.Length(); + uint32_t countU = mStyleSheetURLs.Length(); + + if (countSS != countU) { + return NS_ERROR_UNEXPECTED; + } + + if (!mStyleSheetURLs.AppendElement(aURL)) { + return NS_ERROR_UNEXPECTED; + } + + return mStyleSheets.AppendElement(aStyleSheet) ? NS_OK : NS_ERROR_UNEXPECTED; +} + +nsresult +HTMLEditor::RemoveStyleSheetFromList(const nsAString& aURL) +{ + // is it already in the list? + size_t foundIndex; + foundIndex = mStyleSheetURLs.IndexOf(aURL); + if (foundIndex == mStyleSheetURLs.NoIndex) { + return NS_ERROR_FAILURE; + } + + // Attempt both removals; if one fails there's not much we can do. + mStyleSheets.RemoveElementAt(foundIndex); + mStyleSheetURLs.RemoveElementAt(foundIndex); + + return NS_OK; +} + +StyleSheet* +HTMLEditor::GetStyleSheetForURL(const nsAString& aURL) +{ + // is it already in the list? + size_t foundIndex; + foundIndex = mStyleSheetURLs.IndexOf(aURL); + if (foundIndex == mStyleSheetURLs.NoIndex) { + return nullptr; + } + + MOZ_ASSERT(mStyleSheets[foundIndex]); + return mStyleSheets[foundIndex]; +} + +void +HTMLEditor::GetURLForStyleSheet(StyleSheet* aStyleSheet, + nsAString& aURL) +{ + // is it already in the list? + int32_t foundIndex = mStyleSheets.IndexOf(aStyleSheet); + + // Don't fail if we don't find it in our list + if (foundIndex == -1) { + return; + } + + // Found it in the list! + aURL = mStyleSheetURLs[foundIndex]; +} + +NS_IMETHODIMP +HTMLEditor::GetEmbeddedObjects(nsIArray** aNodeList) +{ + NS_ENSURE_TRUE(aNodeList, NS_ERROR_NULL_POINTER); + + nsresult rv; + nsCOMPtr<nsIMutableArray> nodes = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIContentIterator> iter = + do_CreateInstance("@mozilla.org/content/post-content-iterator;1", &rv); + NS_ENSURE_TRUE(iter, NS_ERROR_NULL_POINTER); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED); + + iter->Init(doc->GetRootElement()); + + // Loop through the content iterator for each content node. + while (!iter->IsDone()) { + nsINode* node = iter->GetCurrentNode(); + if (node->IsElement()) { + dom::Element* element = node->AsElement(); + + // See if it's an image or an embed and also include all links. + // Let mail decide which link to send or not + if (element->IsAnyOfHTMLElements(nsGkAtoms::img, nsGkAtoms::embed, + nsGkAtoms::a) || + (element->IsHTMLElement(nsGkAtoms::body) && + element->HasAttr(kNameSpaceID_None, nsGkAtoms::background))) { + nsCOMPtr<nsIDOMNode> domNode = do_QueryInterface(node); + nodes->AppendElement(domNode, false); + } + } + iter->Next(); + } + + nodes.forget(aNodeList); + return rv; +} + +NS_IMETHODIMP +HTMLEditor::DeleteSelectionImpl(EDirection aAction, + EStripWrappers aStripWrappers) +{ + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + + nsresult rv = EditorBase::DeleteSelectionImpl(aAction, aStripWrappers); + NS_ENSURE_SUCCESS(rv, rv); + + // If we weren't asked to strip any wrappers, we're done. + if (aStripWrappers == eNoStrip) { + return NS_OK; + } + + RefPtr<Selection> selection = GetSelection(); + // Just checking that the selection itself is collapsed doesn't seem to work + // right in the multi-range case + NS_ENSURE_STATE(selection); + NS_ENSURE_STATE(selection->GetAnchorFocusRange()); + NS_ENSURE_STATE(selection->GetAnchorFocusRange()->Collapsed()); + + NS_ENSURE_STATE(selection->GetAnchorNode()->IsContent()); + nsCOMPtr<nsIContent> content = selection->GetAnchorNode()->AsContent(); + + // Don't strip wrappers if this is the only wrapper in the block. Then we'll + // add a <br> later, so it won't be an empty wrapper in the end. + nsCOMPtr<nsIContent> blockParent = content; + while (blockParent && !IsBlockNode(blockParent)) { + blockParent = blockParent->GetParent(); + } + if (!blockParent) { + return NS_OK; + } + bool emptyBlockParent; + rv = IsEmptyNode(blockParent, &emptyBlockParent); + NS_ENSURE_SUCCESS(rv, rv); + if (emptyBlockParent) { + return NS_OK; + } + + if (content && !IsBlockNode(content) && !content->Length() && + content->IsEditable() && content != content->GetEditingHost()) { + while (content->GetParent() && !IsBlockNode(content->GetParent()) && + content->GetParent()->Length() == 1 && + content->GetParent()->IsEditable() && + content->GetParent() != content->GetEditingHost()) { + content = content->GetParent(); + } + rv = DeleteNode(content); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +HTMLEditor::DeleteNode(nsINode* aNode) +{ + nsCOMPtr<nsIDOMNode> node = do_QueryInterface(aNode); + return DeleteNode(node); +} + +NS_IMETHODIMP +HTMLEditor::DeleteNode(nsIDOMNode* aNode) +{ + // do nothing if the node is read-only + nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); + if (!IsModifiableNode(aNode) && !IsMozEditorBogusNode(content)) { + return NS_ERROR_FAILURE; + } + + return EditorBase::DeleteNode(aNode); +} + +nsresult +HTMLEditor::DeleteText(nsGenericDOMDataNode& aCharData, + uint32_t aOffset, + uint32_t aLength) +{ + // Do nothing if the node is read-only + if (!IsModifiableNode(&aCharData)) { + return NS_ERROR_FAILURE; + } + + return EditorBase::DeleteText(aCharData, aOffset, aLength); +} + +nsresult +HTMLEditor::InsertTextImpl(const nsAString& aStringToInsert, + nsCOMPtr<nsINode>* aInOutNode, + int32_t* aInOutOffset, + nsIDocument* aDoc) +{ + // Do nothing if the node is read-only + if (!IsModifiableNode(*aInOutNode)) { + return NS_ERROR_FAILURE; + } + + return EditorBase::InsertTextImpl(aStringToInsert, aInOutNode, aInOutOffset, + aDoc); +} + +void +HTMLEditor::ContentAppended(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aFirstNewContent, + int32_t aIndexInContainer) +{ + DoContentInserted(aDocument, aContainer, aFirstNewContent, aIndexInContainer, + eAppended); +} + +void +HTMLEditor::ContentInserted(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild, + int32_t aIndexInContainer) +{ + DoContentInserted(aDocument, aContainer, aChild, aIndexInContainer, + eInserted); +} + +bool +HTMLEditor::IsInObservedSubtree(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild) +{ + if (!aChild) { + return false; + } + + Element* root = GetRoot(); + // To be super safe here, check both ChromeOnlyAccess and GetBindingParent. + // That catches (also unbound) native anonymous content, XBL and ShadowDOM. + if (root && + (root->ChromeOnlyAccess() != aChild->ChromeOnlyAccess() || + root->GetBindingParent() != aChild->GetBindingParent())) { + return false; + } + + return !aChild->ChromeOnlyAccess() && !aChild->GetBindingParent(); +} + +void +HTMLEditor::DoContentInserted(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild, + int32_t aIndexInContainer, + InsertedOrAppended aInsertedOrAppended) +{ + if (!IsInObservedSubtree(aDocument, aContainer, aChild)) { + return; + } + + nsCOMPtr<nsIHTMLEditor> kungFuDeathGrip(this); + + if (ShouldReplaceRootElement()) { + UpdateRootElement(); + nsContentUtils::AddScriptRunner(NewRunnableMethod( + this, &HTMLEditor::NotifyRootChanged)); + } + // We don't need to handle our own modifications + else if (!mAction && (aContainer ? aContainer->IsEditable() : aDocument->IsEditable())) { + if (IsMozEditorBogusNode(aChild)) { + // Ignore insertion of the bogus node + return; + } + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + rules->DocumentModified(); + + // Update spellcheck for only the newly-inserted node (bug 743819) + if (mInlineSpellChecker) { + RefPtr<nsRange> range = new nsRange(aChild); + int32_t endIndex = aIndexInContainer + 1; + if (aInsertedOrAppended == eAppended) { + // Count all the appended nodes + nsIContent* sibling = aChild->GetNextSibling(); + while (sibling) { + endIndex++; + sibling = sibling->GetNextSibling(); + } + } + nsresult rv = range->Set(aContainer, aIndexInContainer, + aContainer, endIndex); + if (NS_SUCCEEDED(rv)) { + mInlineSpellChecker->SpellCheckRange(range); + } + } + } +} + +void +HTMLEditor::ContentRemoved(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild, + int32_t aIndexInContainer, + nsIContent* aPreviousSibling) +{ + if (!IsInObservedSubtree(aDocument, aContainer, aChild)) { + return; + } + + nsCOMPtr<nsIHTMLEditor> kungFuDeathGrip(this); + + if (SameCOMIdentity(aChild, mRootElement)) { + mRootElement = nullptr; + nsContentUtils::AddScriptRunner(NewRunnableMethod( + this, &HTMLEditor::NotifyRootChanged)); + } + // We don't need to handle our own modifications + else if (!mAction && (aContainer ? aContainer->IsEditable() : aDocument->IsEditable())) { + if (aChild && IsMozEditorBogusNode(aChild)) { + // Ignore removal of the bogus node + return; + } + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + rules->DocumentModified(); + } +} + +NS_IMETHODIMP_(bool) +HTMLEditor::IsModifiableNode(nsIDOMNode* aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return IsModifiableNode(node); +} + +bool +HTMLEditor::IsModifiableNode(nsINode* aNode) +{ + return !aNode || aNode->IsEditable(); +} + +NS_IMETHODIMP +HTMLEditor::GetIsSelectionEditable(bool* aIsSelectionEditable) +{ + MOZ_ASSERT(aIsSelectionEditable); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // Per the editing spec as of June 2012: we have to have a selection whose + // start and end nodes are editable, and which share an ancestor editing + // host. (Bug 766387.) + *aIsSelectionEditable = selection->RangeCount() && + selection->GetAnchorNode()->IsEditable() && + selection->GetFocusNode()->IsEditable(); + + if (*aIsSelectionEditable) { + nsINode* commonAncestor = + selection->GetAnchorFocusRange()->GetCommonAncestor(); + while (commonAncestor && !commonAncestor->IsEditable()) { + commonAncestor = commonAncestor->GetParentNode(); + } + if (!commonAncestor) { + // No editable common ancestor + *aIsSelectionEditable = false; + } + } + + return NS_OK; +} + +static nsresult +SetSelectionAroundHeadChildren(Selection* aSelection, + nsIWeakReference* aDocWeak) +{ + // Set selection around <head> node + nsCOMPtr<nsIDocument> doc = do_QueryReferent(aDocWeak); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + + dom::Element* headNode = doc->GetHeadElement(); + NS_ENSURE_STATE(headNode); + + // Collapse selection to before first child of the head, + nsresult rv = aSelection->CollapseNative(headNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + + // Then extend it to just after. + uint32_t childCount = headNode->GetChildCount(); + return aSelection->ExtendNative(headNode, childCount + 1); +} + +NS_IMETHODIMP +HTMLEditor::GetHeadContentsAsHTML(nsAString& aOutputString) +{ + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // Save current selection + AutoSelectionRestorer selectionRestorer(selection, this); + + nsresult rv = SetSelectionAroundHeadChildren(selection, mDocWeak); + NS_ENSURE_SUCCESS(rv, rv); + + rv = OutputToString(NS_LITERAL_STRING("text/html"), + nsIDocumentEncoder::OutputSelectionOnly, + aOutputString); + if (NS_FAILED(rv)) { + return rv; + } + + // Selection always includes <body></body>, + // so terminate there + nsReadingIterator<char16_t> findIter,endFindIter; + aOutputString.BeginReading(findIter); + aOutputString.EndReading(endFindIter); + //counting on our parser to always lower case!!! + if (CaseInsensitiveFindInReadable(NS_LITERAL_STRING("<body"), + findIter, endFindIter)) { + nsReadingIterator<char16_t> beginIter; + aOutputString.BeginReading(beginIter); + int32_t offset = Distance(beginIter, findIter);//get the distance + + nsWritingIterator<char16_t> writeIter; + aOutputString.BeginWriting(writeIter); + // Ensure the string ends in a newline + char16_t newline ('\n'); + findIter.advance(-1); + if (!offset || (offset > 0 && (*findIter) != newline)) { + writeIter.advance(offset); + *writeIter = newline; + aOutputString.Truncate(offset+1); + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::DebugUnitTests(int32_t* outNumTests, + int32_t* outNumTestsFailed) +{ +#ifdef DEBUG + NS_ENSURE_TRUE(outNumTests && outNumTestsFailed, NS_ERROR_NULL_POINTER); + + TextEditorTest *tester = new TextEditorTest(); + NS_ENSURE_TRUE(tester, NS_ERROR_OUT_OF_MEMORY); + + tester->Run(this, outNumTests, outNumTestsFailed); + delete tester; + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +NS_IMETHODIMP +HTMLEditor::StyleSheetLoaded(StyleSheet* aSheet, + bool aWasAlternate, + nsresult aStatus) +{ + nsresult rv = NS_OK; + AutoEditBatch batchIt(this); + + if (!mLastStyleSheetURL.IsEmpty()) + RemoveStyleSheet(mLastStyleSheetURL); + + RefPtr<AddStyleSheetTransaction> transaction; + rv = CreateTxnForAddStyleSheet(aSheet, getter_AddRefs(transaction)); + if (!transaction) { + rv = NS_ERROR_NULL_POINTER; + } + if (NS_SUCCEEDED(rv)) { + rv = DoTransaction(transaction); + if (NS_SUCCEEDED(rv)) { + // Get the URI, then url spec from the sheet + nsAutoCString spec; + rv = aSheet->GetSheetURI()->GetSpec(spec); + + if (NS_SUCCEEDED(rv)) { + // Save it so we can remove before applying the next one + mLastStyleSheetURL.AssignWithConversion(spec.get()); + + // Also save in our arrays of urls and sheets + AddNewStyleSheetToList(mLastStyleSheetURL, aSheet); + } + } + } + + return NS_OK; +} + +/** + * All editor operations which alter the doc should be prefaced + * with a call to StartOperation, naming the action and direction. + */ +NS_IMETHODIMP +HTMLEditor::StartOperation(EditAction opID, + nsIEditor::EDirection aDirection) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + EditorBase::StartOperation(opID, aDirection); // will set mAction, mDirection + if (rules) { + return rules->BeforeEdit(mAction, mDirection); + } + return NS_OK; +} + +/** + * All editor operations which alter the doc should be followed + * with a call to EndOperation. + */ +NS_IMETHODIMP +HTMLEditor::EndOperation() +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // post processing + nsresult rv = rules ? rules->AfterEdit(mAction, mDirection) : NS_OK; + EditorBase::EndOperation(); // will clear mAction, mDirection + return rv; +} + +bool +HTMLEditor::TagCanContainTag(nsIAtom& aParentTag, + nsIAtom& aChildTag) +{ + nsIParserService* parserService = nsContentUtils::GetParserService(); + + int32_t childTagEnum; + // XXX Should this handle #cdata-section too? + if (&aChildTag == nsGkAtoms::textTagName) { + childTagEnum = eHTMLTag_text; + } else { + childTagEnum = parserService->HTMLAtomTagToId(&aChildTag); + } + + int32_t parentTagEnum = parserService->HTMLAtomTagToId(&aParentTag); + return HTMLEditUtils::CanContain(parentTagEnum, childTagEnum); +} + +bool +HTMLEditor::IsContainer(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + int32_t tagEnum; + // XXX Should this handle #cdata-section too? + if (aNode->IsNodeOfType(nsINode::eTEXT)) { + tagEnum = eHTMLTag_text; + } else { + tagEnum = + nsContentUtils::GetParserService()->HTMLStringTagToId(aNode->NodeName()); + } + + return HTMLEditUtils::IsContainer(tagEnum); +} + +bool +HTMLEditor::IsContainer(nsIDOMNode* aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + if (!node) { + return false; + } + return IsContainer(node); +} + + +nsresult +HTMLEditor::SelectEntireDocument(Selection* aSelection) +{ + if (!aSelection || !mRules) { + return NS_ERROR_NULL_POINTER; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // get editor root node + nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(GetRoot()); + + // is doc empty? + bool bDocIsEmpty; + nsresult rv = rules->DocumentIsEmpty(&bDocIsEmpty); + NS_ENSURE_SUCCESS(rv, rv); + + if (bDocIsEmpty) { + // if its empty dont select entire doc - that would select the bogus node + return aSelection->Collapse(rootElement, 0); + } + + return EditorBase::SelectEntireDocument(aSelection); +} + +NS_IMETHODIMP +HTMLEditor::SelectAll() +{ + ForceCompositionEnd(); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + nsCOMPtr<nsIDOMNode> anchorNode; + nsresult rv = selection->GetAnchorNode(getter_AddRefs(anchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIContent> anchorContent = do_QueryInterface(anchorNode, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsIContent *rootContent; + if (anchorContent->HasIndependentSelection()) { + rv = selection->SetAncestorLimiter(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + rootContent = mRootElement; + } else { + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + rootContent = anchorContent->GetSelectionRootContent(ps); + } + + NS_ENSURE_TRUE(rootContent, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIDOMNode> rootElement = do_QueryInterface(rootContent, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + Maybe<mozilla::dom::Selection::AutoUserInitiated> userSelection; + if (!rootContent->IsEditable()) { + userSelection.emplace(selection); + } + return selection->SelectAllChildren(rootElement); +} + +// this will NOT find aAttribute unless aAttribute has a non-null value +// so singleton attributes like <Table border> will not be matched! +bool +HTMLEditor::IsTextPropertySetByContent(nsINode* aNode, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString* aValue, + nsAString* outValue) +{ + MOZ_ASSERT(aNode && aProperty); + bool isSet; + IsTextPropertySetByContent(aNode->AsDOMNode(), aProperty, aAttribute, aValue, + isSet, outValue); + return isSet; +} + +void +HTMLEditor::IsTextPropertySetByContent(nsIDOMNode* aNode, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool& aIsSet, + nsAString* outValue) +{ + aIsSet = false; // must be initialized to false for code below to work + nsAutoString propName; + aProperty->ToString(propName); + nsCOMPtr<nsIDOMNode>node = aNode; + + while (node) { + nsCOMPtr<nsIDOMElement>element; + element = do_QueryInterface(node); + if (element) { + nsAutoString tag, value; + element->GetTagName(tag); + if (propName.Equals(tag, nsCaseInsensitiveStringComparator())) { + bool found = false; + if (aAttribute && !aAttribute->IsEmpty()) { + element->GetAttribute(*aAttribute, value); + if (outValue) { + *outValue = value; + } + if (!value.IsEmpty()) { + if (!aValue) { + found = true; + } else { + nsString tString(*aValue); + if (tString.Equals(value, nsCaseInsensitiveStringComparator())) { + found = true; + } else { + // We found the prop with the attribute, but the value doesn't + // match. + break; + } + } + } + } else { + found = true; + } + if (found) { + aIsSet = true; + break; + } + } + } + nsCOMPtr<nsIDOMNode>temp; + if (NS_SUCCEEDED(node->GetParentNode(getter_AddRefs(temp))) && temp) { + node = temp; + } else { + node = nullptr; + } + } +} + +bool +HTMLEditor::SetCaretInTableCell(nsIDOMElement* aElement) +{ + nsCOMPtr<dom::Element> element = do_QueryInterface(aElement); + if (!element || !element->IsHTMLElement() || + !HTMLEditUtils::IsTableElement(element) || + !IsDescendantOfEditorRoot(element)) { + return false; + } + + nsIContent* node = element; + while (node->HasChildren()) { + node = node->GetFirstChild(); + } + + // Set selection at beginning of the found node + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, false); + + return NS_SUCCEEDED(selection->CollapseNative(node, 0)); +} + +/** + * GetEnclosingTable() finds ancestor who is a table, if any. + */ +Element* +HTMLEditor::GetEnclosingTable(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + for (nsCOMPtr<Element> block = GetBlockNodeParent(aNode); + block; + block = GetBlockNodeParent(block)) { + if (HTMLEditUtils::IsTable(block)) { + return block; + } + } + return nullptr; +} + +nsIDOMNode* +HTMLEditor::GetEnclosingTable(nsIDOMNode* aNode) +{ + NS_PRECONDITION(aNode, "null node passed to HTMLEditor::GetEnclosingTable"); + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, nullptr); + nsCOMPtr<Element> table = GetEnclosingTable(node); + nsCOMPtr<nsIDOMNode> ret = do_QueryInterface(table); + return ret; +} + + +/** + * This method scans the selection for adjacent text nodes + * and collapses them into a single text node. + * "adjacent" means literally adjacent siblings of the same parent. + * Uses EditorBase::JoinNodes so action is undoable. + * Should be called within the context of a batch transaction. + */ +nsresult +HTMLEditor::CollapseAdjacentTextNodes(nsRange* aInRange) +{ + NS_ENSURE_TRUE(aInRange, NS_ERROR_NULL_POINTER); + AutoTransactionsConserveSelection dontSpazMySelection(this); + nsTArray<nsCOMPtr<nsIDOMNode> > textNodes; + // we can't actually do anything during iteration, so store the text nodes in an array + // don't bother ref counting them because we know we can hold them for the + // lifetime of this method + + + // build a list of editable text nodes + nsresult rv = NS_ERROR_UNEXPECTED; + nsCOMPtr<nsIContentIterator> iter = + do_CreateInstance("@mozilla.org/content/subtree-content-iterator;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + iter->Init(aInRange); + + while (!iter->IsDone()) { + nsINode* node = iter->GetCurrentNode(); + if (node->NodeType() == nsIDOMNode::TEXT_NODE && + IsEditable(static_cast<nsIContent*>(node))) { + nsCOMPtr<nsIDOMNode> domNode = do_QueryInterface(node); + textNodes.AppendElement(domNode); + } + + iter->Next(); + } + + // now that I have a list of text nodes, collapse adjacent text nodes + // NOTE: assumption that JoinNodes keeps the righthand node + while (textNodes.Length() > 1) { + // we assume a textNodes entry can't be nullptr + nsIDOMNode *leftTextNode = textNodes[0]; + nsIDOMNode *rightTextNode = textNodes[1]; + NS_ASSERTION(leftTextNode && rightTextNode,"left or rightTextNode null in CollapseAdjacentTextNodes"); + + // get the prev sibling of the right node, and see if its leftTextNode + nsCOMPtr<nsIDOMNode> prevSibOfRightNode; + rv = rightTextNode->GetPreviousSibling(getter_AddRefs(prevSibOfRightNode)); + NS_ENSURE_SUCCESS(rv, rv); + if (prevSibOfRightNode && prevSibOfRightNode == leftTextNode) { + nsCOMPtr<nsIDOMNode> parent; + rv = rightTextNode->GetParentNode(getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + rv = JoinNodes(leftTextNode, rightTextNode, parent); + NS_ENSURE_SUCCESS(rv, rv); + } + + textNodes.RemoveElementAt(0); // remove the leftmost text node from the list + } + + return NS_OK; +} + +nsresult +HTMLEditor::SetSelectionAtDocumentStart(Selection* aSelection) +{ + dom::Element* rootElement = GetRoot(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER); + + return aSelection->CollapseNative(rootElement, 0); +} + +/** + * Remove aNode, reparenting any children into the parent of aNode. In + * addition, insert any br's needed to preserve identity of removed block. + */ +nsresult +HTMLEditor::RemoveBlockContainer(nsIContent& aNode) +{ + // Two possibilities: the container could be empty of editable content. If + // that is the case, we need to compare what is before and after aNode to + // determine if we need a br. + // + // Or it could be not empty, in which case we have to compare previous + // sibling and first child to determine if we need a leading br, and compare + // following sibling and last child to determine if we need a trailing br. + + nsCOMPtr<nsIContent> child = GetFirstEditableChild(aNode); + + if (child) { + // The case of aNode not being empty. We need a br at start unless: + // 1) previous sibling of aNode is a block, OR + // 2) previous sibling of aNode is a br, OR + // 3) first child of aNode is a block OR + // 4) either is null + + nsCOMPtr<nsIContent> sibling = GetPriorHTMLSibling(&aNode); + if (sibling && !IsBlockNode(sibling) && + !sibling->IsHTMLElement(nsGkAtoms::br) && !IsBlockNode(child)) { + // Insert br node + nsCOMPtr<Element> br = CreateBR(&aNode, 0); + NS_ENSURE_STATE(br); + } + + // We need a br at end unless: + // 1) following sibling of aNode is a block, OR + // 2) last child of aNode is a block, OR + // 3) last child of aNode is a br OR + // 4) either is null + + sibling = GetNextHTMLSibling(&aNode); + if (sibling && !IsBlockNode(sibling)) { + child = GetLastEditableChild(aNode); + MOZ_ASSERT(child, "aNode has first editable child but not last?"); + if (!IsBlockNode(child) && !child->IsHTMLElement(nsGkAtoms::br)) { + // Insert br node + nsCOMPtr<Element> br = CreateBR(&aNode, aNode.Length()); + NS_ENSURE_STATE(br); + } + } + } else { + // The case of aNode being empty. We need a br at start unless: + // 1) previous sibling of aNode is a block, OR + // 2) previous sibling of aNode is a br, OR + // 3) following sibling of aNode is a block, OR + // 4) following sibling of aNode is a br OR + // 5) either is null + nsCOMPtr<nsIContent> sibling = GetPriorHTMLSibling(&aNode); + if (sibling && !IsBlockNode(sibling) && + !sibling->IsHTMLElement(nsGkAtoms::br)) { + sibling = GetNextHTMLSibling(&aNode); + if (sibling && !IsBlockNode(sibling) && + !sibling->IsHTMLElement(nsGkAtoms::br)) { + // Insert br node + nsCOMPtr<Element> br = CreateBR(&aNode, 0); + NS_ENSURE_STATE(br); + } + } + } + + // Now remove container + nsresult rv = RemoveContainer(&aNode); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/** + * GetPriorHTMLSibling() returns the previous editable sibling, if there is + * one within the parent. + */ +nsIContent* +HTMLEditor::GetPriorHTMLSibling(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + nsIContent* node = aNode->GetPreviousSibling(); + while (node && !IsEditable(node)) { + node = node->GetPreviousSibling(); + } + + return node; +} + +nsresult +HTMLEditor::GetPriorHTMLSibling(nsIDOMNode* inNode, + nsCOMPtr<nsIDOMNode>* outNode) +{ + NS_ENSURE_TRUE(outNode, NS_ERROR_NULL_POINTER); + *outNode = nullptr; + + nsCOMPtr<nsINode> node = do_QueryInterface(inNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + + *outNode = do_QueryInterface(GetPriorHTMLSibling(node)); + return NS_OK; +} + +/** + * GetPriorHTMLSibling() returns the previous editable sibling, if there is + * one within the parent. just like above routine but takes a parent/offset + * instead of a node. + */ +nsIContent* +HTMLEditor::GetPriorHTMLSibling(nsINode* aParent, + int32_t aOffset) +{ + MOZ_ASSERT(aParent); + + nsIContent* node = aParent->GetChildAt(aOffset - 1); + if (!node || IsEditable(node)) { + return node; + } + + return GetPriorHTMLSibling(node); +} + +nsresult +HTMLEditor::GetPriorHTMLSibling(nsIDOMNode* inParent, + int32_t inOffset, + nsCOMPtr<nsIDOMNode>* outNode) +{ + NS_ENSURE_TRUE(outNode, NS_ERROR_NULL_POINTER); + *outNode = nullptr; + + nsCOMPtr<nsINode> parent = do_QueryInterface(inParent); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + + *outNode = do_QueryInterface(GetPriorHTMLSibling(parent, inOffset)); + return NS_OK; +} + +/** + * GetNextHTMLSibling() returns the next editable sibling, if there is + * one within the parent. + */ +nsIContent* +HTMLEditor::GetNextHTMLSibling(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + nsIContent* node = aNode->GetNextSibling(); + while (node && !IsEditable(node)) { + node = node->GetNextSibling(); + } + + return node; +} + +nsresult +HTMLEditor::GetNextHTMLSibling(nsIDOMNode* inNode, + nsCOMPtr<nsIDOMNode>* outNode) +{ + NS_ENSURE_TRUE(outNode, NS_ERROR_NULL_POINTER); + *outNode = nullptr; + + nsCOMPtr<nsINode> node = do_QueryInterface(inNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + + *outNode = do_QueryInterface(GetNextHTMLSibling(node)); + return NS_OK; +} + +/** + * GetNextHTMLSibling() returns the next editable sibling, if there is + * one within the parent. just like above routine but takes a parent/offset + * instead of a node. + */ +nsIContent* +HTMLEditor::GetNextHTMLSibling(nsINode* aParent, + int32_t aOffset) +{ + MOZ_ASSERT(aParent); + + nsIContent* node = aParent->GetChildAt(aOffset + 1); + if (!node || IsEditable(node)) { + return node; + } + + return GetNextHTMLSibling(node); +} + +nsresult +HTMLEditor::GetNextHTMLSibling(nsIDOMNode* inParent, + int32_t inOffset, + nsCOMPtr<nsIDOMNode>* outNode) +{ + NS_ENSURE_TRUE(outNode, NS_ERROR_NULL_POINTER); + *outNode = nullptr; + + nsCOMPtr<nsINode> parent = do_QueryInterface(inParent); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + + *outNode = do_QueryInterface(GetNextHTMLSibling(parent, inOffset)); + return NS_OK; +} + +/** + * GetPriorHTMLNode() returns the previous editable leaf node, if there is + * one within the <body>. + */ +nsIContent* +HTMLEditor::GetPriorHTMLNode(nsINode* aNode, + bool aNoBlockCrossing) +{ + MOZ_ASSERT(aNode); + + if (!GetActiveEditingHost()) { + return nullptr; + } + + return GetPriorNode(aNode, true, aNoBlockCrossing); +} + +nsresult +HTMLEditor::GetPriorHTMLNode(nsIDOMNode* aNode, + nsCOMPtr<nsIDOMNode>* aResultNode, + bool aNoBlockCrossing) +{ + NS_ENSURE_TRUE(aResultNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + + *aResultNode = do_QueryInterface(GetPriorHTMLNode(node, aNoBlockCrossing)); + return NS_OK; +} + +/** + * GetPriorHTMLNode() is same as above but takes {parent,offset} instead of + * node. + */ +nsIContent* +HTMLEditor::GetPriorHTMLNode(nsINode* aParent, + int32_t aOffset, + bool aNoBlockCrossing) +{ + MOZ_ASSERT(aParent); + + if (!GetActiveEditingHost()) { + return nullptr; + } + + return GetPriorNode(aParent, aOffset, true, aNoBlockCrossing); +} + +nsresult +HTMLEditor::GetPriorHTMLNode(nsIDOMNode* aNode, + int32_t aOffset, + nsCOMPtr<nsIDOMNode>* aResultNode, + bool aNoBlockCrossing) +{ + NS_ENSURE_TRUE(aResultNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + + *aResultNode = do_QueryInterface(GetPriorHTMLNode(node, aOffset, + aNoBlockCrossing)); + return NS_OK; +} + +/** + * GetNextHTMLNode() returns the next editable leaf node, if there is + * one within the <body>. + */ +nsIContent* +HTMLEditor::GetNextHTMLNode(nsINode* aNode, + bool aNoBlockCrossing) +{ + MOZ_ASSERT(aNode); + + nsIContent* result = GetNextNode(aNode, true, aNoBlockCrossing); + + if (result && !IsDescendantOfEditorRoot(result)) { + return nullptr; + } + + return result; +} + +nsresult +HTMLEditor::GetNextHTMLNode(nsIDOMNode* aNode, + nsCOMPtr<nsIDOMNode>* aResultNode, + bool aNoBlockCrossing) +{ + NS_ENSURE_TRUE(aResultNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + + *aResultNode = do_QueryInterface(GetNextHTMLNode(node, aNoBlockCrossing)); + return NS_OK; +} + +/** + * GetNextHTMLNode() is same as above but takes {parent,offset} instead of node. + */ +nsIContent* +HTMLEditor::GetNextHTMLNode(nsINode* aParent, + int32_t aOffset, + bool aNoBlockCrossing) +{ + nsIContent* content = GetNextNode(aParent, aOffset, true, aNoBlockCrossing); + if (content && !IsDescendantOfEditorRoot(content)) { + return nullptr; + } + return content; +} + +nsresult +HTMLEditor::GetNextHTMLNode(nsIDOMNode* aNode, + int32_t aOffset, + nsCOMPtr<nsIDOMNode>* aResultNode, + bool aNoBlockCrossing) +{ + NS_ENSURE_TRUE(aResultNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + + *aResultNode = do_QueryInterface(GetNextHTMLNode(node, aOffset, + aNoBlockCrossing)); + return NS_OK; +} + +nsresult +HTMLEditor::IsFirstEditableChild(nsIDOMNode* aNode, + bool* aOutIsFirst) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(aOutIsFirst && node, NS_ERROR_NULL_POINTER); + + // init out parms + *aOutIsFirst = false; + + // find first editable child and compare it to aNode + nsCOMPtr<nsINode> parent = node->GetParentNode(); + NS_ENSURE_TRUE(parent, NS_ERROR_FAILURE); + + *aOutIsFirst = (GetFirstEditableChild(*parent) == node); + return NS_OK; +} + +nsresult +HTMLEditor::IsLastEditableChild(nsIDOMNode* aNode, + bool* aOutIsLast) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(aOutIsLast && node, NS_ERROR_NULL_POINTER); + + // init out parms + *aOutIsLast = false; + + // find last editable child and compare it to aNode + nsCOMPtr<nsINode> parent = node->GetParentNode(); + NS_ENSURE_TRUE(parent, NS_ERROR_FAILURE); + + *aOutIsLast = (GetLastEditableChild(*parent) == node); + return NS_OK; +} + +nsIContent* +HTMLEditor::GetFirstEditableChild(nsINode& aNode) +{ + nsCOMPtr<nsIContent> child = aNode.GetFirstChild(); + + while (child && !IsEditable(child)) { + child = child->GetNextSibling(); + } + + return child; +} + +nsIContent* +HTMLEditor::GetLastEditableChild(nsINode& aNode) +{ + nsCOMPtr<nsIContent> child = aNode.GetLastChild(); + + while (child && !IsEditable(child)) { + child = child->GetPreviousSibling(); + } + + return child; +} + +nsIContent* +HTMLEditor::GetFirstEditableLeaf(nsINode& aNode) +{ + nsCOMPtr<nsIContent> child = GetLeftmostChild(&aNode); + while (child && (!IsEditable(child) || child->HasChildren())) { + child = GetNextHTMLNode(child); + + // Only accept nodes that are descendants of aNode + if (!aNode.Contains(child)) { + return nullptr; + } + } + + return child; +} + +nsIContent* +HTMLEditor::GetLastEditableLeaf(nsINode& aNode) +{ + nsCOMPtr<nsIContent> child = GetRightmostChild(&aNode, false); + while (child && (!IsEditable(child) || child->HasChildren())) { + child = GetPriorHTMLNode(child); + + // Only accept nodes that are descendants of aNode + if (!aNode.Contains(child)) { + return nullptr; + } + } + + return child; +} + +/** + * IsVisTextNode() figures out if textnode aTextNode has any visible content. + */ +nsresult +HTMLEditor::IsVisTextNode(nsIContent* aNode, + bool* outIsEmptyNode, + bool aSafeToAskFrames) +{ + MOZ_ASSERT(aNode); + MOZ_ASSERT(aNode->NodeType() == nsIDOMNode::TEXT_NODE); + MOZ_ASSERT(outIsEmptyNode); + + *outIsEmptyNode = true; + + uint32_t length = aNode->TextLength(); + if (aSafeToAskFrames) { + nsCOMPtr<nsISelectionController> selCon; + nsresult rv = GetSelectionController(getter_AddRefs(selCon)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); + bool isVisible = false; + // ask the selection controller for information about whether any + // of the data in the node is really rendered. This is really + // something that frames know about, but we aren't supposed to talk to frames. + // So we put a call in the selection controller interface, since it's already + // in bed with frames anyway. (this is a fix for bug 22227, and a + // partial fix for bug 46209) + rv = selCon->CheckVisibilityContent(aNode, 0, length, &isVisible); + NS_ENSURE_SUCCESS(rv, rv); + if (isVisible) { + *outIsEmptyNode = false; + } + } else if (length) { + if (aNode->TextIsOnlyWhitespace()) { + WSRunObject wsRunObj(this, aNode, 0); + nsCOMPtr<nsINode> visNode; + int32_t outVisOffset=0; + WSType visType; + wsRunObj.NextVisibleNode(aNode, 0, address_of(visNode), + &outVisOffset, &visType); + if (visType == WSType::normalWS || visType == WSType::text) { + *outIsEmptyNode = (aNode != visNode); + } + } else { + *outIsEmptyNode = false; + } + } + return NS_OK; +} + +/** + * IsEmptyNode() figures out if aNode is an empty node. A block can have + * children and still be considered empty, if the children are empty or + * non-editable. + */ +nsresult +HTMLEditor::IsEmptyNode(nsIDOMNode*aNode, + bool* outIsEmptyNode, + bool aSingleBRDoesntCount, + bool aListOrCellNotEmpty, + bool aSafeToAskFrames) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + return IsEmptyNode(node, outIsEmptyNode, aSingleBRDoesntCount, + aListOrCellNotEmpty, aSafeToAskFrames); +} + +nsresult +HTMLEditor::IsEmptyNode(nsINode* aNode, + bool* outIsEmptyNode, + bool aSingleBRDoesntCount, + bool aListOrCellNotEmpty, + bool aSafeToAskFrames) +{ + NS_ENSURE_TRUE(aNode && outIsEmptyNode, NS_ERROR_NULL_POINTER); + *outIsEmptyNode = true; + bool seenBR = false; + return IsEmptyNodeImpl(aNode, outIsEmptyNode, aSingleBRDoesntCount, + aListOrCellNotEmpty, aSafeToAskFrames, &seenBR); +} + +/** + * IsEmptyNodeImpl() is workhorse for IsEmptyNode(). + */ +nsresult +HTMLEditor::IsEmptyNodeImpl(nsINode* aNode, + bool* outIsEmptyNode, + bool aSingleBRDoesntCount, + bool aListOrCellNotEmpty, + bool aSafeToAskFrames, + bool* aSeenBR) +{ + NS_ENSURE_TRUE(aNode && outIsEmptyNode && aSeenBR, NS_ERROR_NULL_POINTER); + + if (aNode->NodeType() == nsIDOMNode::TEXT_NODE) { + return IsVisTextNode(static_cast<nsIContent*>(aNode), outIsEmptyNode, aSafeToAskFrames); + } + + // if it's not a text node (handled above) and it's not a container, + // then we don't call it empty (it's an <hr>, or <br>, etc.). + // Also, if it's an anchor then don't treat it as empty - even though + // anchors are containers, named anchors are "empty" but we don't + // want to treat them as such. Also, don't call ListItems or table + // cells empty if caller desires. Form Widgets not empty. + if (!IsContainer(aNode->AsDOMNode()) || + (HTMLEditUtils::IsNamedAnchor(aNode) || + HTMLEditUtils::IsFormWidget(aNode) || + (aListOrCellNotEmpty && + (HTMLEditUtils::IsListItem(aNode) || + HTMLEditUtils::IsTableCell(aNode))))) { + *outIsEmptyNode = false; + return NS_OK; + } + + // need this for later + bool isListItemOrCell = HTMLEditUtils::IsListItem(aNode) || + HTMLEditUtils::IsTableCell(aNode); + + // loop over children of node. if no children, or all children are either + // empty text nodes or non-editable, then node qualifies as empty + for (nsCOMPtr<nsIContent> child = aNode->GetFirstChild(); + child; + child = child->GetNextSibling()) { + // Is the child editable and non-empty? if so, return false + if (EditorBase::IsEditable(child)) { + if (child->NodeType() == nsIDOMNode::TEXT_NODE) { + nsresult rv = IsVisTextNode(child, outIsEmptyNode, aSafeToAskFrames); + NS_ENSURE_SUCCESS(rv, rv); + // break out if we find we aren't emtpy + if (!*outIsEmptyNode) { + return NS_OK; + } + } else { + // An editable, non-text node. We need to check its content. + // Is it the node we are iterating over? + if (child == aNode) { + break; + } + + if (aSingleBRDoesntCount && !*aSeenBR && child->IsHTMLElement(nsGkAtoms::br)) { + // the first br in a block doesn't count if the caller so indicated + *aSeenBR = true; + } else { + // is it an empty node of some sort? + // note: list items or table cells are not considered empty + // if they contain other lists or tables + if (child->IsElement()) { + if (isListItemOrCell) { + if (HTMLEditUtils::IsList(child) || + child->IsHTMLElement(nsGkAtoms::table)) { + // break out if we find we aren't empty + *outIsEmptyNode = false; + return NS_OK; + } + } else if (HTMLEditUtils::IsFormWidget(child)) { + // is it a form widget? + // break out if we find we aren't empty + *outIsEmptyNode = false; + return NS_OK; + } + } + + bool isEmptyNode = true; + nsresult rv = IsEmptyNodeImpl(child, &isEmptyNode, + aSingleBRDoesntCount, + aListOrCellNotEmpty, aSafeToAskFrames, + aSeenBR); + NS_ENSURE_SUCCESS(rv, rv); + if (!isEmptyNode) { + // otherwise it ain't empty + *outIsEmptyNode = false; + return NS_OK; + } + } + } + } + } + + return NS_OK; +} + +// add to aElement the CSS inline styles corresponding to the HTML attribute +// aAttribute with its value aValue +nsresult +HTMLEditor::SetAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue, + bool aSuppressTransaction) +{ + nsAutoScriptBlocker scriptBlocker; + + if (IsCSSEnabled() && mCSSEditUtils) { + int32_t count; + nsresult rv = + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(aElement, nullptr, + &aAttribute, &aValue, + &count, + aSuppressTransaction); + NS_ENSURE_SUCCESS(rv, rv); + if (count) { + // we found an equivalence ; let's remove the HTML attribute itself if it is set + nsAutoString existingValue; + bool wasSet = false; + rv = GetAttributeValue(aElement, aAttribute, existingValue, &wasSet); + NS_ENSURE_SUCCESS(rv, rv); + if (!wasSet) { + return NS_OK; + } + return aSuppressTransaction ? aElement->RemoveAttribute(aAttribute) : + RemoveAttribute(aElement, aAttribute); + } + + // count is an integer that represents the number of CSS declarations applied to the + // element. If it is zero, we found no equivalence in this implementation for the + // attribute + if (aAttribute.EqualsLiteral("style")) { + // if it is the style attribute, just add the new value to the existing style + // attribute's value + nsAutoString existingValue; + bool wasSet = false; + nsresult rv = GetAttributeValue(aElement, NS_LITERAL_STRING("style"), + existingValue, &wasSet); + NS_ENSURE_SUCCESS(rv, rv); + existingValue.Append(' '); + existingValue.Append(aValue); + return aSuppressTransaction ? + aElement->SetAttribute(aAttribute, existingValue) : + SetAttribute(aElement, aAttribute, existingValue); + } + + // we have no CSS equivalence for this attribute and it is not the style + // attribute; let's set it the good'n'old HTML way + return aSuppressTransaction ? aElement->SetAttribute(aAttribute, aValue) : + SetAttribute(aElement, aAttribute, aValue); + } + + // we are not in an HTML+CSS editor; let's set the attribute the HTML way + return aSuppressTransaction ? aElement->SetAttribute(aAttribute, aValue) : + SetAttribute(aElement, aAttribute, aValue); +} + +nsresult +HTMLEditor::RemoveAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + bool aSuppressTransaction) +{ + nsCOMPtr<dom::Element> element = do_QueryInterface(aElement); + NS_ENSURE_TRUE(element, NS_OK); + + nsCOMPtr<nsIAtom> attribute = NS_Atomize(aAttribute); + MOZ_ASSERT(attribute); + + if (IsCSSEnabled() && mCSSEditUtils) { + nsresult rv = + mCSSEditUtils->RemoveCSSEquivalentToHTMLStyle( + element, nullptr, &aAttribute, nullptr, aSuppressTransaction); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!element->HasAttr(kNameSpaceID_None, attribute)) { + return NS_OK; + } + + return aSuppressTransaction ? + element->UnsetAttr(kNameSpaceID_None, attribute, /* aNotify = */ true) : + RemoveAttribute(aElement, aAttribute); +} + +nsresult +HTMLEditor::SetIsCSSEnabled(bool aIsCSSPrefChecked) +{ + if (!mCSSEditUtils) { + return NS_ERROR_NOT_INITIALIZED; + } + + mCSSEditUtils->SetCSSEnabled(aIsCSSPrefChecked); + + // Disable the eEditorNoCSSMask flag if we're enabling StyleWithCSS. + uint32_t flags = mFlags; + if (aIsCSSPrefChecked) { + // Turn off NoCSS as we're enabling CSS + flags &= ~eEditorNoCSSMask; + } else { + // Turn on NoCSS, as we're disabling CSS. + flags |= eEditorNoCSSMask; + } + + return SetFlags(flags); +} + +// Set the block background color +nsresult +HTMLEditor::SetCSSBackgroundColor(const nsAString& aColor) +{ + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + ForceCompositionEnd(); + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + bool isCollapsed = selection->Collapsed(); + + AutoEditBatch batchIt(this); + AutoRules beginRulesSniffing(this, EditAction::insertElement, + nsIEditor::eNext); + AutoSelectionRestorer selectionRestorer(selection, this); + AutoTransactionsConserveSelection dontSpazMySelection(this); + + bool cancel, handled; + TextRulesInfo ruleInfo(EditAction::setTextProperty); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (!cancel && !handled) { + // Loop through the ranges in the selection + NS_NAMED_LITERAL_STRING(bgcolor, "bgcolor"); + for (uint32_t i = 0; i < selection->RangeCount(); i++) { + RefPtr<nsRange> range = selection->GetRangeAt(i); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + nsCOMPtr<Element> cachedBlockParent; + + // Check for easy case: both range endpoints in same text node + nsCOMPtr<nsINode> startNode = range->GetStartParent(); + int32_t startOffset = range->StartOffset(); + nsCOMPtr<nsINode> endNode = range->GetEndParent(); + int32_t endOffset = range->EndOffset(); + if (startNode == endNode && IsTextNode(startNode)) { + // Let's find the block container of the text node + nsCOMPtr<Element> blockParent = GetBlockNodeParent(startNode); + // And apply the background color to that block container + if (blockParent && cachedBlockParent != blockParent) { + cachedBlockParent = blockParent; + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr, + &bgcolor, &aColor, false); + } + } else if (startNode == endNode && + startNode->IsHTMLElement(nsGkAtoms::body) && isCollapsed) { + // No block in the document, let's apply the background to the body + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(startNode->AsElement(), + nullptr, &bgcolor, &aColor, + false); + } else if (startNode == endNode && (endOffset - startOffset == 1 || + (!startOffset && !endOffset))) { + // A unique node is selected, let's also apply the background color to + // the containing block, possibly the node itself + nsCOMPtr<nsIContent> selectedNode = startNode->GetChildAt(startOffset); + nsCOMPtr<Element> blockParent = GetBlock(*selectedNode); + if (blockParent && cachedBlockParent != blockParent) { + cachedBlockParent = blockParent; + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr, + &bgcolor, &aColor, false); + } + } else { + // Not the easy case. Range not contained in single text node. There + // are up to three phases here. There are all the nodes reported by + // the subtree iterator to be processed. And there are potentially a + // starting textnode and an ending textnode which are only partially + // contained by the range. + + // Let's handle the nodes reported by the iterator. These nodes are + // entirely contained in the selection range. We build up a list of + // them (since doing operations on the document during iteration would + // perturb the iterator). + + OwningNonNull<nsIContentIterator> iter = + NS_NewContentSubtreeIterator(); + + nsTArray<OwningNonNull<nsINode>> arrayOfNodes; + nsCOMPtr<nsINode> node; + + // Iterate range and build up array + rv = iter->Init(range); + // Init returns an error if no nodes in range. This can easily happen + // with the subtree iterator if the selection doesn't contain any + // *whole* nodes. + if (NS_SUCCEEDED(rv)) { + for (; !iter->IsDone(); iter->Next()) { + node = do_QueryInterface(iter->GetCurrentNode()); + NS_ENSURE_TRUE(node, NS_ERROR_FAILURE); + + if (IsEditable(node)) { + arrayOfNodes.AppendElement(*node); + } + } + } + // First check the start parent of the range to see if it needs to be + // separately handled (it does if it's a text node, due to how the + // subtree iterator works - it will not have reported it). + if (IsTextNode(startNode) && IsEditable(startNode)) { + nsCOMPtr<Element> blockParent = GetBlockNodeParent(startNode); + if (blockParent && cachedBlockParent != blockParent) { + cachedBlockParent = blockParent; + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr, + &bgcolor, &aColor, + false); + } + } + + // Then loop through the list, set the property on each node + for (auto& node : arrayOfNodes) { + nsCOMPtr<Element> blockParent = GetBlock(node); + if (blockParent && cachedBlockParent != blockParent) { + cachedBlockParent = blockParent; + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr, + &bgcolor, &aColor, + false); + } + } + arrayOfNodes.Clear(); + + // Last, check the end parent of the range to see if it needs to be + // separately handled (it does if it's a text node, due to how the + // subtree iterator works - it will not have reported it). + if (IsTextNode(endNode) && IsEditable(endNode)) { + nsCOMPtr<Element> blockParent = GetBlockNodeParent(endNode); + if (blockParent && cachedBlockParent != blockParent) { + cachedBlockParent = blockParent; + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(blockParent, nullptr, + &bgcolor, &aColor, + false); + } + } + } + } + } + if (!cancel) { + // Post-process + rv = rules->DidDoAction(selection, &ruleInfo, rv); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::SetBackgroundColor(const nsAString& aColor) +{ + if (IsCSSEnabled()) { + // if we are in CSS mode, we have to apply the background color to the + // containing block (or the body if we have no block-level element in + // the document) + return SetCSSBackgroundColor(aColor); + } + + // but in HTML mode, we can only set the document's background color + return SetHTMLBackgroundColor(aColor); +} + +/** + * NodesSameType() does these nodes have the same tag? + */ +bool +HTMLEditor::AreNodesSameType(nsIContent* aNode1, + nsIContent* aNode2) +{ + MOZ_ASSERT(aNode1); + MOZ_ASSERT(aNode2); + + if (aNode1->NodeInfo()->NameAtom() != aNode2->NodeInfo()->NameAtom()) { + return false; + } + + if (!IsCSSEnabled() || !aNode1->IsHTMLElement(nsGkAtoms::span)) { + return true; + } + + // If CSS is enabled, we are stricter about span nodes. + return mCSSEditUtils->ElementsSameStyle(aNode1->AsDOMNode(), + aNode2->AsDOMNode()); +} + +nsresult +HTMLEditor::CopyLastEditableChildStyles(nsIDOMNode* aPreviousBlock, + nsIDOMNode* aNewBlock, + Element** aOutBrNode) +{ + nsCOMPtr<nsINode> newBlock = do_QueryInterface(aNewBlock); + NS_ENSURE_STATE(newBlock || !aNewBlock); + *aOutBrNode = nullptr; + nsCOMPtr<nsIDOMNode> child, tmp; + // first, clear out aNewBlock. Contract is that we want only the styles from previousBlock. + nsresult rv = aNewBlock->GetFirstChild(getter_AddRefs(child)); + while (NS_SUCCEEDED(rv) && child) { + rv = DeleteNode(child); + NS_ENSURE_SUCCESS(rv, rv); + rv = aNewBlock->GetFirstChild(getter_AddRefs(child)); + } + // now find and clone the styles + child = aPreviousBlock; + tmp = aPreviousBlock; + while (tmp) { + child = tmp; + nsCOMPtr<nsINode> child_ = do_QueryInterface(child); + NS_ENSURE_STATE(child_ || !child); + tmp = GetAsDOMNode(GetLastEditableChild(*child_)); + } + while (child && TextEditUtils::IsBreak(child)) { + nsCOMPtr<nsIDOMNode> priorNode; + rv = GetPriorHTMLNode(child, address_of(priorNode)); + NS_ENSURE_SUCCESS(rv, rv); + child = priorNode; + } + nsCOMPtr<Element> newStyles, deepestStyle; + nsCOMPtr<nsINode> childNode = do_QueryInterface(child); + nsCOMPtr<Element> childElement; + if (childNode) { + childElement = childNode->IsElement() ? childNode->AsElement() + : childNode->GetParentElement(); + } + while (childElement && (childElement->AsDOMNode() != aPreviousBlock)) { + if (HTMLEditUtils::IsInlineStyle(childElement) || + childElement->IsHTMLElement(nsGkAtoms::span)) { + if (newStyles) { + newStyles = InsertContainerAbove(newStyles, + childElement->NodeInfo()->NameAtom()); + NS_ENSURE_STATE(newStyles); + } else { + deepestStyle = newStyles = + CreateNode(childElement->NodeInfo()->NameAtom(), newBlock, 0); + NS_ENSURE_STATE(newStyles); + } + CloneAttributes(newStyles, childElement); + } + childElement = childElement->GetParentElement(); + } + if (deepestStyle) { + RefPtr<Element> retVal = CreateBR(deepestStyle, 0); + retVal.forget(aOutBrNode); + NS_ENSURE_STATE(*aOutBrNode); + } + return NS_OK; +} + +nsresult +HTMLEditor::GetElementOrigin(nsIDOMElement* aElement, + int32_t& aX, + int32_t& aY) +{ + aX = 0; + aY = 0; + + NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED); + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsIContent> content = do_QueryInterface(aElement); + nsIFrame *frame = content->GetPrimaryFrame(); + NS_ENSURE_TRUE(frame, NS_OK); + + nsIFrame *container = ps->GetAbsoluteContainingBlock(frame); + NS_ENSURE_TRUE(container, NS_OK); + nsPoint off = frame->GetOffsetTo(container); + aX = nsPresContext::AppUnitsToIntCSSPixels(off.x); + aY = nsPresContext::AppUnitsToIntCSSPixels(off.y); + + return NS_OK; +} + +nsresult +HTMLEditor::EndUpdateViewBatch() +{ + nsresult rv = EditorBase::EndUpdateViewBatch(); + NS_ENSURE_SUCCESS(rv, rv); + + if (mUpdateCount) { + return NS_OK; + } + + // We may need to show resizing handles or update existing ones after + // all transactions are done. This way of doing is preferred to DOM + // mutation events listeners because all the changes the user can apply + // to a document may result in multiple events, some of them quite hard + // to listen too (in particular when an ancestor of the selection is + // changed but the selection itself is not changed). + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + return CheckSelectionStateForAnonymousButtons(selection); +} + +NS_IMETHODIMP +HTMLEditor::GetSelectionContainer(nsIDOMElement** aReturn) +{ + nsCOMPtr<nsIDOMElement> container = + static_cast<nsIDOMElement*>(GetAsDOMNode(GetSelectionContainer())); + NS_ENSURE_TRUE(container, NS_ERROR_FAILURE); + container.forget(aReturn); + return NS_OK; +} + +Element* +HTMLEditor::GetSelectionContainer() +{ + // If we don't get the selection, just skip this + NS_ENSURE_TRUE(GetSelection(), nullptr); + + OwningNonNull<Selection> selection = *GetSelection(); + + nsCOMPtr<nsINode> focusNode; + + if (selection->Collapsed()) { + focusNode = selection->GetFocusNode(); + } else { + int32_t rangeCount = selection->RangeCount(); + + if (rangeCount == 1) { + RefPtr<nsRange> range = selection->GetRangeAt(0); + + nsCOMPtr<nsINode> startContainer = range->GetStartParent(); + int32_t startOffset = range->StartOffset(); + nsCOMPtr<nsINode> endContainer = range->GetEndParent(); + int32_t endOffset = range->EndOffset(); + + if (startContainer == endContainer && startOffset + 1 == endOffset) { + nsCOMPtr<nsIDOMElement> focusElement; + nsresult rv = GetSelectedElement(EmptyString(), + getter_AddRefs(focusElement)); + NS_ENSURE_SUCCESS(rv, nullptr); + if (focusElement) { + focusNode = do_QueryInterface(focusElement); + } + } + if (!focusNode) { + focusNode = range->GetCommonAncestor(); + } + } else { + for (int32_t i = 0; i < rangeCount; i++) { + RefPtr<nsRange> range = selection->GetRangeAt(i); + + nsCOMPtr<nsINode> startContainer = range->GetStartParent(); + if (!focusNode) { + focusNode = startContainer; + } else if (focusNode != startContainer) { + focusNode = startContainer->GetParentNode(); + break; + } + } + } + } + + if (focusNode && focusNode->GetAsText()) { + focusNode = focusNode->GetParentNode(); + } + + if (focusNode && focusNode->IsElement()) { + return focusNode->AsElement(); + } + + return nullptr; +} + +NS_IMETHODIMP +HTMLEditor::IsAnonymousElement(nsIDOMElement* aElement, + bool* aReturn) +{ + NS_ENSURE_TRUE(aElement, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIContent> content = do_QueryInterface(aElement); + *aReturn = content->IsRootOfNativeAnonymousSubtree(); + return NS_OK; +} + +nsresult +HTMLEditor::SetReturnInParagraphCreatesNewParagraph(bool aCreatesNewParagraph) +{ + mCRInParagraphCreatesParagraph = aCreatesNewParagraph; + return NS_OK; +} + +bool +HTMLEditor::GetReturnInParagraphCreatesNewParagraph() +{ + return mCRInParagraphCreatesParagraph; +} + +nsresult +HTMLEditor::GetReturnInParagraphCreatesNewParagraph(bool* aCreatesNewParagraph) +{ + *aCreatesNewParagraph = mCRInParagraphCreatesParagraph; + return NS_OK; +} + +already_AddRefed<nsIContent> +HTMLEditor::GetFocusedContent() +{ + NS_ENSURE_TRUE(mDocWeak, nullptr); + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, nullptr); + + nsCOMPtr<nsIContent> focusedContent = fm->GetFocusedContent(); + + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + bool inDesignMode = doc->HasFlag(NODE_IS_EDITABLE); + if (!focusedContent) { + // in designMode, nobody gets focus in most cases. + if (inDesignMode && OurWindowHasFocus()) { + nsCOMPtr<nsIContent> docRoot = doc->GetRootElement(); + return docRoot.forget(); + } + return nullptr; + } + + if (inDesignMode) { + return OurWindowHasFocus() && + nsContentUtils::ContentIsDescendantOf(focusedContent, doc) ? + focusedContent.forget() : nullptr; + } + + // We're HTML editor for contenteditable + + // If the focused content isn't editable, or it has independent selection, + // we don't have focus. + if (!focusedContent->HasFlag(NODE_IS_EDITABLE) || + focusedContent->HasIndependentSelection()) { + return nullptr; + } + // If our window is focused, we're focused. + return OurWindowHasFocus() ? focusedContent.forget() : nullptr; +} + +already_AddRefed<nsIContent> +HTMLEditor::GetFocusedContentForIME() +{ + nsCOMPtr<nsIContent> focusedContent = GetFocusedContent(); + if (!focusedContent) { + return nullptr; + } + + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, nullptr); + return doc->HasFlag(NODE_IS_EDITABLE) ? nullptr : focusedContent.forget(); +} + +bool +HTMLEditor::IsActiveInDOMWindow() +{ + NS_ENSURE_TRUE(mDocWeak, false); + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, false); + + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + bool inDesignMode = doc->HasFlag(NODE_IS_EDITABLE); + + // If we're in designMode, we're always active in the DOM window. + if (inDesignMode) { + return true; + } + + nsPIDOMWindowOuter* ourWindow = doc->GetWindow(); + nsCOMPtr<nsPIDOMWindowOuter> win; + nsIContent* content = + nsFocusManager::GetFocusedDescendant(ourWindow, false, + getter_AddRefs(win)); + if (!content) { + return false; + } + + // We're HTML editor for contenteditable + + // If the active content isn't editable, or it has independent selection, + // we're not active). + if (!content->HasFlag(NODE_IS_EDITABLE) || + content->HasIndependentSelection()) { + return false; + } + return true; +} + +Element* +HTMLEditor::GetActiveEditingHost() +{ + NS_ENSURE_TRUE(mDocWeak, nullptr); + + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, nullptr); + if (doc->HasFlag(NODE_IS_EDITABLE)) { + return doc->GetBodyElement(); + } + + // We're HTML editor for contenteditable + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, nullptr); + nsCOMPtr<nsIDOMNode> focusNode; + nsresult rv = selection->GetFocusNode(getter_AddRefs(focusNode)); + NS_ENSURE_SUCCESS(rv, nullptr); + nsCOMPtr<nsIContent> content = do_QueryInterface(focusNode); + if (!content) { + return nullptr; + } + + // If the active content isn't editable, or it has independent selection, + // we're not active. + if (!content->HasFlag(NODE_IS_EDITABLE) || + content->HasIndependentSelection()) { + return nullptr; + } + return content->GetEditingHost(); +} + +already_AddRefed<EventTarget> +HTMLEditor::GetDOMEventTarget() +{ + // Don't use getDocument here, because we have no way of knowing + // whether Init() was ever called. So we need to get the document + // ourselves, if it exists. + NS_PRECONDITION(mDocWeak, "This editor has not been initialized yet"); + nsCOMPtr<mozilla::dom::EventTarget> target = do_QueryReferent(mDocWeak); + return target.forget(); +} + +bool +HTMLEditor::ShouldReplaceRootElement() +{ + if (!mRootElement) { + // If we don't know what is our root element, we should find our root. + return true; + } + + // If we temporary set document root element to mRootElement, but there is + // body element now, we should replace the root element by the body element. + nsCOMPtr<nsIDOMHTMLElement> docBody; + GetBodyElement(getter_AddRefs(docBody)); + return !SameCOMIdentity(docBody, mRootElement); +} + +void +HTMLEditor::NotifyRootChanged() +{ + nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this); + + RemoveEventListeners(); + nsresult rv = InstallEventListeners(); + if (NS_FAILED(rv)) { + return; + } + + UpdateRootElement(); + if (!mRootElement) { + return; + } + + rv = BeginningOfDocument(); + if (NS_FAILED(rv)) { + return; + } + + // When this editor has focus, we need to reset the selection limiter to + // new root. Otherwise, that is going to be done when this gets focus. + nsCOMPtr<nsINode> node = GetFocusedNode(); + nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(node); + if (target) { + InitializeSelection(target); + } + + SyncRealTimeSpell(); +} + +nsresult +HTMLEditor::GetBodyElement(nsIDOMHTMLElement** aBody) +{ + NS_PRECONDITION(mDocWeak, "bad state, null mDocWeak"); + nsCOMPtr<nsIDOMHTMLDocument> htmlDoc = do_QueryReferent(mDocWeak); + if (!htmlDoc) { + return NS_ERROR_NOT_INITIALIZED; + } + return htmlDoc->GetBody(aBody); +} + +already_AddRefed<nsINode> +HTMLEditor::GetFocusedNode() +{ + nsCOMPtr<nsIContent> focusedContent = GetFocusedContent(); + if (!focusedContent) { + return nullptr; + } + + nsIFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ASSERTION(fm, "Focus manager is null"); + nsCOMPtr<nsIDOMElement> focusedElement; + fm->GetFocusedElement(getter_AddRefs(focusedElement)); + if (focusedElement) { + nsCOMPtr<nsINode> node = do_QueryInterface(focusedElement); + return node.forget(); + } + + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + return doc.forget(); +} + +bool +HTMLEditor::OurWindowHasFocus() +{ + NS_ENSURE_TRUE(mDocWeak, false); + nsIFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, false); + nsCOMPtr<mozIDOMWindowProxy> focusedWindow; + fm->GetFocusedWindow(getter_AddRefs(focusedWindow)); + if (!focusedWindow) { + return false; + } + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + nsPIDOMWindowOuter* ourWindow = doc->GetWindow(); + return ourWindow == focusedWindow; +} + +bool +HTMLEditor::IsAcceptableInputEvent(nsIDOMEvent* aEvent) +{ + if (!EditorBase::IsAcceptableInputEvent(aEvent)) { + return false; + } + + // While there is composition, all composition events in its top level window + // are always fired on the composing editor. Therefore, if this editor has + // composition, the composition events should be handled in this editor. + if (mComposition && aEvent->WidgetEventPtr()->AsCompositionEvent()) { + return true; + } + + NS_ENSURE_TRUE(mDocWeak, false); + + nsCOMPtr<nsIDOMEventTarget> target; + aEvent->GetTarget(getter_AddRefs(target)); + NS_ENSURE_TRUE(target, false); + + nsCOMPtr<nsIDocument> document = do_QueryReferent(mDocWeak); + if (document->HasFlag(NODE_IS_EDITABLE)) { + // If this editor is in designMode and the event target is the document, + // the event is for this editor. + nsCOMPtr<nsIDocument> targetDocument = do_QueryInterface(target); + if (targetDocument) { + return targetDocument == document; + } + // Otherwise, check whether the event target is in this document or not. + nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target); + NS_ENSURE_TRUE(targetContent, false); + return document == targetContent->GetUncomposedDoc(); + } + + // This HTML editor is for contenteditable. We need to check the validity of + // the target. + nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target); + NS_ENSURE_TRUE(targetContent, false); + + // If the event is a mouse event, we need to check if the target content is + // the focused editing host or its descendant. + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent); + if (mouseEvent) { + nsIContent* editingHost = GetActiveEditingHost(); + // If there is no active editing host, we cannot handle the mouse event + // correctly. + if (!editingHost) { + return false; + } + // If clicked on non-editable root element but the body element is the + // active editing host, we should assume that the click event is targetted. + if (targetContent == document->GetRootElement() && + !targetContent->HasFlag(NODE_IS_EDITABLE) && + editingHost == document->GetBodyElement()) { + targetContent = editingHost; + } + // If the target element is neither the active editing host nor a descendant + // of it, we may not be able to handle the event. + if (!nsContentUtils::ContentIsDescendantOf(targetContent, editingHost)) { + return false; + } + // If the clicked element has an independent selection, we shouldn't + // handle this click event. + if (targetContent->HasIndependentSelection()) { + return false; + } + // If the target content is editable, we should handle this event. + return targetContent->HasFlag(NODE_IS_EDITABLE); + } + + // If the target of the other events which target focused element isn't + // editable or has an independent selection, this editor shouldn't handle the + // event. + if (!targetContent->HasFlag(NODE_IS_EDITABLE) || + targetContent->HasIndependentSelection()) { + return false; + } + + // Finally, check whether we're actually focused or not. When we're not + // focused, we should ignore the dispatched event by script (or something) + // because content editable element needs selection in itself for editing. + // However, when we're not focused, it's not guaranteed. + return IsActiveInDOMWindow(); +} + +NS_IMETHODIMP +HTMLEditor::GetPreferredIMEState(IMEState* aState) +{ + // HTML editor don't prefer the CSS ime-mode because IE didn't do so too. + aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE; + if (IsReadonly() || IsDisabled()) { + aState->mEnabled = IMEState::DISABLED; + } else { + aState->mEnabled = IMEState::ENABLED; + } + return NS_OK; +} + +already_AddRefed<nsIContent> +HTMLEditor::GetInputEventTargetContent() +{ + nsCOMPtr<nsIContent> target = GetActiveEditingHost(); + return target.forget(); +} + +bool +HTMLEditor::IsEditable(nsINode* aNode) +{ + if (!TextEditor::IsEditable(aNode)) { + return false; + } + if (aNode->IsElement()) { + // If we're dealing with an element, then ask it whether it's editable. + return aNode->IsEditable(); + } + // We might be dealing with a text node for example, which we always consider + // to be editable. + return true; +} + +Element* +HTMLEditor::GetEditorRoot() +{ + return GetActiveEditingHost(); +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h new file mode 100644 index 000000000..477ec9741 --- /dev/null +++ b/editor/libeditor/HTMLEditor.h @@ -0,0 +1,1109 @@ +/* -*- 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/. */ + +#ifndef mozilla_HTMLEditor_h +#define mozilla_HTMLEditor_h + +#include "mozilla/Attributes.h" +#include "mozilla/CSSEditUtils.h" +#include "mozilla/StyleSheet.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/File.h" + +#include "nsAttrName.h" +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "nsIContentFilter.h" +#include "nsICSSLoaderObserver.h" +#include "nsIDocumentObserver.h" +#include "nsIDOMElement.h" +#include "nsIDOMEventListener.h" +#include "nsIEditor.h" +#include "nsIEditorMailSupport.h" +#include "nsIEditorStyleSheets.h" +#include "nsIEditorUtils.h" +#include "nsIEditRules.h" +#include "nsIHTMLAbsPosEditor.h" +#include "nsIHTMLEditor.h" +#include "nsIHTMLInlineTableEditor.h" +#include "nsIHTMLObjectResizeListener.h" +#include "nsIHTMLObjectResizer.h" +#include "nsISelectionListener.h" +#include "nsITableEditor.h" +#include "nsPoint.h" +#include "nsStubMutationObserver.h" +#include "nsTArray.h" + +class nsDocumentFragment; +class nsIDOMKeyEvent; +class nsITransferable; +class nsIClipboard; +class nsILinkHandler; +class nsTableWrapperFrame; +class nsIDOMRange; +class nsRange; + +namespace mozilla { + +class HTMLEditorEventListener; +class HTMLEditRules; +class TextEditRules; +class TypeInState; +class WSRunObject; +struct PropItem; +template<class T> class OwningNonNull; +namespace dom { +class DocumentFragment; +} // namespace dom +namespace widget { +struct IMEState; +} // namespace widget + +/** + * The HTML editor implementation.<br> + * Use to edit HTML document represented as a DOM tree. + */ +class HTMLEditor final : public TextEditor + , public nsIHTMLEditor + , public nsIHTMLObjectResizer + , public nsIHTMLAbsPosEditor + , public nsITableEditor + , public nsIHTMLInlineTableEditor + , public nsIEditorStyleSheets + , public nsICSSLoaderObserver + , public nsStubMutationObserver +{ +private: + enum BlockTransformationType + { + eNoOp, + eReplaceParent = 1, + eInsertParent = 2 + }; + + const char16_t kNBSP = 160; + +public: + enum ResizingRequestID + { + kX = 0, + kY = 1, + kWidth = 2, + kHeight = 3 + }; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLEditor, TextEditor) + + HTMLEditor(); + + bool GetReturnInParagraphCreatesNewParagraph(); + Element* GetSelectionContainer(); + + // TextEditor overrides + NS_IMETHOD GetIsDocumentEditable(bool* aIsDocumentEditable) override; + NS_IMETHOD BeginningOfDocument() override; + virtual nsresult HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) override; + virtual already_AddRefed<nsIContent> GetFocusedContent() override; + virtual already_AddRefed<nsIContent> GetFocusedContentForIME() override; + virtual bool IsActiveInDOMWindow() override; + virtual already_AddRefed<dom::EventTarget> GetDOMEventTarget() override; + virtual Element* GetEditorRoot() override; + virtual already_AddRefed<nsIContent> FindSelectionRoot( + nsINode *aNode) override; + virtual bool IsAcceptableInputEvent(nsIDOMEvent* aEvent) override; + virtual already_AddRefed<nsIContent> GetInputEventTargetContent() override; + virtual bool IsEditable(nsINode* aNode) override; + using EditorBase::IsEditable; + + // nsStubMutationObserver overrides + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + // nsIEditorIMESupport overrides + NS_IMETHOD GetPreferredIMEState(widget::IMEState* aState) override; + + // nsIHTMLEditor methods + NS_DECL_NSIHTMLEDITOR + + // nsIHTMLObjectResizer methods (implemented in HTMLObjectResizer.cpp) + NS_DECL_NSIHTMLOBJECTRESIZER + + // nsIHTMLAbsPosEditor methods (implemented in HTMLAbsPositionEditor.cpp) + NS_DECL_NSIHTMLABSPOSEDITOR + + // nsIHTMLInlineTableEditor methods (implemented in HTMLInlineTableEditor.cpp) + NS_DECL_NSIHTMLINLINETABLEEDITOR + + // XXX Following methods are not overriding but defined here... + nsresult CopyLastEditableChildStyles(nsIDOMNode* aPreviousBlock, + nsIDOMNode* aNewBlock, + Element** aOutBrNode); + + nsresult LoadHTML(const nsAString& aInputString); + + nsresult GetCSSBackgroundColorState(bool* aMixed, nsAString& aOutColor, + bool aBlockLevel); + NS_IMETHOD GetHTMLBackgroundColorState(bool* aMixed, nsAString& outColor); + + // nsIEditorStyleSheets methods + NS_IMETHOD AddStyleSheet(const nsAString& aURL) override; + NS_IMETHOD ReplaceStyleSheet(const nsAString& aURL) override; + NS_IMETHOD RemoveStyleSheet(const nsAString &aURL) override; + + NS_IMETHOD AddOverrideStyleSheet(const nsAString& aURL) override; + NS_IMETHOD ReplaceOverrideStyleSheet(const nsAString& aURL) override; + NS_IMETHOD RemoveOverrideStyleSheet(const nsAString &aURL) override; + + NS_IMETHOD EnableStyleSheet(const nsAString& aURL, bool aEnable) override; + + // nsIEditorMailSupport methods + NS_DECL_NSIEDITORMAILSUPPORT + + // nsITableEditor methods + NS_IMETHOD InsertTableCell(int32_t aNumber, bool aAfter) override; + NS_IMETHOD InsertTableColumn(int32_t aNumber, bool aAfter) override; + NS_IMETHOD InsertTableRow(int32_t aNumber, bool aAfter) override; + NS_IMETHOD DeleteTable() override; + NS_IMETHOD DeleteTableCell(int32_t aNumber) override; + NS_IMETHOD DeleteTableCellContents() override; + NS_IMETHOD DeleteTableColumn(int32_t aNumber) override; + NS_IMETHOD DeleteTableRow(int32_t aNumber) override; + NS_IMETHOD SelectTableCell() override; + NS_IMETHOD SelectBlockOfCells(nsIDOMElement* aStartCell, + nsIDOMElement* aEndCell) override; + NS_IMETHOD SelectTableRow() override; + NS_IMETHOD SelectTableColumn() override; + NS_IMETHOD SelectTable() override; + NS_IMETHOD SelectAllTableCells() override; + NS_IMETHOD SwitchTableCellHeaderType(nsIDOMElement* aSourceCell, + nsIDOMElement** aNewCell) override; + NS_IMETHOD JoinTableCells(bool aMergeNonContiguousContents) override; + NS_IMETHOD SplitTableCell() override; + NS_IMETHOD NormalizeTable(nsIDOMElement* aTable) override; + NS_IMETHOD GetCellIndexes(nsIDOMElement* aCell, + int32_t* aRowIndex, int32_t* aColIndex) override; + NS_IMETHOD GetTableSize(nsIDOMElement* aTable, + int32_t* aRowCount, int32_t* aColCount) override; + NS_IMETHOD GetCellAt(nsIDOMElement* aTable, int32_t aRowIndex, + int32_t aColIndex, nsIDOMElement **aCell) override; + NS_IMETHOD GetCellDataAt(nsIDOMElement* aTable, + int32_t aRowIndex, int32_t aColIndex, + nsIDOMElement** aCell, + int32_t* aStartRowIndex, int32_t* aStartColIndex, + int32_t* aRowSpan, int32_t* aColSpan, + int32_t* aActualRowSpan, int32_t* aActualColSpan, + bool* aIsSelected) override; + NS_IMETHOD GetFirstRow(nsIDOMElement* aTableElement, + nsIDOMNode** aRowNode) override; + NS_IMETHOD GetNextRow(nsIDOMNode* aCurrentRowNode, + nsIDOMNode** aRowNode) override; + nsresult GetLastCellInRow(nsIDOMNode* aRowNode, + nsIDOMNode** aCellNode); + + NS_IMETHOD SetSelectionAfterTableEdit(nsIDOMElement* aTable, int32_t aRow, + int32_t aCol, int32_t aDirection, + bool aSelected) override; + NS_IMETHOD GetSelectedOrParentTableElement( + nsAString& aTagName, int32_t* aSelectedCount, + nsIDOMElement** aTableElement) override; + NS_IMETHOD GetSelectedCellsType(nsIDOMElement* aElement, + uint32_t* aSelectionType) override; + + nsresult GetCellFromRange(nsRange* aRange, nsIDOMElement** aCell); + + /** + * Finds the first selected cell in first range of selection + * This is in the *order of selection*, not order in the table + * (i.e., each cell added to selection is added in another range + * in the selection's rangelist, independent of location in table) + * aRange is optional: returns the range around the cell. + */ + NS_IMETHOD GetFirstSelectedCell(nsIDOMRange** aRange, + nsIDOMElement** aCell) override; + /** + * Get next cell until no more are found. Always use GetFirstSelected cell + * first aRange is optional: returns the range around the cell. + */ + NS_IMETHOD GetNextSelectedCell(nsIDOMRange** aRange, + nsIDOMElement** aCell) override; + + /** + * Upper-left-most selected cell in table. + */ + NS_IMETHOD GetFirstSelectedCellInTable(int32_t* aRowIndex, int32_t* aColIndex, + nsIDOMElement** aCell) override; + + // Miscellaneous + + /** + * This sets background on the appropriate container element (table, cell,) + * or calls into nsTextEditor to set the page background. + */ + nsresult SetCSSBackgroundColor(const nsAString& aColor); + nsresult SetHTMLBackgroundColor(const nsAString& aColor); + + // Block methods moved from EditorBase + static Element* GetBlockNodeParent(nsINode* aNode); + static nsIDOMNode* GetBlockNodeParent(nsIDOMNode* aNode); + static Element* GetBlock(nsINode& aNode); + + void IsNextCharInNodeWhitespace(nsIContent* aContent, + int32_t aOffset, + bool* outIsSpace, + bool* outIsNBSP, + nsIContent** outNode = nullptr, + int32_t* outOffset = 0); + void IsPrevCharInNodeWhitespace(nsIContent* aContent, + int32_t aOffset, + bool* outIsSpace, + bool* outIsNBSP, + nsIContent** outNode = nullptr, + int32_t* outOffset = 0); + + // Overrides of EditorBase interface methods + nsresult EndUpdateViewBatch() override; + + NS_IMETHOD Init(nsIDOMDocument* aDoc, nsIContent* aRoot, + nsISelectionController* aSelCon, uint32_t aFlags, + const nsAString& aValue) override; + NS_IMETHOD PreDestroy(bool aDestroyingFrames) override; + + /** + * @param aElement Must not be null. + */ + static bool NodeIsBlockStatic(const nsINode* aElement); + static nsresult NodeIsBlockStatic(nsIDOMNode *aNode, bool *aIsBlock); + +protected: + virtual ~HTMLEditor(); + + using EditorBase::IsBlockNode; + virtual bool IsBlockNode(nsINode *aNode) override; + +public: + // XXX Why don't we move following methods above for grouping by the origins? + NS_IMETHOD SetFlags(uint32_t aFlags) override; + + NS_IMETHOD Paste(int32_t aSelectionType) override; + NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override; + + NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override; + NS_IMETHOD CanPasteTransferable(nsITransferable* aTransferable, + bool* aCanPaste) override; + + NS_IMETHOD DebugUnitTests(int32_t* outNumTests, + int32_t* outNumTestsFailed) override; + + /** + * All editor operations which alter the doc should be prefaced + * with a call to StartOperation, naming the action and direction. + */ + NS_IMETHOD StartOperation(EditAction opID, + nsIEditor::EDirection aDirection) override; + + /** + * All editor operations which alter the doc should be followed + * with a call to EndOperation. + */ + NS_IMETHOD EndOperation() override; + + /** + * returns true if aParentTag can contain a child of type aChildTag. + */ + virtual bool TagCanContainTag(nsIAtom& aParentTag, + nsIAtom& aChildTag) override; + + /** + * Returns true if aNode is a container. + */ + virtual bool IsContainer(nsINode* aNode) override; + virtual bool IsContainer(nsIDOMNode* aNode) override; + + /** + * Make the given selection span the entire document. + */ + virtual nsresult SelectEntireDocument(Selection* aSelection) override; + + NS_IMETHOD SetAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue, + bool aSuppressTransaction) override; + NS_IMETHOD RemoveAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + bool aSuppressTransaction) override; + + /** + * Join together any adjacent editable text nodes in the range. + */ + nsresult CollapseAdjacentTextNodes(nsRange* aRange); + + virtual bool AreNodesSameType(nsIContent* aNode1, + nsIContent* aNode2) override; + + NS_IMETHOD DeleteSelectionImpl(EDirection aAction, + EStripWrappers aStripWrappers) override; + nsresult DeleteNode(nsINode* aNode); + NS_IMETHOD DeleteNode(nsIDOMNode* aNode) override; + nsresult DeleteText(nsGenericDOMDataNode& aTextNode, uint32_t aOffset, + uint32_t aLength); + virtual nsresult InsertTextImpl(const nsAString& aStringToInsert, + nsCOMPtr<nsINode>* aInOutNode, + int32_t* aInOutOffset, + nsIDocument* aDoc) override; + NS_IMETHOD_(bool) IsModifiableNode(nsIDOMNode* aNode) override; + virtual bool IsModifiableNode(nsINode* aNode) override; + + NS_IMETHOD GetIsSelectionEditable(bool* aIsSelectionEditable) override; + + NS_IMETHOD SelectAll() override; + + // nsICSSLoaderObserver + NS_IMETHOD StyleSheetLoaded(StyleSheet* aSheet, + bool aWasAlternate, nsresult aStatus) override; + + // Utility Routines, not part of public API + NS_IMETHOD TypedText(const nsAString& aString, + ETypingAction aAction) override; + nsresult InsertNodeAtPoint(nsIDOMNode* aNode, + nsCOMPtr<nsIDOMNode>* ioParent, + int32_t* ioOffset, + bool aNoEmptyNodes); + + /** + * Use this to assure that selection is set after attribute nodes when + * trying to collapse selection at begining of a block node + * e.g., when setting at beginning of a table cell + * This will stop at a table, however, since we don't want to + * "drill down" into nested tables. + * @param aSelection Optional. If null, we get current selection. + */ + void CollapseSelectionToDeepestNonTableFirstChild(Selection* aSelection, + nsINode* aNode); + + /** + * aNode must be a non-null text node. + * outIsEmptyNode must be non-null. + */ + nsresult IsVisTextNode(nsIContent* aNode, + bool* outIsEmptyNode, + bool aSafeToAskFrames); + nsresult IsEmptyNode(nsIDOMNode* aNode, bool* outIsEmptyBlock, + bool aMozBRDoesntCount = false, + bool aListOrCellNotEmpty = false, + bool aSafeToAskFrames = false); + nsresult IsEmptyNode(nsINode* aNode, bool* outIsEmptyBlock, + bool aMozBRDoesntCount = false, + bool aListOrCellNotEmpty = false, + bool aSafeToAskFrames = false); + nsresult IsEmptyNodeImpl(nsINode* aNode, + bool* outIsEmptyBlock, + bool aMozBRDoesntCount, + bool aListOrCellNotEmpty, + bool aSafeToAskFrames, + bool* aSeenBR); + + /** + * Returns TRUE if sheet was loaded, false if it wasn't. + */ + bool EnableExistingStyleSheet(const nsAString& aURL); + + /** + * Dealing with the internal style sheet lists. + */ + StyleSheet* GetStyleSheetForURL(const nsAString& aURL); + void GetURLForStyleSheet(StyleSheet* aStyleSheet, + nsAString& aURL); + + /** + * Add a url + known style sheet to the internal lists. + */ + nsresult AddNewStyleSheetToList(const nsAString &aURL, + StyleSheet* aStyleSheet); + nsresult RemoveStyleSheetFromList(const nsAString &aURL); + + bool IsCSSEnabled() + { + // TODO: removal of mCSSAware and use only the presence of mCSSEditUtils + return mCSSAware && mCSSEditUtils && mCSSEditUtils->IsCSSPrefChecked(); + } + + static bool HasAttributes(Element* aElement) + { + MOZ_ASSERT(aElement); + uint32_t attrCount = aElement->GetAttrCount(); + return attrCount > 1 || + (1 == attrCount && + !aElement->GetAttrNameAt(0)->Equals(nsGkAtoms::mozdirty)); + } + +protected: + class BlobReader final : public nsIEditorBlobListener + { + public: + BlobReader(dom::BlobImpl* aBlob, HTMLEditor* aHTMLEditor, + bool aIsSafe, nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, int32_t aDestOffset, + bool aDoDeleteSelection); + + NS_DECL_ISUPPORTS + NS_DECL_NSIEDITORBLOBLISTENER + + private: + ~BlobReader() + { + } + + RefPtr<dom::BlobImpl> mBlob; + RefPtr<HTMLEditor> mHTMLEditor; + bool mIsSafe; + nsCOMPtr<nsIDOMDocument> mSourceDoc; + nsCOMPtr<nsIDOMNode> mDestinationNode; + int32_t mDestOffset; + bool mDoDeleteSelection; + }; + + NS_IMETHOD InitRules() override; + + virtual void CreateEventListeners() override; + virtual nsresult InstallEventListeners() override; + virtual void RemoveEventListeners() override; + + bool ShouldReplaceRootElement(); + void NotifyRootChanged(); + nsresult GetBodyElement(nsIDOMHTMLElement** aBody); + + /** + * Get the focused node of this editor. + * @return If the editor has focus, this returns the focused node. + * Otherwise, returns null. + */ + already_AddRefed<nsINode> GetFocusedNode(); + + /** + * Return TRUE if aElement is a table-related elemet and caret was set. + */ + bool SetCaretInTableCell(nsIDOMElement* aElement); + + NS_IMETHOD TabInTable(bool inIsShift, bool* outHandled); + already_AddRefed<Element> CreateBR(nsINode* aNode, int32_t aOffset, + EDirection aSelect = eNone); + NS_IMETHOD CreateBR( + nsIDOMNode* aNode, int32_t aOffset, + nsCOMPtr<nsIDOMNode>* outBRNode, + nsIEditor::EDirection aSelect = nsIEditor::eNone) override; + + // Table Editing (implemented in nsTableEditor.cpp) + + /** + * Insert a new cell after or before supplied aCell. + * Optional: If aNewCell supplied, returns the newly-created cell (addref'd, + * of course) + * This doesn't change or use the current selection. + */ + NS_IMETHOD InsertCell(nsIDOMElement* aCell, int32_t aRowSpan, + int32_t aColSpan, bool aAfter, bool aIsHeader, + nsIDOMElement** aNewCell); + + /** + * Helpers that don't touch the selection or do batch transactions. + */ + NS_IMETHOD DeleteRow(nsIDOMElement* aTable, int32_t aRowIndex); + NS_IMETHOD DeleteColumn(nsIDOMElement* aTable, int32_t aColIndex); + NS_IMETHOD DeleteCellContents(nsIDOMElement* aCell); + + /** + * Move all contents from aCellToMerge into aTargetCell (append at end). + */ + NS_IMETHOD MergeCells(nsCOMPtr<nsIDOMElement> aTargetCell, + nsCOMPtr<nsIDOMElement> aCellToMerge, + bool aDeleteCellToMerge); + + nsresult DeleteTable2(nsIDOMElement* aTable, Selection* aSelection); + NS_IMETHOD SetColSpan(nsIDOMElement* aCell, int32_t aColSpan); + NS_IMETHOD SetRowSpan(nsIDOMElement* aCell, int32_t aRowSpan); + + /** + * Helper used to get nsTableWrapperFrame for a table. + */ + nsTableWrapperFrame* GetTableFrame(nsIDOMElement* aTable); + + /** + * Needed to do appropriate deleting when last cell or row is about to be + * deleted. This doesn't count cells that don't start in the given row (are + * spanning from row above). + */ + int32_t GetNumberOfCellsInRow(nsIDOMElement* aTable, int32_t rowIndex); + + /** + * Test if all cells in row or column at given index are selected. + */ + bool AllCellsInRowSelected(nsIDOMElement* aTable, int32_t aRowIndex, + int32_t aNumberOfColumns); + bool AllCellsInColumnSelected(nsIDOMElement* aTable, int32_t aColIndex, + int32_t aNumberOfRows); + + bool IsEmptyCell(Element* aCell); + + /** + * Most insert methods need to get the same basic context data. + * Any of the pointers may be null if you don't need that datum (for more + * efficiency). + * Input: *aCell is a known cell, + * if null, cell is obtained from the anchor node of the selection. + * Returns NS_EDITOR_ELEMENT_NOT_FOUND if cell is not found even if aCell is + * null. + */ + nsresult GetCellContext(Selection** aSelection, nsIDOMElement** aTable, + nsIDOMElement** aCell, nsIDOMNode** aCellParent, + int32_t* aCellOffset, int32_t* aRowIndex, + int32_t* aColIndex); + + NS_IMETHOD GetCellSpansAt(nsIDOMElement* aTable, int32_t aRowIndex, + int32_t aColIndex, int32_t& aActualRowSpan, + int32_t& aActualColSpan); + + NS_IMETHOD SplitCellIntoColumns(nsIDOMElement* aTable, int32_t aRowIndex, + int32_t aColIndex, int32_t aColSpanLeft, + int32_t aColSpanRight, + nsIDOMElement** aNewCell); + + NS_IMETHOD SplitCellIntoRows(nsIDOMElement* aTable, int32_t aRowIndex, + int32_t aColIndex, int32_t aRowSpanAbove, + int32_t aRowSpanBelow, nsIDOMElement** aNewCell); + + nsresult CopyCellBackgroundColor(nsIDOMElement* destCell, + nsIDOMElement* sourceCell); + + /** + * Reduce rowspan/colspan when cells span into nonexistent rows/columns. + */ + NS_IMETHOD FixBadRowSpan(nsIDOMElement* aTable, int32_t aRowIndex, + int32_t& aNewRowCount); + NS_IMETHOD FixBadColSpan(nsIDOMElement* aTable, int32_t aColIndex, + int32_t& aNewColCount); + + /** + * Fallback method: Call this after using ClearSelection() and you + * failed to set selection to some other content in the document. + */ + nsresult SetSelectionAtDocumentStart(Selection* aSelection); + + // End of Table Editing utilities + + static Element* GetEnclosingTable(nsINode* aNode); + static nsIDOMNode* GetEnclosingTable(nsIDOMNode* aNode); + + /** + * Content-based query returns true if <aProperty aAttribute=aValue> effects + * aNode. If <aProperty aAttribute=aValue> contains aNode, but + * <aProperty aAttribute=SomeOtherValue> also contains aNode and the second is + * more deeply nested than the first, then the first does not effect aNode. + * + * @param aNode The target of the query + * @param aProperty The property that we are querying for + * @param aAttribute The attribute of aProperty, example: color in + * <FONT color="blue"> May be null. + * @param aValue The value of aAttribute, example: blue in + * <FONT color="blue"> May be null. Ignored if aAttribute + * is null. + * @param aIsSet [OUT] true if <aProperty aAttribute=aValue> effects + * aNode. + * @param outValue [OUT] the value of the attribute, if aIsSet is true + * + * The nsIContent variant returns aIsSet instead of using an out parameter. + */ + bool IsTextPropertySetByContent(nsINode* aNode, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString* aValue, + nsAString* outValue = nullptr); + + void IsTextPropertySetByContent(nsIDOMNode* aNode, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool& aIsSet, + nsAString* outValue = nullptr); + + // Methods for handling plaintext quotations + NS_IMETHOD PasteAsPlaintextQuotation(int32_t aSelectionType); + + /** + * Insert a string as quoted text, replacing the selected text (if any). + * @param aQuotedText The string to insert. + * @param aAddCites Whether to prepend extra ">" to each line + * (usually true, unless those characters + * have already been added.) + * @return aNodeInserted The node spanning the insertion, if applicable. + * If aAddCites is false, this will be null. + */ + NS_IMETHOD InsertAsPlaintextQuotation(const nsAString& aQuotedText, + bool aAddCites, + nsIDOMNode** aNodeInserted); + + nsresult InsertObject(const nsACString& aType, nsISupports* aObject, + bool aIsSafe, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection); + + // factored methods for handling insertion of data from transferables + // (drag&drop or clipboard) + NS_IMETHOD PrepareTransferable(nsITransferable** transferable) override; + nsresult PrepareHTMLTransferable(nsITransferable** transferable); + nsresult InsertFromTransferable(nsITransferable* transferable, + nsIDOMDocument* aSourceDoc, + const nsAString& aContextStr, + const nsAString& aInfoStr, + bool havePrivateHTMLFlavor, + nsIDOMNode *aDestinationNode, + int32_t aDestinationOffset, + bool aDoDeleteSelection); + nsresult InsertFromDataTransfer(dom::DataTransfer* aDataTransfer, + int32_t aIndex, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) override; + bool HavePrivateHTMLFlavor(nsIClipboard* clipboard ); + nsresult ParseCFHTML(nsCString& aCfhtml, char16_t** aStuffToPaste, + char16_t** aCfcontext); + nsresult DoContentFilterCallback(const nsAString& aFlavor, + nsIDOMDocument* aSourceDoc, + bool aWillDeleteSelection, + nsIDOMNode** aFragmentAsNode, + nsIDOMNode** aFragStartNode, + int32_t* aFragStartOffset, + nsIDOMNode** aFragEndNode, + int32_t* aFragEndOffset, + nsIDOMNode** aTargetNode, + int32_t* aTargetOffset, + bool* aDoContinue); + + bool IsInLink(nsIDOMNode* aNode, nsCOMPtr<nsIDOMNode>* outLink = nullptr); + nsresult StripFormattingNodes(nsIContent& aNode, bool aOnlyList = false); + nsresult CreateDOMFragmentFromPaste(const nsAString& aInputString, + const nsAString& aContextStr, + const nsAString& aInfoStr, + nsCOMPtr<nsIDOMNode>* outFragNode, + nsCOMPtr<nsIDOMNode>* outStartNode, + nsCOMPtr<nsIDOMNode>* outEndNode, + int32_t* outStartOffset, + int32_t* outEndOffset, + bool aTrustedInput); + nsresult ParseFragment(const nsAString& aStr, nsIAtom* aContextLocalName, + nsIDocument* aTargetDoc, + dom::DocumentFragment** aFragment, bool aTrustedInput); + void CreateListOfNodesToPaste(dom::DocumentFragment& aFragment, + nsTArray<OwningNonNull<nsINode>>& outNodeList, + nsINode* aStartNode, + int32_t aStartOffset, + nsINode* aEndNode, + int32_t aEndOffset); + nsresult CreateTagStack(nsTArray<nsString>& aTagStack, + nsIDOMNode* aNode); + enum class StartOrEnd { start, end }; + void GetListAndTableParents(StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodeList, + nsTArray<OwningNonNull<Element>>& outArray); + int32_t DiscoverPartialListsAndTables( + nsTArray<OwningNonNull<nsINode>>& aPasteNodes, + nsTArray<OwningNonNull<Element>>& aListsAndTables); + nsINode* ScanForListAndTableStructure( + StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodes, + Element& aListOrTable); + void ReplaceOrphanedStructure( + StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodeArray, + nsTArray<OwningNonNull<Element>>& aListAndTableArray, + int32_t aHighWaterMark); + + /** + * Small utility routine to test if a break node is visible to user. + */ + bool IsVisBreak(nsINode* aNode); + + /** + * Utility routine to possibly adjust the insertion position when + * inserting a block level element. + */ + void NormalizeEOLInsertPosition(nsINode* firstNodeToInsert, + nsCOMPtr<nsIDOMNode>* insertParentNode, + int32_t* insertOffset); + + /** + * Small utility routine to test the eEditorReadonly bit. + */ + bool IsModifiable(); + + /** + * Helpers for block transformations. + */ + nsresult MakeDefinitionItem(const nsAString& aItemType); + nsresult InsertBasicBlock(const nsAString& aBlockType); + + /** + * Increase/decrease the font size of selection. + */ + enum class FontSize { incr, decr }; + nsresult RelativeFontChange(FontSize aDir); + + /** + * Helper routines for font size changing. + */ + nsresult RelativeFontChangeOnTextNode(FontSize aDir, + Text& aTextNode, + int32_t aStartOffset, + int32_t aEndOffset); + nsresult RelativeFontChangeOnNode(int32_t aSizeChange, nsIContent* aNode); + nsresult RelativeFontChangeHelper(int32_t aSizeChange, nsINode* aNode); + + /** + * Helper routines for inline style. + */ + nsresult SetInlinePropertyOnTextNode(Text& aData, + int32_t aStartOffset, + int32_t aEndOffset, + nsIAtom& aProperty, + const nsAString* aAttribute, + const nsAString& aValue); + nsresult SetInlinePropertyOnNode(nsIContent& aNode, + nsIAtom& aProperty, + const nsAString* aAttribute, + const nsAString& aValue); + + nsresult PromoteInlineRange(nsRange& aRange); + nsresult PromoteRangeIfStartsOrEndsInNamedAnchor(nsRange& aRange); + nsresult SplitStyleAboveRange(nsRange* aRange, + nsIAtom* aProperty, + const nsAString* aAttribute); + nsresult SplitStyleAbovePoint(nsCOMPtr<nsINode>* aNode, int32_t* aOffset, + nsIAtom* aProperty, + const nsAString* aAttribute, + nsIContent** aOutLeftNode = nullptr, + nsIContent** aOutRightNode = nullptr); + nsresult ApplyDefaultProperties(); + nsresult RemoveStyleInside(nsIContent& aNode, + nsIAtom* aProperty, + const nsAString* aAttribute, + const bool aChildrenOnly = false); + nsresult RemoveInlinePropertyImpl(nsIAtom* aProperty, + const nsAString* aAttribute); + + bool NodeIsProperty(nsINode& aNode); + bool IsAtFrontOfNode(nsINode& aNode, int32_t aOffset); + bool IsAtEndOfNode(nsINode& aNode, int32_t aOffset); + bool IsOnlyAttribute(const nsIContent* aElement, const nsAString& aAttribute); + + nsresult RemoveBlockContainer(nsIContent& aNode); + + nsIContent* GetPriorHTMLSibling(nsINode* aNode); + nsresult GetPriorHTMLSibling(nsIDOMNode*inNode, + nsCOMPtr<nsIDOMNode>* outNode); + nsIContent* GetPriorHTMLSibling(nsINode* aParent, int32_t aOffset); + nsresult GetPriorHTMLSibling(nsIDOMNode* inParent, int32_t inOffset, + nsCOMPtr<nsIDOMNode>* outNode); + + nsIContent* GetNextHTMLSibling(nsINode* aNode); + nsresult GetNextHTMLSibling(nsIDOMNode* inNode, + nsCOMPtr<nsIDOMNode>* outNode); + nsIContent* GetNextHTMLSibling(nsINode* aParent, int32_t aOffset); + nsresult GetNextHTMLSibling(nsIDOMNode* inParent, int32_t inOffset, + nsCOMPtr<nsIDOMNode>* outNode); + + nsIContent* GetPriorHTMLNode(nsINode* aNode, bool aNoBlockCrossing = false); + nsresult GetPriorHTMLNode(nsIDOMNode* inNode, nsCOMPtr<nsIDOMNode>* outNode, + bool bNoBlockCrossing = false); + nsIContent* GetPriorHTMLNode(nsINode* aParent, int32_t aOffset, + bool aNoBlockCrossing = false); + nsresult GetPriorHTMLNode(nsIDOMNode* inParent, int32_t inOffset, + nsCOMPtr<nsIDOMNode>* outNode, + bool bNoBlockCrossing = false); + + nsIContent* GetNextHTMLNode(nsINode* aNode, bool aNoBlockCrossing = false); + nsresult GetNextHTMLNode(nsIDOMNode* inNode, nsCOMPtr<nsIDOMNode>* outNode, + bool bNoBlockCrossing = false); + nsIContent* GetNextHTMLNode(nsINode* aParent, int32_t aOffset, + bool aNoBlockCrossing = false); + nsresult GetNextHTMLNode(nsIDOMNode* inParent, int32_t inOffset, + nsCOMPtr<nsIDOMNode>* outNode, + bool bNoBlockCrossing = false); + + nsresult IsFirstEditableChild(nsIDOMNode* aNode, bool* aOutIsFirst); + nsresult IsLastEditableChild(nsIDOMNode* aNode, bool* aOutIsLast); + nsIContent* GetFirstEditableChild(nsINode& aNode); + nsIContent* GetLastEditableChild(nsINode& aNode); + + nsIContent* GetFirstEditableLeaf(nsINode& aNode); + nsIContent* GetLastEditableLeaf(nsINode& aNode); + + nsresult GetInlinePropertyBase(nsIAtom& aProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool* aFirst, + bool* aAny, + bool* aAll, + nsAString* outValue, + bool aCheckDefaults = true); + bool HasStyleOrIdOrClass(Element* aElement); + nsresult RemoveElementIfNoStyleOrIdOrClass(Element& aElement); + + /** + * Whether the outer window of the DOM event target has focus or not. + */ + bool OurWindowHasFocus(); + + /** + * This function is used to insert a string of HTML input optionally with some + * context information into the editable field. The HTML input either comes + * from a transferable object created as part of a drop/paste operation, or + * from the InsertHTML method. We may want the HTML input to be sanitized + * (for example, if it's coming from a transferable object), in which case + * aTrustedInput should be set to false, otherwise, the caller should set it + * to true, which means that the HTML will be inserted in the DOM verbatim. + * + * aClearStyle should be set to false if you want the paste to be affected by + * local style (e.g., for the insertHTML command). + */ + nsresult DoInsertHTMLWithContext(const nsAString& aInputString, + const nsAString& aContextStr, + const nsAString& aInfoStr, + const nsAString& aFlavor, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestNode, + int32_t aDestOffset, + bool aDeleteSelection, + bool aTrustedInput, + bool aClearStyle = true); + + nsresult ClearStyle(nsCOMPtr<nsINode>* aNode, int32_t* aOffset, + nsIAtom* aProperty, const nsAString* aAttribute); + + void SetElementPosition(Element& aElement, int32_t aX, int32_t aY); + +protected: + nsTArray<OwningNonNull<nsIContentFilter>> mContentFilters; + + RefPtr<TypeInState> mTypeInState; + + bool mCRInParagraphCreatesParagraph; + + bool mCSSAware; + nsAutoPtr<CSSEditUtils> mCSSEditUtils; + + // Used by GetFirstSelectedCell and GetNextSelectedCell + int32_t mSelectedCellIndex; + + nsString mLastStyleSheetURL; + nsString mLastOverrideStyleSheetURL; + + // Maintain a list of associated style sheets and their urls. + nsTArray<nsString> mStyleSheetURLs; + nsTArray<RefPtr<StyleSheet>> mStyleSheets; + + // an array for holding default style settings + nsTArray<PropItem*> mDefaultStyles; + +protected: + // ANONYMOUS UTILS + void RemoveListenerAndDeleteRef(const nsAString& aEvent, + nsIDOMEventListener* aListener, + bool aUseCapture, + Element* aElement, + nsIContent* aParentContent, + nsIPresShell* aShell); + void DeleteRefToAnonymousNode(nsIDOMElement* aElement, + nsIContent* aParentContent, + nsIPresShell* aShell); + + nsresult ShowResizersInner(nsIDOMElement *aResizedElement); + + /** + * Returns the offset of an element's frame to its absolute containing block. + */ + nsresult GetElementOrigin(nsIDOMElement* aElement, + int32_t& aX, int32_t& aY); + nsresult GetPositionAndDimensions(nsIDOMElement* aElement, + int32_t& aX, int32_t& aY, + int32_t& aW, int32_t& aH, + int32_t& aBorderLeft, + int32_t& aBorderTop, + int32_t& aMarginLeft, + int32_t& aMarginTop); + + bool IsInObservedSubtree(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild); + + void UpdateRootElement(); + + // resizing + bool mIsObjectResizingEnabled; + bool mIsResizing; + bool mPreserveRatio; + bool mResizedObjectIsAnImage; + + // absolute positioning + bool mIsAbsolutelyPositioningEnabled; + bool mResizedObjectIsAbsolutelyPositioned; + + bool mGrabberClicked; + bool mIsMoving; + + bool mSnapToGridEnabled; + + // inline table editing + bool mIsInlineTableEditingEnabled; + + // resizing + nsCOMPtr<Element> mTopLeftHandle; + nsCOMPtr<Element> mTopHandle; + nsCOMPtr<Element> mTopRightHandle; + nsCOMPtr<Element> mLeftHandle; + nsCOMPtr<Element> mRightHandle; + nsCOMPtr<Element> mBottomLeftHandle; + nsCOMPtr<Element> mBottomHandle; + nsCOMPtr<Element> mBottomRightHandle; + + nsCOMPtr<Element> mActivatedHandle; + + nsCOMPtr<Element> mResizingShadow; + nsCOMPtr<Element> mResizingInfo; + + nsCOMPtr<Element> mResizedObject; + + nsCOMPtr<nsIDOMEventListener> mMouseMotionListenerP; + nsCOMPtr<nsISelectionListener> mSelectionListenerP; + nsCOMPtr<nsIDOMEventListener> mResizeEventListenerP; + + nsTArray<OwningNonNull<nsIHTMLObjectResizeListener>> mObjectResizeEventListeners; + + int32_t mOriginalX; + int32_t mOriginalY; + + int32_t mResizedObjectX; + int32_t mResizedObjectY; + int32_t mResizedObjectWidth; + int32_t mResizedObjectHeight; + + int32_t mResizedObjectMarginLeft; + int32_t mResizedObjectMarginTop; + int32_t mResizedObjectBorderLeft; + int32_t mResizedObjectBorderTop; + + int32_t mXIncrementFactor; + int32_t mYIncrementFactor; + int32_t mWidthIncrementFactor; + int32_t mHeightIncrementFactor; + + int8_t mInfoXIncrement; + int8_t mInfoYIncrement; + + nsresult SetAllResizersPosition(); + + already_AddRefed<Element> CreateResizer(int16_t aLocation, + nsIDOMNode* aParentNode); + void SetAnonymousElementPosition(int32_t aX, int32_t aY, + nsIDOMElement* aResizer); + + already_AddRefed<Element> CreateShadow(nsIDOMNode* aParentNode, + nsIDOMElement* aOriginalObject); + nsresult SetShadowPosition(Element* aShadow, Element* aOriginalObject, + int32_t aOriginalObjectX, + int32_t aOriginalObjectY); + + already_AddRefed<Element> CreateResizingInfo(nsIDOMNode* aParentNode); + nsresult SetResizingInfoPosition(int32_t aX, int32_t aY, + int32_t aW, int32_t aH); + + int32_t GetNewResizingIncrement(int32_t aX, int32_t aY, int32_t aID); + nsresult StartResizing(nsIDOMElement* aHandle); + int32_t GetNewResizingX(int32_t aX, int32_t aY); + int32_t GetNewResizingY(int32_t aX, int32_t aY); + int32_t GetNewResizingWidth(int32_t aX, int32_t aY); + int32_t GetNewResizingHeight(int32_t aX, int32_t aY); + void HideShadowAndInfo(); + void SetFinalSize(int32_t aX, int32_t aY); + void DeleteRefToAnonymousNode(nsIDOMNode* aNode); + void SetResizeIncrements(int32_t aX, int32_t aY, int32_t aW, int32_t aH, + bool aPreserveRatio); + void HideAnonymousEditingUIs(); + + // absolute positioning + int32_t mPositionedObjectX; + int32_t mPositionedObjectY; + int32_t mPositionedObjectWidth; + int32_t mPositionedObjectHeight; + + int32_t mPositionedObjectMarginLeft; + int32_t mPositionedObjectMarginTop; + int32_t mPositionedObjectBorderLeft; + int32_t mPositionedObjectBorderTop; + + nsCOMPtr<Element> mAbsolutelyPositionedObject; + nsCOMPtr<Element> mGrabber; + nsCOMPtr<Element> mPositioningShadow; + + int32_t mGridSize; + + already_AddRefed<Element> CreateGrabber(nsINode* aParentNode); + nsresult StartMoving(nsIDOMElement* aHandle); + nsresult SetFinalPosition(int32_t aX, int32_t aY); + void AddPositioningOffset(int32_t& aX, int32_t& aY); + void SnapToGrid(int32_t& newX, int32_t& newY); + nsresult GrabberClicked(); + nsresult EndMoving(); + nsresult CheckPositionedElementBGandFG(nsIDOMElement* aElement, + nsAString& aReturn); + + // inline table editing + nsCOMPtr<nsIDOMElement> mInlineEditedCell; + + nsCOMPtr<nsIDOMElement> mAddColumnBeforeButton; + nsCOMPtr<nsIDOMElement> mRemoveColumnButton; + nsCOMPtr<nsIDOMElement> mAddColumnAfterButton; + + nsCOMPtr<nsIDOMElement> mAddRowBeforeButton; + nsCOMPtr<nsIDOMElement> mRemoveRowButton; + nsCOMPtr<nsIDOMElement> mAddRowAfterButton; + + void AddMouseClickListener(nsIDOMElement* aElement); + void RemoveMouseClickListener(nsIDOMElement* aElement); + + nsCOMPtr<nsILinkHandler> mLinkHandler; + +public: + friend class HTMLEditorEventListener; + friend class HTMLEditRules; + friend class TextEditRules; + friend class WSRunObject; + +private: + bool IsSimpleModifiableNode(nsIContent* aContent, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString* aValue); + nsresult SetInlinePropertyOnNodeImpl(nsIContent& aNode, + nsIAtom& aProperty, + const nsAString* aAttribute, + const nsAString& aValue); + typedef enum { eInserted, eAppended } InsertedOrAppended; + void DoContentInserted(nsIDocument* aDocument, nsIContent* aContainer, + nsIContent* aChild, int32_t aIndexInContainer, + InsertedOrAppended aInsertedOrAppended); + already_AddRefed<Element> GetElementOrParentByTagName( + const nsAString& aTagName, nsINode* aNode); + already_AddRefed<Element> CreateElementWithDefaults( + const nsAString& aTagName); +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_HTMLEditor_h diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp new file mode 100644 index 000000000..b9cd8adb9 --- /dev/null +++ b/editor/libeditor/HTMLEditorDataTransfer.cpp @@ -0,0 +1,2409 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 et tw=78: */ +/* 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/HTMLEditor.h" + +#include <string.h> + +#include "HTMLEditUtils.h" +#include "TextEditUtils.h" +#include "WSRunObject.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Base64.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/Preferences.h" +#include "mozilla/SelectionState.h" +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsCRTGlue.h" // for CRLF +#include "nsComponentManagerUtils.h" +#include "nsIScriptError.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsDependentSubstring.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIClipboard.h" +#include "nsIContent.h" +#include "nsIContentFilter.h" +#include "nsIDOMComment.h" +#include "nsIDOMDocument.h" +#include "nsIDOMDocumentFragment.h" +#include "nsIDOMElement.h" +#include "nsIDOMHTMLAnchorElement.h" +#include "nsIDOMHTMLEmbedElement.h" +#include "nsIDOMHTMLFrameElement.h" +#include "nsIDOMHTMLIFrameElement.h" +#include "nsIDOMHTMLImageElement.h" +#include "nsIDOMHTMLInputElement.h" +#include "nsIDOMHTMLLinkElement.h" +#include "nsIDOMHTMLObjectElement.h" +#include "nsIDOMHTMLScriptElement.h" +#include "nsIDOMNode.h" +#include "nsIDocument.h" +#include "nsIEditor.h" +#include "nsIEditorIMESupport.h" +#include "nsIEditorMailSupport.h" +#include "nsIEditRules.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsIMIMEService.h" +#include "nsNameSpaceManager.h" +#include "nsINode.h" +#include "nsIParserUtils.h" +#include "nsIPlaintextEditor.h" +#include "nsISupportsImpl.h" +#include "nsISupportsPrimitives.h" +#include "nsISupportsUtils.h" +#include "nsITransferable.h" +#include "nsIURI.h" +#include "nsIVariant.h" +#include "nsLinebreakConverter.h" +#include "nsLiteralString.h" +#include "nsNetUtil.h" +#include "nsRange.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsStringIterator.h" +#include "nsSubstringTuple.h" +#include "nsTreeSanitizer.h" +#include "nsXPCOM.h" +#include "nscore.h" +#include "nsContentUtils.h" + +class nsIAtom; +class nsILoadContext; +class nsISupports; + +namespace mozilla { + +using namespace dom; + +#define kInsertCookie "_moz_Insert Here_moz_" + +// some little helpers +static bool FindIntegerAfterString(const char* aLeadingString, + nsCString& aCStr, int32_t& foundNumber); +static nsresult RemoveFragComments(nsCString& theStr); +static void RemoveBodyAndHead(nsINode& aNode); +static nsresult FindTargetNode(nsIDOMNode* aStart, + nsCOMPtr<nsIDOMNode>& aResult); + +nsresult +HTMLEditor::LoadHTML(const nsAString& aInputString) +{ + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + + // force IME commit; set up rules sniffing and batching + ForceCompositionEnd(); + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::loadHTML, nsIEditor::eNext); + + // Get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + TextRulesInfo ruleInfo(EditAction::loadHTML); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel) { + return NS_OK; // rules canceled the operation + } + + if (!handled) { + // Delete Selection, but only if it isn't collapsed, see bug #106269 + if (!selection->Collapsed()) { + rv = DeleteSelection(eNone, eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get the first range in the selection, for context: + RefPtr<nsRange> range = selection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER); + + // create fragment for pasted html + nsCOMPtr<nsIDOMDocumentFragment> docfrag; + rv = range->CreateContextualFragment(aInputString, getter_AddRefs(docfrag)); + NS_ENSURE_SUCCESS(rv, rv); + // put the fragment into the document + nsCOMPtr<nsIDOMNode> parent; + rv = range->GetStartContainer(getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + int32_t childOffset; + rv = range->GetStartOffset(&childOffset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> nodeToInsert; + docfrag->GetFirstChild(getter_AddRefs(nodeToInsert)); + while (nodeToInsert) { + rv = InsertNode(nodeToInsert, parent, childOffset++); + NS_ENSURE_SUCCESS(rv, rv); + docfrag->GetFirstChild(getter_AddRefs(nodeToInsert)); + } + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +HTMLEditor::InsertHTML(const nsAString& aInString) +{ + const nsAFlatString& empty = EmptyString(); + + return InsertHTMLWithContext(aInString, empty, empty, empty, + nullptr, nullptr, 0, true); +} + +nsresult +HTMLEditor::InsertHTMLWithContext(const nsAString& aInputString, + const nsAString& aContextStr, + const nsAString& aInfoStr, + const nsAString& aFlavor, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestNode, + int32_t aDestOffset, + bool aDeleteSelection) +{ + return DoInsertHTMLWithContext(aInputString, aContextStr, aInfoStr, + aFlavor, aSourceDoc, aDestNode, aDestOffset, aDeleteSelection, + /* trusted input */ true, /* clear style */ false); +} + +nsresult +HTMLEditor::DoInsertHTMLWithContext(const nsAString& aInputString, + const nsAString& aContextStr, + const nsAString& aInfoStr, + const nsAString& aFlavor, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestNode, + int32_t aDestOffset, + bool aDeleteSelection, + bool aTrustedInput, + bool aClearStyle) +{ + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + + // Prevent the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // force IME commit; set up rules sniffing and batching + ForceCompositionEnd(); + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::htmlPaste, nsIEditor::eNext); + + // Get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + // create a dom document fragment that represents the structure to paste + nsCOMPtr<nsIDOMNode> fragmentAsNode, streamStartParent, streamEndParent; + int32_t streamStartOffset = 0, streamEndOffset = 0; + + nsresult rv = CreateDOMFragmentFromPaste(aInputString, aContextStr, aInfoStr, + address_of(fragmentAsNode), + address_of(streamStartParent), + address_of(streamEndParent), + &streamStartOffset, + &streamEndOffset, + aTrustedInput); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> targetNode; + int32_t targetOffset=0; + + if (!aDestNode) { + // if caller didn't provide the destination/target node, + // fetch the paste insertion point from our selection + rv = GetStartNodeAndOffset(selection, getter_AddRefs(targetNode), &targetOffset); + NS_ENSURE_SUCCESS(rv, rv); + if (!targetNode || !IsEditable(targetNode)) { + return NS_ERROR_FAILURE; + } + } else { + targetNode = aDestNode; + targetOffset = aDestOffset; + } + + bool doContinue = true; + + rv = DoContentFilterCallback(aFlavor, aSourceDoc, aDeleteSelection, + (nsIDOMNode **)address_of(fragmentAsNode), + (nsIDOMNode **)address_of(streamStartParent), + &streamStartOffset, + (nsIDOMNode **)address_of(streamEndParent), + &streamEndOffset, + (nsIDOMNode **)address_of(targetNode), + &targetOffset, &doContinue); + + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(doContinue, NS_OK); + + // if we have a destination / target node, we want to insert there + // rather than in place of the selection + // ignore aDeleteSelection here if no aDestNode since deletion will + // also occur later; this block is intended to cover the various + // scenarios where we are dropping in an editor (and may want to delete + // the selection before collapsing the selection in the new destination) + if (aDestNode) { + if (aDeleteSelection) { + // Use an auto tracker so that our drop point is correctly + // positioned after the delete. + AutoTrackDOMPoint tracker(mRangeUpdater, &targetNode, &targetOffset); + rv = DeleteSelection(eNone, eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = selection->Collapse(targetNode, targetOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + // we need to recalculate various things based on potentially new offsets + // this is work to be completed at a later date (probably by jfrancis) + + // make a list of what nodes in docFrag we need to move + nsTArray<OwningNonNull<nsINode>> nodeList; + nsCOMPtr<nsINode> fragmentAsNodeNode = do_QueryInterface(fragmentAsNode); + NS_ENSURE_STATE(fragmentAsNodeNode || !fragmentAsNode); + nsCOMPtr<nsINode> streamStartParentNode = + do_QueryInterface(streamStartParent); + NS_ENSURE_STATE(streamStartParentNode || !streamStartParent); + nsCOMPtr<nsINode> streamEndParentNode = + do_QueryInterface(streamEndParent); + NS_ENSURE_STATE(streamEndParentNode || !streamEndParent); + CreateListOfNodesToPaste(*static_cast<DocumentFragment*>(fragmentAsNodeNode.get()), + nodeList, + streamStartParentNode, streamStartOffset, + streamEndParentNode, streamEndOffset); + + if (nodeList.IsEmpty()) { + // We aren't inserting anything, but if aDeleteSelection is set, we do want + // to delete everything. + if (aDeleteSelection) { + return DeleteSelection(eNone, eStrip); + } + return NS_OK; + } + + // Are there any table elements in the list? + // node and offset for insertion + nsCOMPtr<nsIDOMNode> parentNode; + int32_t offsetOfNewNode; + + // check for table cell selection mode + bool cellSelectionMode = false; + nsCOMPtr<nsIDOMElement> cell; + rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell)); + if (NS_SUCCEEDED(rv) && cell) { + cellSelectionMode = true; + } + + if (cellSelectionMode) { + // do we have table content to paste? If so, we want to delete + // the selected table cells and replace with new table elements; + // but if not we want to delete _contents_ of cells and replace + // with non-table elements. Use cellSelectionMode bool to + // indicate results. + if (!HTMLEditUtils::IsTableElement(nodeList[0])) { + cellSelectionMode = false; + } + } + + if (!cellSelectionMode) { + rv = DeleteSelectionAndPrepareToCreateNode(); + NS_ENSURE_SUCCESS(rv, rv); + + if (aClearStyle) { + // pasting does not inherit local inline styles + nsCOMPtr<nsINode> tmpNode = selection->GetAnchorNode(); + int32_t tmpOffset = static_cast<int32_t>(selection->AnchorOffset()); + rv = ClearStyle(address_of(tmpNode), &tmpOffset, nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Delete whole cells: we will replace with new table content. + + // Braces for artificial block to scope AutoSelectionRestorer. + // Save current selection since DeleteTableCell() perturbs it. + { + AutoSelectionRestorer selectionRestorer(selection, this); + rv = DeleteTableCell(1); + NS_ENSURE_SUCCESS(rv, rv); + } + // collapse selection to beginning of deleted table content + selection->CollapseToStart(); + } + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel) { + return NS_OK; // rules canceled the operation + } + + if (!handled) { + // The rules code (WillDoAction above) might have changed the selection. + // refresh our memory... + rv = GetStartNodeAndOffset(selection, getter_AddRefs(parentNode), &offsetOfNewNode); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parentNode, NS_ERROR_FAILURE); + + // Adjust position based on the first node we are going to insert. + NormalizeEOLInsertPosition(nodeList[0], address_of(parentNode), + &offsetOfNewNode); + + // if there are any invisible br's after our insertion point, remove them. + // this is because if there is a br at end of what we paste, it will make + // the invisible br visible. + WSRunObject wsObj(this, parentNode, offsetOfNewNode); + if (wsObj.mEndReasonNode && + TextEditUtils::IsBreak(wsObj.mEndReasonNode) && + !IsVisBreak(wsObj.mEndReasonNode)) { + rv = DeleteNode(wsObj.mEndReasonNode); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Remember if we are in a link. + bool bStartedInLink = IsInLink(parentNode); + + // Are we in a text node? If so, split it. + if (IsTextNode(parentNode)) { + nsCOMPtr<nsIContent> parentContent = do_QueryInterface(parentNode); + NS_ENSURE_STATE(parentContent || !parentNode); + offsetOfNewNode = SplitNodeDeep(*parentContent, *parentContent, + offsetOfNewNode); + NS_ENSURE_STATE(offsetOfNewNode != -1); + nsCOMPtr<nsIDOMNode> temp; + rv = parentNode->GetParentNode(getter_AddRefs(temp)); + NS_ENSURE_SUCCESS(rv, rv); + parentNode = temp; + } + + // build up list of parents of first node in list that are either + // lists or tables. First examine front of paste node list. + nsTArray<OwningNonNull<Element>> startListAndTableArray; + GetListAndTableParents(StartOrEnd::start, nodeList, + startListAndTableArray); + + // remember number of lists and tables above us + int32_t highWaterMark = -1; + if (!startListAndTableArray.IsEmpty()) { + highWaterMark = DiscoverPartialListsAndTables(nodeList, + startListAndTableArray); + } + + // if we have pieces of tables or lists to be inserted, let's force the paste + // to deal with table elements right away, so that it doesn't orphan some + // table or list contents outside the table or list. + if (highWaterMark >= 0) { + ReplaceOrphanedStructure(StartOrEnd::start, nodeList, + startListAndTableArray, highWaterMark); + } + + // Now go through the same process again for the end of the paste node list. + nsTArray<OwningNonNull<Element>> endListAndTableArray; + GetListAndTableParents(StartOrEnd::end, nodeList, endListAndTableArray); + highWaterMark = -1; + + // remember number of lists and tables above us + if (!endListAndTableArray.IsEmpty()) { + highWaterMark = DiscoverPartialListsAndTables(nodeList, + endListAndTableArray); + } + + // don't orphan partial list or table structure + if (highWaterMark >= 0) { + ReplaceOrphanedStructure(StartOrEnd::end, nodeList, + endListAndTableArray, highWaterMark); + } + + // Loop over the node list and paste the nodes: + nsCOMPtr<nsIDOMNode> parentBlock, lastInsertNode, insertedContextParent; + nsCOMPtr<nsINode> parentNodeNode = do_QueryInterface(parentNode); + NS_ENSURE_STATE(parentNodeNode || !parentNode); + if (IsBlockNode(parentNodeNode)) { + parentBlock = parentNode; + } else { + parentBlock = GetBlockNodeParent(parentNode); + } + + int32_t listCount = nodeList.Length(); + for (int32_t j = 0; j < listCount; j++) { + bool bDidInsert = false; + nsCOMPtr<nsIDOMNode> curNode = nodeList[j]->AsDOMNode(); + + NS_ENSURE_TRUE(curNode, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(curNode != fragmentAsNode, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(!TextEditUtils::IsBody(curNode), NS_ERROR_FAILURE); + + if (insertedContextParent) { + // if we had to insert something higher up in the paste hierarchy, we want to + // skip any further paste nodes that descend from that. Else we will paste twice. + if (EditorUtils::IsDescendantOf(curNode, insertedContextParent)) { + continue; + } + } + + // give the user a hand on table element insertion. if they have + // a table or table row on the clipboard, and are trying to insert + // into a table or table row, insert the appropriate children instead. + if (HTMLEditUtils::IsTableRow(curNode) && + HTMLEditUtils::IsTableRow(parentNode) && + (HTMLEditUtils::IsTable(curNode) || + HTMLEditUtils::IsTable(parentNode))) { + nsCOMPtr<nsIDOMNode> child; + curNode->GetFirstChild(getter_AddRefs(child)); + while (child) { + rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true); + if (NS_FAILED(rv)) { + break; + } + + bDidInsert = true; + lastInsertNode = child; + offsetOfNewNode++; + + curNode->GetFirstChild(getter_AddRefs(child)); + } + } + // give the user a hand on list insertion. if they have + // a list on the clipboard, and are trying to insert + // into a list or list item, insert the appropriate children instead, + // ie, merge the lists instead of pasting in a sublist. + else if (HTMLEditUtils::IsList(curNode) && + (HTMLEditUtils::IsList(parentNode) || + HTMLEditUtils::IsListItem(parentNode))) { + nsCOMPtr<nsIDOMNode> child, tmp; + curNode->GetFirstChild(getter_AddRefs(child)); + while (child) { + if (HTMLEditUtils::IsListItem(child) || + HTMLEditUtils::IsList(child)) { + // Check if we are pasting into empty list item. If so + // delete it and paste into parent list instead. + if (HTMLEditUtils::IsListItem(parentNode)) { + bool isEmpty; + rv = IsEmptyNode(parentNode, &isEmpty, true); + if (NS_SUCCEEDED(rv) && isEmpty) { + int32_t newOffset; + nsCOMPtr<nsIDOMNode> listNode = GetNodeLocation(parentNode, &newOffset); + if (listNode) { + DeleteNode(parentNode); + parentNode = listNode; + offsetOfNewNode = newOffset; + } + } + } + rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true); + if (NS_FAILED(rv)) { + break; + } + + bDidInsert = true; + lastInsertNode = child; + offsetOfNewNode++; + } else { + curNode->RemoveChild(child, getter_AddRefs(tmp)); + } + curNode->GetFirstChild(getter_AddRefs(child)); + } + } else if (parentBlock && HTMLEditUtils::IsPre(parentBlock) && + HTMLEditUtils::IsPre(curNode)) { + // Check for pre's going into pre's. + nsCOMPtr<nsIDOMNode> child; + curNode->GetFirstChild(getter_AddRefs(child)); + while (child) { + rv = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, true); + if (NS_FAILED(rv)) { + break; + } + + bDidInsert = true; + lastInsertNode = child; + offsetOfNewNode++; + + curNode->GetFirstChild(getter_AddRefs(child)); + } + } + + if (!bDidInsert || NS_FAILED(rv)) { + // try to insert + rv = InsertNodeAtPoint(curNode, address_of(parentNode), &offsetOfNewNode, true); + if (NS_SUCCEEDED(rv)) { + bDidInsert = true; + lastInsertNode = curNode; + } + + // Assume failure means no legal parent in the document hierarchy, + // try again with the parent of curNode in the paste hierarchy. + nsCOMPtr<nsIDOMNode> parent; + while (NS_FAILED(rv) && curNode) { + curNode->GetParentNode(getter_AddRefs(parent)); + if (parent && !TextEditUtils::IsBody(parent)) { + rv = InsertNodeAtPoint(parent, address_of(parentNode), &offsetOfNewNode, true); + if (NS_SUCCEEDED(rv)) { + bDidInsert = true; + insertedContextParent = parent; + lastInsertNode = GetChildAt(parentNode, offsetOfNewNode); + } + } + curNode = parent; + } + } + if (lastInsertNode) { + parentNode = GetNodeLocation(lastInsertNode, &offsetOfNewNode); + offsetOfNewNode++; + } + } + + // Now collapse the selection to the end of what we just inserted: + if (lastInsertNode) { + // set selection to the end of what we just pasted. + nsCOMPtr<nsIDOMNode> selNode, tmp, highTable; + int32_t selOffset; + + // but don't cross tables + if (!HTMLEditUtils::IsTable(lastInsertNode)) { + nsCOMPtr<nsINode> lastInsertNode_ = do_QueryInterface(lastInsertNode); + NS_ENSURE_STATE(lastInsertNode_ || !lastInsertNode); + selNode = GetAsDOMNode(GetLastEditableLeaf(*lastInsertNode_)); + tmp = selNode; + while (tmp && tmp != lastInsertNode) { + if (HTMLEditUtils::IsTable(tmp)) { + highTable = tmp; + } + nsCOMPtr<nsIDOMNode> parent = tmp; + tmp->GetParentNode(getter_AddRefs(parent)); + tmp = parent; + } + if (highTable) { + selNode = highTable; + } + } + if (!selNode) { + selNode = lastInsertNode; + } + if (IsTextNode(selNode) || + (IsContainer(selNode) && !HTMLEditUtils::IsTable(selNode))) { + rv = GetLengthOfDOMNode(selNode, (uint32_t&)selOffset); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // We need to find a container for selection. Look up. + tmp = selNode; + selNode = GetNodeLocation(tmp, &selOffset); + // selNode might be null in case a mutation listener removed + // the stuff we just inserted from the DOM. + NS_ENSURE_STATE(selNode); + ++selOffset; // want to be *after* last leaf node in paste + } + + // make sure we don't end up with selection collapsed after an invisible break node + WSRunObject wsRunObj(this, selNode, selOffset); + nsCOMPtr<nsINode> visNode; + int32_t outVisOffset=0; + WSType visType; + nsCOMPtr<nsINode> selNode_(do_QueryInterface(selNode)); + wsRunObj.PriorVisibleNode(selNode_, selOffset, address_of(visNode), + &outVisOffset, &visType); + if (visType == WSType::br) { + // we are after a break. Is it visible? Despite the name, + // PriorVisibleNode does not make that determination for breaks. + // It also may not return the break in visNode. We have to pull it + // out of the WSRunObject's state. + if (!IsVisBreak(wsRunObj.mStartReasonNode)) { + // don't leave selection past an invisible break; + // reset {selNode,selOffset} to point before break + selNode = GetNodeLocation(GetAsDOMNode(wsRunObj.mStartReasonNode), &selOffset); + // we want to be inside any inline style prior to break + WSRunObject wsRunObj(this, selNode, selOffset); + selNode_ = do_QueryInterface(selNode); + wsRunObj.PriorVisibleNode(selNode_, selOffset, address_of(visNode), + &outVisOffset, &visType); + if (visType == WSType::text || visType == WSType::normalWS) { + selNode = GetAsDOMNode(visNode); + selOffset = outVisOffset; // PriorVisibleNode already set offset to _after_ the text or ws + } else if (visType == WSType::special) { + // prior visible thing is an image or some other non-text thingy. + // We want to be right after it. + selNode = GetNodeLocation(GetAsDOMNode(wsRunObj.mStartReasonNode), &selOffset); + ++selOffset; + } + } + } + selection->Collapse(selNode, selOffset); + + // if we just pasted a link, discontinue link style + nsCOMPtr<nsIDOMNode> link; + if (!bStartedInLink && IsInLink(selNode, address_of(link))) { + // so, if we just pasted a link, I split it. Why do that instead of just + // nudging selection point beyond it? Because it might have ended in a BR + // that is not visible. If so, the code above just placed selection + // inside that. So I split it instead. + nsCOMPtr<nsIContent> linkContent = do_QueryInterface(link); + NS_ENSURE_STATE(linkContent || !link); + nsCOMPtr<nsIContent> selContent = do_QueryInterface(selNode); + NS_ENSURE_STATE(selContent || !selNode); + nsCOMPtr<nsIContent> leftLink; + SplitNodeDeep(*linkContent, *selContent, selOffset, + EmptyContainers::no, getter_AddRefs(leftLink)); + if (leftLink) { + selNode = GetNodeLocation(GetAsDOMNode(leftLink), &selOffset); + selection->Collapse(selNode, selOffset+1); + } + } + } + } + + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +HTMLEditor::AddInsertionListener(nsIContentFilter* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER); + + // don't let a listener be added more than once + if (!mContentFilters.Contains(aListener)) { + mContentFilters.AppendElement(*aListener); + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RemoveInsertionListener(nsIContentFilter* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_FAILURE); + + mContentFilters.RemoveElement(aListener); + + return NS_OK; +} + +nsresult +HTMLEditor::DoContentFilterCallback(const nsAString& aFlavor, + nsIDOMDocument* sourceDoc, + bool aWillDeleteSelection, + nsIDOMNode** aFragmentAsNode, + nsIDOMNode** aFragStartNode, + int32_t* aFragStartOffset, + nsIDOMNode** aFragEndNode, + int32_t* aFragEndOffset, + nsIDOMNode** aTargetNode, + int32_t* aTargetOffset, + bool* aDoContinue) +{ + *aDoContinue = true; + + for (auto& listener : mContentFilters) { + if (!*aDoContinue) { + break; + } + listener->NotifyOfInsertion(aFlavor, nullptr, sourceDoc, + aWillDeleteSelection, aFragmentAsNode, + aFragStartNode, aFragStartOffset, + aFragEndNode, aFragEndOffset, aTargetNode, + aTargetOffset, aDoContinue); + } + + return NS_OK; +} + +bool +HTMLEditor::IsInLink(nsIDOMNode* aNode, + nsCOMPtr<nsIDOMNode>* outLink) +{ + NS_ENSURE_TRUE(aNode, false); + if (outLink) { + *outLink = nullptr; + } + nsCOMPtr<nsIDOMNode> tmp, node = aNode; + while (node) { + if (HTMLEditUtils::IsLink(node)) { + if (outLink) { + *outLink = node; + } + return true; + } + tmp = node; + tmp->GetParentNode(getter_AddRefs(node)); + } + return false; +} + +nsresult +HTMLEditor::StripFormattingNodes(nsIContent& aNode, + bool aListOnly) +{ + if (aNode.TextIsOnlyWhitespace()) { + nsCOMPtr<nsINode> parent = aNode.GetParentNode(); + if (parent) { + if (!aListOnly || HTMLEditUtils::IsList(parent)) { + ErrorResult rv; + parent->RemoveChild(aNode, rv); + return rv.StealNSResult(); + } + return NS_OK; + } + } + + if (!aNode.IsHTMLElement(nsGkAtoms::pre)) { + nsCOMPtr<nsIContent> child = aNode.GetLastChild(); + while (child) { + nsCOMPtr<nsIContent> previous = child->GetPreviousSibling(); + nsresult rv = StripFormattingNodes(*child, aListOnly); + NS_ENSURE_SUCCESS(rv, rv); + child = previous.forget(); + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::PrepareTransferable(nsITransferable** aTransferable) +{ + return NS_OK; +} + +nsresult +HTMLEditor::PrepareHTMLTransferable(nsITransferable** aTransferable) +{ + // Create generic Transferable for getting the data + nsresult rv = CallCreateInstance("@mozilla.org/widget/transferable;1", aTransferable); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the nsITransferable interface for getting the data from the clipboard + if (aTransferable) { + nsCOMPtr<nsIDocument> destdoc = GetDocument(); + nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr; + (*aTransferable)->Init(loadContext); + + // Create the desired DataFlavor for the type of data + // we want to get out of the transferable + // This should only happen in html editors, not plaintext + if (!IsPlaintextEditor()) { + (*aTransferable)->AddDataFlavor(kNativeHTMLMime); + (*aTransferable)->AddDataFlavor(kHTMLMime); + (*aTransferable)->AddDataFlavor(kFileMime); + + switch (Preferences::GetInt("clipboard.paste_image_type", 1)) { + case 0: // prefer JPEG over PNG over GIF encoding + (*aTransferable)->AddDataFlavor(kJPEGImageMime); + (*aTransferable)->AddDataFlavor(kJPGImageMime); + (*aTransferable)->AddDataFlavor(kPNGImageMime); + (*aTransferable)->AddDataFlavor(kGIFImageMime); + break; + case 1: // prefer PNG over JPEG over GIF encoding (default) + default: + (*aTransferable)->AddDataFlavor(kPNGImageMime); + (*aTransferable)->AddDataFlavor(kJPEGImageMime); + (*aTransferable)->AddDataFlavor(kJPGImageMime); + (*aTransferable)->AddDataFlavor(kGIFImageMime); + break; + case 2: // prefer GIF over JPEG over PNG encoding + (*aTransferable)->AddDataFlavor(kGIFImageMime); + (*aTransferable)->AddDataFlavor(kJPEGImageMime); + (*aTransferable)->AddDataFlavor(kJPGImageMime); + (*aTransferable)->AddDataFlavor(kPNGImageMime); + break; + } + } + (*aTransferable)->AddDataFlavor(kUnicodeMime); + (*aTransferable)->AddDataFlavor(kMozTextInternal); + } + + return NS_OK; +} + +bool +FindIntegerAfterString(const char* aLeadingString, + nsCString& aCStr, + int32_t& foundNumber) +{ + // first obtain offsets from cfhtml str + int32_t numFront = aCStr.Find(aLeadingString); + if (numFront == -1) { + return false; + } + numFront += strlen(aLeadingString); + + int32_t numBack = aCStr.FindCharInSet(CRLF, numFront); + if (numBack == -1) { + return false; + } + + nsAutoCString numStr(Substring(aCStr, numFront, numBack-numFront)); + nsresult errorCode; + foundNumber = numStr.ToInteger(&errorCode); + return true; +} + +nsresult +RemoveFragComments(nsCString& aStr) +{ + // remove the StartFragment/EndFragment comments from the str, if present + int32_t startCommentIndx = aStr.Find("<!--StartFragment"); + if (startCommentIndx >= 0) { + int32_t startCommentEnd = aStr.Find("-->", false, startCommentIndx); + if (startCommentEnd > startCommentIndx) { + aStr.Cut(startCommentIndx, (startCommentEnd + 3) - startCommentIndx); + } + } + int32_t endCommentIndx = aStr.Find("<!--EndFragment"); + if (endCommentIndx >= 0) { + int32_t endCommentEnd = aStr.Find("-->", false, endCommentIndx); + if (endCommentEnd > endCommentIndx) { + aStr.Cut(endCommentIndx, (endCommentEnd + 3) - endCommentIndx); + } + } + return NS_OK; +} + +nsresult +HTMLEditor::ParseCFHTML(nsCString& aCfhtml, + char16_t** aStuffToPaste, + char16_t** aCfcontext) +{ + // First obtain offsets from cfhtml str. + int32_t startHTML, endHTML, startFragment, endFragment; + if (!FindIntegerAfterString("StartHTML:", aCfhtml, startHTML) || + startHTML < -1) { + return NS_ERROR_FAILURE; + } + if (!FindIntegerAfterString("EndHTML:", aCfhtml, endHTML) || + endHTML < -1) { + return NS_ERROR_FAILURE; + } + if (!FindIntegerAfterString("StartFragment:", aCfhtml, startFragment) || + startFragment < 0) { + return NS_ERROR_FAILURE; + } + if (!FindIntegerAfterString("EndFragment:", aCfhtml, endFragment) || + startFragment < 0) { + return NS_ERROR_FAILURE; + } + + // The StartHTML and EndHTML markers are allowed to be -1 to include everything. + // See Reference: MSDN doc entitled "HTML Clipboard Format" + // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854 + if (startHTML == -1) { + startHTML = aCfhtml.Find("<!--StartFragment-->"); + if (startHTML == -1) { + return NS_OK; + } + } + if (endHTML == -1) { + const char endFragmentMarker[] = "<!--EndFragment-->"; + endHTML = aCfhtml.Find(endFragmentMarker); + if (endHTML == -1) { + return NS_OK; + } + endHTML += ArrayLength(endFragmentMarker) - 1; + } + + // create context string + nsAutoCString contextUTF8(Substring(aCfhtml, startHTML, startFragment - startHTML) + + NS_LITERAL_CSTRING("<!--" kInsertCookie "-->") + + Substring(aCfhtml, endFragment, endHTML - endFragment)); + + // validate startFragment + // make sure it's not in the middle of a HTML tag + // see bug #228879 for more details + int32_t curPos = startFragment; + while (curPos > startHTML) { + if (aCfhtml[curPos] == '>') { + // working backwards, the first thing we see is the end of a tag + // so StartFragment is good, so do nothing. + break; + } + if (aCfhtml[curPos] == '<') { + // if we are at the start, then we want to see the '<' + if (curPos != startFragment) { + // working backwards, the first thing we see is the start of a tag + // so StartFragment is bad, so we need to update it. + NS_ERROR("StartFragment byte count in the clipboard looks bad, see bug #228879"); + startFragment = curPos - 1; + } + break; + } + curPos--; + } + + // create fragment string + nsAutoCString fragmentUTF8(Substring(aCfhtml, startFragment, endFragment-startFragment)); + + // remove the StartFragment/EndFragment comments from the fragment, if present + RemoveFragComments(fragmentUTF8); + + // remove the StartFragment/EndFragment comments from the context, if present + RemoveFragComments(contextUTF8); + + // convert both strings to usc2 + const nsAFlatString& fragUcs2Str = NS_ConvertUTF8toUTF16(fragmentUTF8); + const nsAFlatString& cntxtUcs2Str = NS_ConvertUTF8toUTF16(contextUTF8); + + // translate platform linebreaks for fragment + int32_t oldLengthInChars = fragUcs2Str.Length() + 1; // +1 to include null terminator + int32_t newLengthInChars = 0; + *aStuffToPaste = nsLinebreakConverter::ConvertUnicharLineBreaks(fragUcs2Str.get(), + nsLinebreakConverter::eLinebreakAny, + nsLinebreakConverter::eLinebreakContent, + oldLengthInChars, &newLengthInChars); + NS_ENSURE_TRUE(*aStuffToPaste, NS_ERROR_FAILURE); + + // translate platform linebreaks for context + oldLengthInChars = cntxtUcs2Str.Length() + 1; // +1 to include null terminator + newLengthInChars = 0; + *aCfcontext = nsLinebreakConverter::ConvertUnicharLineBreaks(cntxtUcs2Str.get(), + nsLinebreakConverter::eLinebreakAny, + nsLinebreakConverter::eLinebreakContent, + oldLengthInChars, &newLengthInChars); + // it's ok for context to be empty. frag might be whole doc and contain all its context. + + // we're done! + return NS_OK; +} + +static nsresult +ImgFromData(const nsACString& aType, const nsACString& aData, nsString& aOutput) +{ + nsAutoCString data64; + nsresult rv = Base64Encode(aData, data64); + NS_ENSURE_SUCCESS(rv, rv); + + aOutput.AssignLiteral("<IMG src=\"data:"); + AppendUTF8toUTF16(aType, aOutput); + aOutput.AppendLiteral(";base64,"); + if (!AppendASCIItoUTF16(data64, aOutput, fallible_t())) { + return NS_ERROR_OUT_OF_MEMORY; + } + aOutput.AppendLiteral("\" alt=\"\" >"); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(HTMLEditor::BlobReader, nsIEditorBlobListener) + +HTMLEditor::BlobReader::BlobReader(BlobImpl* aBlob, + HTMLEditor* aHTMLEditor, + bool aIsSafe, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) + : mBlob(aBlob) + , mHTMLEditor(aHTMLEditor) + , mIsSafe(aIsSafe) + , mSourceDoc(aSourceDoc) + , mDestinationNode(aDestinationNode) + , mDestOffset(aDestOffset) + , mDoDeleteSelection(aDoDeleteSelection) +{ + MOZ_ASSERT(mBlob); + MOZ_ASSERT(mHTMLEditor); + MOZ_ASSERT(mDestinationNode); +} + +NS_IMETHODIMP +HTMLEditor::BlobReader::OnResult(const nsACString& aResult) +{ + nsString blobType; + mBlob->GetType(blobType); + + NS_ConvertUTF16toUTF8 type(blobType); + nsAutoString stuffToPaste; + nsresult rv = ImgFromData(type, aResult, stuffToPaste); + NS_ENSURE_SUCCESS(rv, rv); + + AutoEditBatch beginBatching(mHTMLEditor); + rv = mHTMLEditor->DoInsertHTMLWithContext(stuffToPaste, EmptyString(), + EmptyString(), + NS_LITERAL_STRING(kFileMime), + mSourceDoc, + mDestinationNode, mDestOffset, + mDoDeleteSelection, + mIsSafe, false); + return rv; +} + +NS_IMETHODIMP +HTMLEditor::BlobReader::OnError(const nsAString& aError) +{ + nsCOMPtr<nsINode> destNode = do_QueryInterface(mDestinationNode); + const nsPromiseFlatString& flat = PromiseFlatString(aError); + const char16_t* error = flat.get(); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Editor"), + destNode->OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, + "EditorFileDropFailed", + &error, 1); + return NS_OK; +} + +nsresult +HTMLEditor::InsertObject(const nsACString& aType, + nsISupports* aObject, + bool aIsSafe, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + nsresult rv; + + if (nsCOMPtr<BlobImpl> blob = do_QueryInterface(aObject)) { + RefPtr<BlobReader> br = new BlobReader(blob, this, aIsSafe, aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection); + nsCOMPtr<nsIEditorUtils> utils = + do_GetService("@mozilla.org/editor-utils;1"); + NS_ENSURE_TRUE(utils, NS_ERROR_FAILURE); + + nsCOMPtr<nsINode> node = do_QueryInterface(aDestinationNode); + MOZ_ASSERT(node); + + nsCOMPtr<nsIDOMBlob> domBlob = Blob::Create(node->GetOwnerGlobal(), blob); + NS_ENSURE_TRUE(domBlob, NS_ERROR_FAILURE); + + return utils->SlurpBlob(domBlob, node->OwnerDoc()->GetWindow(), br); + } + + nsAutoCString type(aType); + + // Check to see if we can insert an image file + bool insertAsImage = false; + nsCOMPtr<nsIFile> fileObj; + if (type.EqualsLiteral(kFileMime)) { + fileObj = do_QueryInterface(aObject); + if (fileObj) { + // Accept any image type fed to us + if (nsContentUtils::IsFileImage(fileObj, type)) { + insertAsImage = true; + } else { + // Reset type. + type.AssignLiteral(kFileMime); + } + } + } + + if (type.EqualsLiteral(kJPEGImageMime) || + type.EqualsLiteral(kJPGImageMime) || + type.EqualsLiteral(kPNGImageMime) || + type.EqualsLiteral(kGIFImageMime) || + insertAsImage) { + nsCString imageData; + if (insertAsImage) { + rv = nsContentUtils::SlurpFileToString(fileObj, imageData); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCOMPtr<nsIInputStream> imageStream = do_QueryInterface(aObject); + NS_ENSURE_TRUE(imageStream, NS_ERROR_FAILURE); + + rv = NS_ConsumeStream(imageStream, UINT32_MAX, imageData); + NS_ENSURE_SUCCESS(rv, rv); + + rv = imageStream->Close(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsAutoString stuffToPaste; + rv = ImgFromData(type, imageData, stuffToPaste); + NS_ENSURE_SUCCESS(rv, rv); + + AutoEditBatch beginBatching(this); + rv = DoInsertHTMLWithContext(stuffToPaste, EmptyString(), EmptyString(), + NS_LITERAL_STRING(kFileMime), + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + aIsSafe, false); + } + + return NS_OK; +} + +nsresult +HTMLEditor::InsertFromTransferable(nsITransferable* transferable, + nsIDOMDocument* aSourceDoc, + const nsAString& aContextStr, + const nsAString& aInfoStr, + bool havePrivateHTMLFlavor, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + nsresult rv = NS_OK; + nsAutoCString bestFlavor; + nsCOMPtr<nsISupports> genericDataObj; + uint32_t len = 0; + if (NS_SUCCEEDED( + transferable->GetAnyTransferData(bestFlavor, + getter_AddRefs(genericDataObj), + &len))) { + AutoTransactionsConserveSelection dontSpazMySelection(this); + nsAutoString flavor; + flavor.AssignWithConversion(bestFlavor); + nsAutoString stuffToPaste; + bool isSafe = IsSafeToInsertData(aSourceDoc); + + if (bestFlavor.EqualsLiteral(kFileMime) || + bestFlavor.EqualsLiteral(kJPEGImageMime) || + bestFlavor.EqualsLiteral(kJPGImageMime) || + bestFlavor.EqualsLiteral(kPNGImageMime) || + bestFlavor.EqualsLiteral(kGIFImageMime)) { + rv = InsertObject(bestFlavor, genericDataObj, isSafe, + aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection); + } else if (bestFlavor.EqualsLiteral(kNativeHTMLMime)) { + // note cf_html uses utf8, hence use length = len, not len/2 as in flavors below + nsCOMPtr<nsISupportsCString> textDataObj = do_QueryInterface(genericDataObj); + if (textDataObj && len > 0) { + nsAutoCString cfhtml; + textDataObj->GetData(cfhtml); + NS_ASSERTION(cfhtml.Length() <= (len), "Invalid length!"); + nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now + + rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext)); + if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) { + AutoEditBatch beginBatching(this); + // If we have our private HTML flavor, we will only use the fragment + // from the CF_HTML. The rest comes from the clipboard. + if (havePrivateHTMLFlavor) { + rv = DoInsertHTMLWithContext(cffragment, + aContextStr, aInfoStr, flavor, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } else { + rv = DoInsertHTMLWithContext(cffragment, + cfcontext, cfselection, flavor, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + + } + } else { + // In some platforms (like Linux), the clipboard might return data + // requested for unknown flavors (for example: + // application/x-moz-nativehtml). In this case, treat the data + // to be pasted as mere HTML to get the best chance of pasting it + // correctly. + bestFlavor.AssignLiteral(kHTMLMime); + // Fall through the next case + } + } + } + if (bestFlavor.EqualsLiteral(kHTMLMime) || + bestFlavor.EqualsLiteral(kUnicodeMime) || + bestFlavor.EqualsLiteral(kMozTextInternal)) { + nsCOMPtr<nsISupportsString> textDataObj = do_QueryInterface(genericDataObj); + if (textDataObj && len > 0) { + nsAutoString text; + textDataObj->GetData(text); + NS_ASSERTION(text.Length() <= (len/2), "Invalid length!"); + stuffToPaste.Assign(text.get(), len / 2); + } else { + nsCOMPtr<nsISupportsCString> textDataObj(do_QueryInterface(genericDataObj)); + if (textDataObj && len > 0) { + nsAutoCString text; + textDataObj->GetData(text); + NS_ASSERTION(text.Length() <= len, "Invalid length!"); + stuffToPaste.Assign(NS_ConvertUTF8toUTF16(Substring(text, 0, len))); + } + } + + if (!stuffToPaste.IsEmpty()) { + AutoEditBatch beginBatching(this); + if (bestFlavor.EqualsLiteral(kHTMLMime)) { + rv = DoInsertHTMLWithContext(stuffToPaste, + aContextStr, aInfoStr, flavor, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } else { + rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection); + } + } + } + } + + // Try to scroll the selection into view if the paste succeeded + if (NS_SUCCEEDED(rv)) { + ScrollSelectionIntoView(false); + } + return rv; +} + +static void +GetStringFromDataTransfer(nsIDOMDataTransfer* aDataTransfer, + const nsAString& aType, + int32_t aIndex, + nsAString& aOutputString) +{ + nsCOMPtr<nsIVariant> variant; + DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(aType, aIndex, getter_AddRefs(variant)); + if (variant) { + variant->GetAsAString(aOutputString); + } +} + +nsresult +HTMLEditor::InsertFromDataTransfer(DataTransfer* aDataTransfer, + int32_t aIndex, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + ErrorResult rv; + RefPtr<DOMStringList> types = aDataTransfer->MozTypesAt(aIndex, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + bool hasPrivateHTMLFlavor = types->Contains(NS_LITERAL_STRING(kHTMLContext)); + + bool isText = IsPlaintextEditor(); + bool isSafe = IsSafeToInsertData(aSourceDoc); + + uint32_t length = types->Length(); + for (uint32_t t = 0; t < length; t++) { + nsAutoString type; + types->Item(t, type); + + if (!isText) { + if (type.EqualsLiteral(kFileMime) || + type.EqualsLiteral(kJPEGImageMime) || + type.EqualsLiteral(kJPGImageMime) || + type.EqualsLiteral(kPNGImageMime) || + type.EqualsLiteral(kGIFImageMime)) { + nsCOMPtr<nsIVariant> variant; + DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(type, aIndex, getter_AddRefs(variant)); + if (variant) { + nsCOMPtr<nsISupports> object; + variant->GetAsISupports(getter_AddRefs(object)); + return InsertObject(NS_ConvertUTF16toUTF8(type), object, isSafe, + aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection); + } + } else if (type.EqualsLiteral(kNativeHTMLMime)) { + // Windows only clipboard parsing. + nsAutoString text; + GetStringFromDataTransfer(aDataTransfer, type, aIndex, text); + NS_ConvertUTF16toUTF8 cfhtml(text); + + nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now + + nsresult rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext)); + if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) { + AutoEditBatch beginBatching(this); + + if (hasPrivateHTMLFlavor) { + // If we have our private HTML flavor, we will only use the fragment + // from the CF_HTML. The rest comes from the clipboard. + nsAutoString contextString, infoString; + GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLContext), aIndex, contextString); + GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLInfo), aIndex, infoString); + return DoInsertHTMLWithContext(cffragment, + contextString, infoString, type, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } else { + return DoInsertHTMLWithContext(cffragment, + cfcontext, cfselection, type, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } + } + } else if (type.EqualsLiteral(kHTMLMime)) { + nsAutoString text, contextString, infoString; + GetStringFromDataTransfer(aDataTransfer, type, aIndex, text); + GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLContext), aIndex, contextString); + GetStringFromDataTransfer(aDataTransfer, NS_LITERAL_STRING(kHTMLInfo), aIndex, infoString); + + AutoEditBatch beginBatching(this); + if (type.EqualsLiteral(kHTMLMime)) { + return DoInsertHTMLWithContext(text, + contextString, infoString, type, + aSourceDoc, + aDestinationNode, aDestOffset, + aDoDeleteSelection, + isSafe); + } + } + } + + if (type.EqualsLiteral(kTextMime) || + type.EqualsLiteral(kMozTextInternal)) { + nsAutoString text; + GetStringFromDataTransfer(aDataTransfer, type, aIndex, text); + + AutoEditBatch beginBatching(this); + return InsertTextAt(text, aDestinationNode, aDestOffset, aDoDeleteSelection); + } + } + + return NS_OK; +} + +bool +HTMLEditor::HavePrivateHTMLFlavor(nsIClipboard* aClipboard) +{ + // check the clipboard for our special kHTMLContext flavor. If that is there, we know + // we have our own internal html format on clipboard. + + NS_ENSURE_TRUE(aClipboard, false); + bool bHavePrivateHTMLFlavor = false; + + const char* flavArray[] = { kHTMLContext }; + + if (NS_SUCCEEDED( + aClipboard->HasDataMatchingFlavors(flavArray, + ArrayLength(flavArray), + nsIClipboard::kGlobalClipboard, + &bHavePrivateHTMLFlavor))) { + return bHavePrivateHTMLFlavor; + } + + return false; +} + + +NS_IMETHODIMP +HTMLEditor::Paste(int32_t aSelectionType) +{ + if (!FireClipboardEvent(ePaste, aSelectionType)) { + return NS_OK; + } + + // Get Clipboard Service + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the nsITransferable interface for getting the data from the clipboard + nsCOMPtr<nsITransferable> trans; + rv = PrepareHTMLTransferable(getter_AddRefs(trans)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE); + // Get the Data from the clipboard + rv = clipboard->GetData(trans, aSelectionType); + NS_ENSURE_SUCCESS(rv, rv); + if (!IsModifiable()) { + return NS_OK; + } + + // also get additional html copy hints, if present + nsAutoString contextStr, infoStr; + + // If we have our internal html flavor on the clipboard, there is special + // context to use instead of cfhtml context. + bool bHavePrivateHTMLFlavor = HavePrivateHTMLFlavor(clipboard); + if (bHavePrivateHTMLFlavor) { + nsCOMPtr<nsISupports> contextDataObj, infoDataObj; + uint32_t contextLen, infoLen; + nsCOMPtr<nsISupportsString> textDataObj; + + nsCOMPtr<nsITransferable> contextTrans = + do_CreateInstance("@mozilla.org/widget/transferable;1"); + NS_ENSURE_TRUE(contextTrans, NS_ERROR_NULL_POINTER); + contextTrans->Init(nullptr); + contextTrans->AddDataFlavor(kHTMLContext); + clipboard->GetData(contextTrans, aSelectionType); + contextTrans->GetTransferData(kHTMLContext, getter_AddRefs(contextDataObj), &contextLen); + + nsCOMPtr<nsITransferable> infoTrans = + do_CreateInstance("@mozilla.org/widget/transferable;1"); + NS_ENSURE_TRUE(infoTrans, NS_ERROR_NULL_POINTER); + infoTrans->Init(nullptr); + infoTrans->AddDataFlavor(kHTMLInfo); + clipboard->GetData(infoTrans, aSelectionType); + infoTrans->GetTransferData(kHTMLInfo, getter_AddRefs(infoDataObj), &infoLen); + + if (contextDataObj) { + nsAutoString text; + textDataObj = do_QueryInterface(contextDataObj); + textDataObj->GetData(text); + NS_ASSERTION(text.Length() <= (contextLen/2), "Invalid length!"); + contextStr.Assign(text.get(), contextLen / 2); + } + + if (infoDataObj) { + nsAutoString text; + textDataObj = do_QueryInterface(infoDataObj); + textDataObj->GetData(text); + NS_ASSERTION(text.Length() <= (infoLen/2), "Invalid length!"); + infoStr.Assign(text.get(), infoLen / 2); + } + } + + // handle transferable hooks + nsCOMPtr<nsIDOMDocument> domdoc; + GetDocument(getter_AddRefs(domdoc)); + if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, trans)) { + return NS_OK; + } + + return InsertFromTransferable(trans, nullptr, contextStr, infoStr, bHavePrivateHTMLFlavor, + nullptr, 0, true); +} + +NS_IMETHODIMP +HTMLEditor::PasteTransferable(nsITransferable* aTransferable) +{ + // Use an invalid value for the clipboard type as data comes from aTransferable + // and we don't currently implement a way to put that in the data transfer yet. + if (!FireClipboardEvent(ePaste, nsIClipboard::kGlobalClipboard)) { + return NS_OK; + } + + // handle transferable hooks + nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument(); + if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, aTransferable)) { + return NS_OK; + } + + nsAutoString contextStr, infoStr; + return InsertFromTransferable(aTransferable, nullptr, contextStr, infoStr, false, + nullptr, 0, true); +} + +/** + * HTML PasteNoFormatting. Ignore any HTML styles and formating in paste source. + */ +NS_IMETHODIMP +HTMLEditor::PasteNoFormatting(int32_t aSelectionType) +{ + if (!FireClipboardEvent(ePaste, aSelectionType)) { + return NS_OK; + } + + ForceCompositionEnd(); + + // Get Clipboard Service + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the nsITransferable interface for getting the data from the clipboard. + // use TextEditor::PrepareTransferable() to force unicode plaintext data. + nsCOMPtr<nsITransferable> trans; + rv = TextEditor::PrepareTransferable(getter_AddRefs(trans)); + if (NS_SUCCEEDED(rv) && trans) { + // Get the Data from the clipboard + if (NS_SUCCEEDED(clipboard->GetData(trans, aSelectionType)) && + IsModifiable()) { + const nsAFlatString& empty = EmptyString(); + rv = InsertFromTransferable(trans, nullptr, empty, empty, false, nullptr, 0, + true); + } + } + + return rv; +} + +// The following arrays contain the MIME types that we can paste. The arrays +// are used by CanPaste() and CanPasteTransferable() below. + +static const char* textEditorFlavors[] = { kUnicodeMime }; +static const char* textHtmlEditorFlavors[] = { kUnicodeMime, kHTMLMime, + kJPEGImageMime, kJPGImageMime, + kPNGImageMime, kGIFImageMime }; + +NS_IMETHODIMP +HTMLEditor::CanPaste(int32_t aSelectionType, + bool* aCanPaste) +{ + NS_ENSURE_ARG_POINTER(aCanPaste); + *aCanPaste = false; + + // can't paste if readonly + if (!IsModifiable()) { + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool haveFlavors; + + // Use the flavors depending on the current editor mask + if (IsPlaintextEditor()) { + rv = clipboard->HasDataMatchingFlavors(textEditorFlavors, + ArrayLength(textEditorFlavors), + aSelectionType, &haveFlavors); + } else { + rv = clipboard->HasDataMatchingFlavors(textHtmlEditorFlavors, + ArrayLength(textHtmlEditorFlavors), + aSelectionType, &haveFlavors); + } + NS_ENSURE_SUCCESS(rv, rv); + + *aCanPaste = haveFlavors; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable, + bool* aCanPaste) +{ + NS_ENSURE_ARG_POINTER(aCanPaste); + + // can't paste if readonly + if (!IsModifiable()) { + *aCanPaste = false; + return NS_OK; + } + + // If |aTransferable| is null, assume that a paste will succeed. + if (!aTransferable) { + *aCanPaste = true; + return NS_OK; + } + + // Peek in |aTransferable| to see if it contains a supported MIME type. + + // Use the flavors depending on the current editor mask + const char ** flavors; + unsigned length; + if (IsPlaintextEditor()) { + flavors = textEditorFlavors; + length = ArrayLength(textEditorFlavors); + } else { + flavors = textHtmlEditorFlavors; + length = ArrayLength(textHtmlEditorFlavors); + } + + for (unsigned int i = 0; i < length; i++, flavors++) { + nsCOMPtr<nsISupports> data; + uint32_t dataLen; + nsresult rv = aTransferable->GetTransferData(*flavors, + getter_AddRefs(data), + &dataLen); + if (NS_SUCCEEDED(rv) && data) { + *aCanPaste = true; + return NS_OK; + } + } + + *aCanPaste = false; + return NS_OK; +} + +/** + * HTML PasteAsQuotation: Paste in a blockquote type=cite. + */ +NS_IMETHODIMP +HTMLEditor::PasteAsQuotation(int32_t aSelectionType) +{ + if (IsPlaintextEditor()) { + return PasteAsPlaintextQuotation(aSelectionType); + } + + nsAutoString citation; + return PasteAsCitedQuotation(citation, aSelectionType); +} + +NS_IMETHODIMP +HTMLEditor::PasteAsCitedQuotation(const nsAString& aCitation, + int32_t aSelectionType) +{ + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertQuotation, + nsIEditor::eNext); + + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel || handled) { + return NS_OK; // rules canceled the operation + } + + nsCOMPtr<Element> newNode = + DeleteSelectionAndCreateElement(*nsGkAtoms::blockquote); + NS_ENSURE_TRUE(newNode, NS_ERROR_NULL_POINTER); + + // Try to set type=cite. Ignore it if this fails. + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("cite"), true); + + // Set the selection to the underneath the node we just inserted: + rv = selection->Collapse(newNode, 0); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensure that the inserted <blockquote> has a frame to make it IsEditable. + FlushFrames(); + + return Paste(aSelectionType); +} + +/** + * Paste a plaintext quotation. + */ +NS_IMETHODIMP +HTMLEditor::PasteAsPlaintextQuotation(int32_t aSelectionType) +{ + // Get Clipboard Service + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Create generic Transferable for getting the data + nsCOMPtr<nsITransferable> trans = + do_CreateInstance("@mozilla.org/widget/transferable;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDocument> destdoc = GetDocument(); + nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr; + trans->Init(loadContext); + + // We only handle plaintext pastes here + trans->AddDataFlavor(kUnicodeMime); + + // Get the Data from the clipboard + clipboard->GetData(trans, aSelectionType); + + // Now we ask the transferable for the data + // it still owns the data, we just have a pointer to it. + // If it can't support a "text" output of the data the call will fail + nsCOMPtr<nsISupports> genericDataObj; + uint32_t len = 0; + nsAutoCString flav; + rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj), &len); + NS_ENSURE_SUCCESS(rv, rv); + + if (flav.EqualsLiteral(kUnicodeMime)) { + nsCOMPtr<nsISupportsString> textDataObj = do_QueryInterface(genericDataObj); + if (textDataObj && len > 0) { + nsAutoString stuffToPaste; + textDataObj->GetData(stuffToPaste); + NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!"); + AutoEditBatch beginBatching(this); + rv = InsertAsPlaintextQuotation(stuffToPaste, true, 0); + } + } + + return rv; +} + +NS_IMETHODIMP +HTMLEditor::InsertTextWithQuotations(const nsAString& aStringToInsert) +{ + AutoEditBatch beginBatching(this); + // The whole operation should be undoable in one transaction: + BeginTransaction(); + + // We're going to loop over the string, collecting up a "hunk" + // that's all the same type (quoted or not), + // Whenever the quotedness changes (or we reach the string's end) + // we will insert the hunk all at once, quoted or non. + + static const char16_t cite('>'); + bool curHunkIsQuoted = (aStringToInsert.First() == cite); + + nsAString::const_iterator hunkStart, strEnd; + aStringToInsert.BeginReading(hunkStart); + aStringToInsert.EndReading(strEnd); + + // In the loop below, we only look for DOM newlines (\n), + // because we don't have a FindChars method that can look + // for both \r and \n. \r is illegal in the dom anyway, + // but in debug builds, let's take the time to verify that + // there aren't any there: +#ifdef DEBUG + nsAString::const_iterator dbgStart (hunkStart); + if (FindCharInReadable('\r', dbgStart, strEnd)) { + NS_ASSERTION(false, + "Return characters in DOM! InsertTextWithQuotations may be wrong"); + } +#endif /* DEBUG */ + + // Loop over lines: + nsresult rv = NS_OK; + nsAString::const_iterator lineStart (hunkStart); + // We will break from inside when we run out of newlines. + for (;;) { + // Search for the end of this line (dom newlines, see above): + bool found = FindCharInReadable('\n', lineStart, strEnd); + bool quoted = false; + if (found) { + // if there's another newline, lineStart now points there. + // Loop over any consecutive newline chars: + nsAString::const_iterator firstNewline (lineStart); + while (*lineStart == '\n') { + ++lineStart; + } + quoted = (*lineStart == cite); + if (quoted == curHunkIsQuoted) { + continue; + } + // else we're changing state, so we need to insert + // from curHunk to lineStart then loop around. + + // But if the current hunk is quoted, then we want to make sure + // that any extra newlines on the end do not get included in + // the quoted section: blank lines flaking a quoted section + // should be considered unquoted, so that if the user clicks + // there and starts typing, the new text will be outside of + // the quoted block. + if (curHunkIsQuoted) { + lineStart = firstNewline; + + // 'firstNewline' points to the first '\n'. We want to + // ensure that this first newline goes into the hunk + // since quoted hunks can be displayed as blocks + // (and the newline should become invisible in this case). + // So the next line needs to start at the next character. + lineStart++; + } + } + + // If no newline found, lineStart is now strEnd and we can finish up, + // inserting from curHunk to lineStart then returning. + const nsAString &curHunk = Substring(hunkStart, lineStart); + nsCOMPtr<nsIDOMNode> dummyNode; + if (curHunkIsQuoted) { + rv = InsertAsPlaintextQuotation(curHunk, false, + getter_AddRefs(dummyNode)); + } else { + rv = InsertText(curHunk); + } + if (!found) { + break; + } + curHunkIsQuoted = quoted; + hunkStart = lineStart; + } + + EndTransaction(); + + return rv; +} + +NS_IMETHODIMP +HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText, + nsIDOMNode** aNodeInserted) +{ + if (IsPlaintextEditor()) { + return InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted); + } + + nsAutoString citation; + return InsertAsCitedQuotation(aQuotedText, citation, false, + aNodeInserted); +} + +// Insert plaintext as a quotation, with cite marks (e.g. "> "). +// This differs from its corresponding method in TextEditor +// in that here, quoted material is enclosed in a <pre> tag +// in order to preserve the original line wrapping. +NS_IMETHODIMP +HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText, + bool aAddCites, + nsIDOMNode** aNodeInserted) +{ + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertQuotation, + nsIEditor::eNext); + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel || handled) { + return NS_OK; // rules canceled the operation + } + + // Wrap the inserted quote in a <span> so we can distinguish it. If we're + // inserting into the <body>, we use a <span> which is displayed as a block + // and sized to the screen using 98 viewport width units. + // We could use 100vw, but 98vw avoids a horizontal scroll bar where possible. + // All this is done to wrap overlong lines to the screen and not to the + // container element, the width-restricted body. + nsCOMPtr<Element> newNode = + DeleteSelectionAndCreateElement(*nsGkAtoms::span); + + // If this succeeded, then set selection inside the pre + // so the inserted text will end up there. + // If it failed, we don't care what the return value was, + // but we'll fall through and try to insert the text anyway. + if (newNode) { + // Add an attribute on the pre node so we'll know it's a quotation. + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::mozquote, + NS_LITERAL_STRING("true"), true); + // Allow wrapping on spans so long lines get wrapped to the screen. + nsCOMPtr<nsINode> parent = newNode->GetParentNode(); + if (parent && parent->IsHTMLElement(nsGkAtoms::body)) { + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::style, + NS_LITERAL_STRING("white-space: pre-wrap; display: block; width: 98vw;"), + true); + } else { + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::style, + NS_LITERAL_STRING("white-space: pre-wrap;"), true); + } + + // and set the selection inside it: + selection->Collapse(newNode, 0); + } + + // Ensure that the inserted <span> has a frame to make it IsEditable. + FlushFrames(); + + if (aAddCites) { + rv = TextEditor::InsertAsQuotation(aQuotedText, aNodeInserted); + } else { + rv = TextEditor::InsertText(aQuotedText); + } + // Note that if !aAddCites, aNodeInserted isn't set. + // That's okay because the routines that use aAddCites + // don't need to know the inserted node. + + if (aNodeInserted && NS_SUCCEEDED(rv)) { + *aNodeInserted = GetAsDOMNode(newNode); + NS_IF_ADDREF(*aNodeInserted); + } + + // Set the selection to just after the inserted node: + if (NS_SUCCEEDED(rv) && newNode) { + nsCOMPtr<nsINode> parent = newNode->GetParentNode(); + int32_t offset = parent ? parent->IndexOf(newNode) : -1; + if (parent) { + selection->Collapse(parent, offset + 1); + } + } + return rv; +} + +NS_IMETHODIMP +HTMLEditor::StripCites() +{ + return TextEditor::StripCites(); +} + +NS_IMETHODIMP +HTMLEditor::Rewrap(bool aRespectNewlines) +{ + return TextEditor::Rewrap(aRespectNewlines); +} + +NS_IMETHODIMP +HTMLEditor::InsertAsCitedQuotation(const nsAString& aQuotedText, + const nsAString& aCitation, + bool aInsertHTML, + nsIDOMNode** aNodeInserted) +{ + // Don't let anyone insert html into a "plaintext" editor: + if (IsPlaintextEditor()) { + NS_ASSERTION(!aInsertHTML, "InsertAsCitedQuotation: trying to insert html into plaintext editor"); + return InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted); + } + + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertQuotation, + nsIEditor::eNext); + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel || handled) { + return NS_OK; // rules canceled the operation + } + + nsCOMPtr<Element> newNode = + DeleteSelectionAndCreateElement(*nsGkAtoms::blockquote); + NS_ENSURE_TRUE(newNode, NS_ERROR_NULL_POINTER); + + // Try to set type=cite. Ignore it if this fails. + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("cite"), true); + + if (!aCitation.IsEmpty()) { + newNode->SetAttr(kNameSpaceID_None, nsGkAtoms::cite, aCitation, true); + } + + // Set the selection inside the blockquote so aQuotedText will go there: + selection->Collapse(newNode, 0); + + // Ensure that the inserted <blockquote> has a frame to make it IsEditable. + FlushFrames(); + + if (aInsertHTML) { + rv = LoadHTML(aQuotedText); + } else { + rv = InsertText(aQuotedText); // XXX ignore charset + } + + if (aNodeInserted && NS_SUCCEEDED(rv)) { + *aNodeInserted = GetAsDOMNode(newNode); + NS_IF_ADDREF(*aNodeInserted); + } + + // Set the selection to just after the inserted node: + if (NS_SUCCEEDED(rv) && newNode) { + nsCOMPtr<nsINode> parent = newNode->GetParentNode(); + int32_t offset = parent ? parent->IndexOf(newNode) : -1; + if (parent) { + selection->Collapse(parent, offset + 1); + } + } + return rv; +} + + +void RemoveBodyAndHead(nsINode& aNode) +{ + nsCOMPtr<nsIContent> body, head; + // find the body and head nodes if any. + // look only at immediate children of aNode. + for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild(); + child; + child = child->GetNextSibling()) { + if (child->IsHTMLElement(nsGkAtoms::body)) { + body = child; + } else if (child->IsHTMLElement(nsGkAtoms::head)) { + head = child; + } + } + if (head) { + ErrorResult ignored; + aNode.RemoveChild(*head, ignored); + } + if (body) { + nsCOMPtr<nsIContent> child = body->GetFirstChild(); + while (child) { + ErrorResult ignored; + aNode.InsertBefore(*child, body, ignored); + child = body->GetFirstChild(); + } + + ErrorResult ignored; + aNode.RemoveChild(*body, ignored); + } +} + +/** + * This function finds the target node that we will be pasting into. aStart is + * the context that we're given and aResult will be the target. Initially, + * *aResult must be nullptr. + * + * The target for a paste is found by either finding the node that contains + * the magical comment node containing kInsertCookie or, failing that, the + * firstChild of the firstChild (until we reach a leaf). + */ +nsresult FindTargetNode(nsIDOMNode *aStart, nsCOMPtr<nsIDOMNode> &aResult) +{ + NS_ENSURE_TRUE(aStart, NS_OK); + + nsCOMPtr<nsIDOMNode> child, tmp; + + nsresult rv = aStart->GetFirstChild(getter_AddRefs(child)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!child) { + // If the current result is nullptr, then aStart is a leaf, and is the + // fallback result. + if (!aResult) { + aResult = aStart; + } + return NS_OK; + } + + do { + // Is this child the magical cookie? + nsCOMPtr<nsIDOMComment> comment = do_QueryInterface(child); + if (comment) { + nsAutoString data; + rv = comment->GetData(data); + NS_ENSURE_SUCCESS(rv, rv); + + if (data.EqualsLiteral(kInsertCookie)) { + // Yes it is! Return an error so we bubble out and short-circuit the + // search. + aResult = aStart; + + // Note: it doesn't matter if this fails. + aStart->RemoveChild(child, getter_AddRefs(tmp)); + + return NS_SUCCESS_EDITOR_FOUND_TARGET; + } + } + + rv = FindTargetNode(child, aResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (rv == NS_SUCCESS_EDITOR_FOUND_TARGET) { + return NS_SUCCESS_EDITOR_FOUND_TARGET; + } + + rv = child->GetNextSibling(getter_AddRefs(tmp)); + NS_ENSURE_SUCCESS(rv, rv); + + child = tmp; + } while (child); + + return NS_OK; +} + +nsresult +HTMLEditor::CreateDOMFragmentFromPaste(const nsAString& aInputString, + const nsAString& aContextStr, + const nsAString& aInfoStr, + nsCOMPtr<nsIDOMNode>* outFragNode, + nsCOMPtr<nsIDOMNode>* outStartNode, + nsCOMPtr<nsIDOMNode>* outEndNode, + int32_t* outStartOffset, + int32_t* outEndOffset, + bool aTrustedInput) +{ + NS_ENSURE_TRUE(outFragNode && outStartNode && outEndNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + // if we have context info, create a fragment for that + nsresult rv = NS_OK; + nsCOMPtr<nsIDOMNode> contextLeaf; + RefPtr<DocumentFragment> contextAsNode; + if (!aContextStr.IsEmpty()) { + rv = ParseFragment(aContextStr, nullptr, doc, getter_AddRefs(contextAsNode), + aTrustedInput); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(contextAsNode, NS_ERROR_FAILURE); + + rv = StripFormattingNodes(*contextAsNode); + NS_ENSURE_SUCCESS(rv, rv); + + RemoveBodyAndHead(*contextAsNode); + + rv = FindTargetNode(contextAsNode, contextLeaf); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIContent> contextLeafAsContent = do_QueryInterface(contextLeaf); + + // create fragment for pasted html + nsIAtom* contextAtom; + if (contextLeafAsContent) { + contextAtom = contextLeafAsContent->NodeInfo()->NameAtom(); + if (contextLeafAsContent->IsHTMLElement(nsGkAtoms::html)) { + contextAtom = nsGkAtoms::body; + } + } else { + contextAtom = nsGkAtoms::body; + } + RefPtr<DocumentFragment> fragment; + rv = ParseFragment(aInputString, + contextAtom, + doc, + getter_AddRefs(fragment), + aTrustedInput); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(fragment, NS_ERROR_FAILURE); + + RemoveBodyAndHead(*fragment); + + if (contextAsNode) { + // unite the two trees + nsCOMPtr<nsIDOMNode> junk; + contextLeaf->AppendChild(fragment, getter_AddRefs(junk)); + fragment = contextAsNode; + } + + rv = StripFormattingNodes(*fragment, true); + NS_ENSURE_SUCCESS(rv, rv); + + // If there was no context, then treat all of the data we did get as the + // pasted data. + if (contextLeaf) { + *outEndNode = *outStartNode = contextLeaf; + } else { + *outEndNode = *outStartNode = fragment; + } + + *outFragNode = fragment.forget(); + *outStartOffset = 0; + + // get the infoString contents + if (!aInfoStr.IsEmpty()) { + int32_t sep = aInfoStr.FindChar((char16_t)','); + nsAutoString numstr1(Substring(aInfoStr, 0, sep)); + nsAutoString numstr2(Substring(aInfoStr, sep+1, aInfoStr.Length() - (sep+1))); + + // Move the start and end children. + nsresult err; + int32_t num = numstr1.ToInteger(&err); + + nsCOMPtr<nsIDOMNode> tmp; + while (num--) { + (*outStartNode)->GetFirstChild(getter_AddRefs(tmp)); + NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE); + tmp.swap(*outStartNode); + } + + num = numstr2.ToInteger(&err); + while (num--) { + (*outEndNode)->GetLastChild(getter_AddRefs(tmp)); + NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE); + tmp.swap(*outEndNode); + } + } + + nsCOMPtr<nsINode> node = do_QueryInterface(*outEndNode); + *outEndOffset = node->Length(); + return NS_OK; +} + + +nsresult +HTMLEditor::ParseFragment(const nsAString& aFragStr, + nsIAtom* aContextLocalName, + nsIDocument* aTargetDocument, + DocumentFragment** aFragment, + bool aTrustedInput) +{ + nsAutoScriptBlockerSuppressNodeRemoved autoBlocker; + + RefPtr<DocumentFragment> fragment = + new DocumentFragment(aTargetDocument->NodeInfoManager()); + nsresult rv = nsContentUtils::ParseFragmentHTML(aFragStr, + fragment, + aContextLocalName ? + aContextLocalName : nsGkAtoms::body, + kNameSpaceID_XHTML, + false, + true); + if (!aTrustedInput) { + nsTreeSanitizer sanitizer(aContextLocalName ? + nsIParserUtils::SanitizerAllowStyle : + nsIParserUtils::SanitizerAllowComments); + sanitizer.Sanitize(fragment); + } + fragment.forget(aFragment); + return rv; +} + +void +HTMLEditor::CreateListOfNodesToPaste( + DocumentFragment& aFragment, + nsTArray<OwningNonNull<nsINode>>& outNodeList, + nsINode* aStartNode, + int32_t aStartOffset, + nsINode* aEndNode, + int32_t aEndOffset) +{ + // If no info was provided about the boundary between context and stream, + // then assume all is stream. + if (!aStartNode) { + aStartNode = &aFragment; + aStartOffset = 0; + aEndNode = &aFragment; + aEndOffset = aFragment.Length(); + } + + RefPtr<nsRange> docFragRange; + nsresult rv = nsRange::CreateRange(aStartNode, aStartOffset, + aEndNode, aEndOffset, + getter_AddRefs(docFragRange)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, ); + + // Now use a subtree iterator over the range to create a list of nodes + TrivialFunctor functor; + DOMSubtreeIterator iter; + rv = iter.Init(*docFragRange); + NS_ENSURE_SUCCESS(rv, ); + iter.AppendList(functor, outNodeList); +} + +void +HTMLEditor::GetListAndTableParents(StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodeList, + nsTArray<OwningNonNull<Element>>& outArray) +{ + MOZ_ASSERT(aNodeList.Length()); + + // Build up list of parents of first (or last) node in list that are either + // lists, or tables. + int32_t idx = aStartOrEnd == StartOrEnd::end ? aNodeList.Length() - 1 : 0; + + for (nsCOMPtr<nsINode> node = aNodeList[idx]; node; + node = node->GetParentNode()) { + if (HTMLEditUtils::IsList(node) || HTMLEditUtils::IsTable(node)) { + outArray.AppendElement(*node->AsElement()); + } + } +} + +int32_t +HTMLEditor::DiscoverPartialListsAndTables( + nsTArray<OwningNonNull<nsINode>>& aPasteNodes, + nsTArray<OwningNonNull<Element>>& aListsAndTables) +{ + int32_t ret = -1; + int32_t listAndTableParents = aListsAndTables.Length(); + + // Scan insertion list for table elements (other than table). + for (auto& curNode : aPasteNodes) { + if (HTMLEditUtils::IsTableElement(curNode) && + !curNode->IsHTMLElement(nsGkAtoms::table)) { + nsCOMPtr<Element> table = curNode->GetParentElement(); + while (table && !table->IsHTMLElement(nsGkAtoms::table)) { + table = table->GetParentElement(); + } + if (table) { + int32_t idx = aListsAndTables.IndexOf(table); + if (idx == -1) { + return ret; + } + ret = idx; + if (ret == listAndTableParents - 1) { + return ret; + } + } + } + if (HTMLEditUtils::IsListItem(curNode)) { + nsCOMPtr<Element> list = curNode->GetParentElement(); + while (list && !HTMLEditUtils::IsList(list)) { + list = list->GetParentElement(); + } + if (list) { + int32_t idx = aListsAndTables.IndexOf(list); + if (idx == -1) { + return ret; + } + ret = idx; + if (ret == listAndTableParents - 1) { + return ret; + } + } + } + } + return ret; +} + +nsINode* +HTMLEditor::ScanForListAndTableStructure( + StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodes, + Element& aListOrTable) +{ + // Look upward from first/last paste node for a piece of this list/table + int32_t idx = aStartOrEnd == StartOrEnd::end ? aNodes.Length() - 1 : 0; + bool isList = HTMLEditUtils::IsList(&aListOrTable); + + for (nsCOMPtr<nsINode> node = aNodes[idx]; node; + node = node->GetParentNode()) { + if ((isList && HTMLEditUtils::IsListItem(node)) || + (!isList && HTMLEditUtils::IsTableElement(node) && + !node->IsHTMLElement(nsGkAtoms::table))) { + nsCOMPtr<Element> structureNode = node->GetParentElement(); + if (isList) { + while (structureNode && !HTMLEditUtils::IsList(structureNode)) { + structureNode = structureNode->GetParentElement(); + } + } else { + while (structureNode && + !structureNode->IsHTMLElement(nsGkAtoms::table)) { + structureNode = structureNode->GetParentElement(); + } + } + if (structureNode == &aListOrTable) { + if (isList) { + return structureNode; + } + return node; + } + } + } + return nullptr; +} + +void +HTMLEditor::ReplaceOrphanedStructure( + StartOrEnd aStartOrEnd, + nsTArray<OwningNonNull<nsINode>>& aNodeArray, + nsTArray<OwningNonNull<Element>>& aListAndTableArray, + int32_t aHighWaterMark) +{ + OwningNonNull<Element> curNode = aListAndTableArray[aHighWaterMark]; + + // Find substructure of list or table that must be included in paste. + nsCOMPtr<nsINode> replaceNode = + ScanForListAndTableStructure(aStartOrEnd, aNodeArray, curNode); + + if (!replaceNode) { + return; + } + + // If we found substructure, paste it instead of its descendants. + // Only replace with the substructure if all the nodes in the list are + // descendants. + bool shouldReplaceNodes = true; + for (uint32_t i = 0; i < aNodeArray.Length(); i++) { + uint32_t idx = aStartOrEnd == StartOrEnd::start ? + i : (aNodeArray.Length() - i - 1); + OwningNonNull<nsINode> endpoint = aNodeArray[idx]; + if (!EditorUtils::IsDescendantOf(endpoint, replaceNode)) { + shouldReplaceNodes = false; + break; + } + } + + if (shouldReplaceNodes) { + // Now replace the removed nodes with the structural parent + aNodeArray.Clear(); + if (aStartOrEnd == StartOrEnd::end) { + aNodeArray.AppendElement(*replaceNode); + } else { + aNodeArray.InsertElementAt(0, *replaceNode); + } + } +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLEditorEventListener.cpp b/editor/libeditor/HTMLEditorEventListener.cpp new file mode 100644 index 000000000..8fb9459c2 --- /dev/null +++ b/editor/libeditor/HTMLEditorEventListener.cpp @@ -0,0 +1,215 @@ +/* -*- 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 "HTMLEditorEventListener.h" + +#include "HTMLEditUtils.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/Selection.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIDOMElement.h" +#include "nsIDOMEvent.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMMouseEvent.h" +#include "nsIDOMNode.h" +#include "nsIEditor.h" +#include "nsIHTMLInlineTableEditor.h" +#include "nsIHTMLObjectResizer.h" +#include "nsISupportsImpl.h" +#include "nsLiteralString.h" +#include "nsQueryObject.h" +#include "nsRange.h" + +namespace mozilla { + +using namespace dom; + +#ifdef DEBUG +nsresult +HTMLEditorEventListener::Connect(EditorBase* aEditorBase) +{ + nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryObject(aEditorBase); + nsCOMPtr<nsIHTMLInlineTableEditor> htmlInlineTableEditor = + do_QueryObject(aEditorBase); + NS_PRECONDITION(htmlEditor && htmlInlineTableEditor, + "Set HTMLEditor or its sub class"); + return EditorEventListener::Connect(aEditorBase); +} +#endif + +HTMLEditor* +HTMLEditorEventListener::GetHTMLEditor() +{ + // mEditor must be HTMLEditor or its subclass. + return static_cast<HTMLEditor*>(mEditorBase); +} + +nsresult +HTMLEditorEventListener::MouseUp(nsIDOMMouseEvent* aMouseEvent) +{ + HTMLEditor* htmlEditor = GetHTMLEditor(); + + nsCOMPtr<nsIDOMEventTarget> target; + nsresult rv = aMouseEvent->AsEvent()->GetTarget(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(target, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(target); + + int32_t clientX, clientY; + aMouseEvent->GetClientX(&clientX); + aMouseEvent->GetClientY(&clientY); + htmlEditor->MouseUp(clientX, clientY, element); + + return EditorEventListener::MouseUp(aMouseEvent); +} + +nsresult +HTMLEditorEventListener::MouseDown(nsIDOMMouseEvent* aMouseEvent) +{ + HTMLEditor* htmlEditor = GetHTMLEditor(); + // Contenteditable should disregard mousedowns outside it. + // IsAcceptableInputEvent() checks it for a mouse event. + if (!htmlEditor->IsAcceptableInputEvent(aMouseEvent->AsEvent())) { + // If it's not acceptable mousedown event (including when mousedown event + // is fired outside of the active editing host), we need to commit + // composition because it will be change the selection to the clicked + // point. Then, we won't be able to commit the composition. + return EditorEventListener::MouseDown(aMouseEvent); + } + + // Detect only "context menu" click + // XXX This should be easier to do! + // But eDOMEvents_contextmenu and eContextMenu is not exposed in any event + // interface :-( + int16_t buttonNumber; + nsresult rv = aMouseEvent->GetButton(&buttonNumber); + NS_ENSURE_SUCCESS(rv, rv); + + bool isContextClick = buttonNumber == 2; + + int32_t clickCount; + rv = aMouseEvent->GetDetail(&clickCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMEventTarget> target; + rv = aMouseEvent->AsEvent()->GetExplicitOriginalTarget(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(target, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(target); + + if (isContextClick || (buttonNumber == 0 && clickCount == 2)) { + RefPtr<Selection> selection = mEditorBase->GetSelection(); + NS_ENSURE_TRUE(selection, NS_OK); + + // Get location of mouse within target node + nsCOMPtr<nsIDOMNode> parent; + rv = aMouseEvent->GetRangeParent(getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parent, NS_ERROR_FAILURE); + + int32_t offset = 0; + rv = aMouseEvent->GetRangeOffset(&offset); + NS_ENSURE_SUCCESS(rv, rv); + + // Detect if mouse point is within current selection for context click + bool nodeIsInSelection = false; + if (isContextClick && !selection->Collapsed()) { + int32_t rangeCount; + rv = selection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, rv); + + for (int32_t i = 0; i < rangeCount; i++) { + RefPtr<nsRange> range = selection->GetRangeAt(i); + if (!range) { + // Don't bail yet, iterate through them all + continue; + } + + range->IsPointInRange(parent, offset, &nodeIsInSelection); + + // Done when we find a range that we are in + if (nodeIsInSelection) { + break; + } + } + } + nsCOMPtr<nsIDOMNode> node = do_QueryInterface(target); + if (node && !nodeIsInSelection) { + if (!element) { + if (isContextClick) { + // Set the selection to the point under the mouse cursor: + selection->Collapse(parent, offset); + } else { + // Get enclosing link if in text so we can select the link + nsCOMPtr<nsIDOMElement> linkElement; + rv = htmlEditor->GetElementOrParentByTagName( + NS_LITERAL_STRING("href"), node, + getter_AddRefs(linkElement)); + NS_ENSURE_SUCCESS(rv, rv); + if (linkElement) { + element = linkElement; + } + } + } + // Select entire element clicked on if NOT within an existing selection + // and not the entire body, or table-related elements + if (element) { + nsCOMPtr<nsIDOMNode> selectAllNode = + htmlEditor->FindUserSelectAllNode(element); + + if (selectAllNode) { + nsCOMPtr<nsIDOMElement> newElement = do_QueryInterface(selectAllNode); + if (newElement) { + node = selectAllNode; + element = newElement; + } + } + + if (isContextClick && !HTMLEditUtils::IsImage(node)) { + selection->Collapse(parent, offset); + } else { + htmlEditor->SelectElement(element); + } + } + } + // HACK !!! Context click places the caret but the context menu consumes + // the event; so we need to check resizing state ourselves + htmlEditor->CheckSelectionStateForAnonymousButtons(selection); + + // Prevent bubbling if we changed selection or + // for all context clicks + if (element || isContextClick) { + aMouseEvent->AsEvent()->PreventDefault(); + return NS_OK; + } + } else if (!isContextClick && buttonNumber == 0 && clickCount == 1) { + // if the target element is an image, we have to display resizers + int32_t clientX, clientY; + aMouseEvent->GetClientX(&clientX); + aMouseEvent->GetClientY(&clientY); + htmlEditor->MouseDown(clientX, clientY, element, aMouseEvent->AsEvent()); + } + + return EditorEventListener::MouseDown(aMouseEvent); +} + +nsresult +HTMLEditorEventListener::MouseClick(nsIDOMMouseEvent* aMouseEvent) +{ + nsCOMPtr<nsIDOMEventTarget> target; + nsresult rv = aMouseEvent->AsEvent()->GetTarget(getter_AddRefs(target)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(target, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(target); + + GetHTMLEditor()->DoInlineTableEditingAction(element); + + return EditorEventListener::MouseClick(aMouseEvent); +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLEditorEventListener.h b/editor/libeditor/HTMLEditorEventListener.h new file mode 100644 index 000000000..b97b675b5 --- /dev/null +++ b/editor/libeditor/HTMLEditorEventListener.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; 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/. */ + +#ifndef HTMLEditorEventListener_h +#define HTMLEditorEventListener_h + +#include "EditorEventListener.h" +#include "nscore.h" + +namespace mozilla { + +class EditorBase; +class HTMLEditor; + +class HTMLEditorEventListener final : public EditorEventListener +{ +public: + HTMLEditorEventListener() + { + } + + virtual ~HTMLEditorEventListener() + { + } + +#ifdef DEBUG + // WARNING: You must be use HTMLEditor or its sub class for this class. + virtual nsresult Connect(EditorBase* aEditorBase) override; +#endif + +protected: + virtual nsresult MouseDown(nsIDOMMouseEvent* aMouseEvent) override; + virtual nsresult MouseUp(nsIDOMMouseEvent* aMouseEvent) override; + virtual nsresult MouseClick(nsIDOMMouseEvent* aMouseEvent) override; + + inline HTMLEditor* GetHTMLEditor(); +}; + +} // namespace mozilla + +#endif // #ifndef HTMLEditorEventListener_h diff --git a/editor/libeditor/HTMLEditorObjectResizer.cpp b/editor/libeditor/HTMLEditorObjectResizer.cpp new file mode 100644 index 000000000..111a3f975 --- /dev/null +++ b/editor/libeditor/HTMLEditorObjectResizer.cpp @@ -0,0 +1,1059 @@ +/* -*- 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/HTMLEditor.h" +#include "HTMLEditorObjectResizerUtils.h" + +#include "HTMLEditUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Preferences.h" +#include "mozilla/mozalloc.h" +#include "nsAString.h" +#include "nsAlgorithm.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsID.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIDOMEvent.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMMouseEvent.h" +#include "nsIDOMNode.h" +#include "nsIDOMText.h" +#include "nsIDocument.h" +#include "nsIEditor.h" +#include "nsIHTMLObjectResizeListener.h" +#include "nsIHTMLObjectResizer.h" +#include "nsIPresShell.h" +#include "nsISupportsUtils.h" +#include "nsPIDOMWindow.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsSubstringTuple.h" +#include "nscore.h" +#include <algorithm> + +class nsISelection; + +namespace mozilla { + +using namespace dom; + +/****************************************************************************** + * mozilla::DocumentResizeEventListener + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(DocumentResizeEventListener, nsIDOMEventListener) + +DocumentResizeEventListener::DocumentResizeEventListener(nsIHTMLEditor* aEditor) +{ + mEditor = do_GetWeakReference(aEditor); +} + +NS_IMETHODIMP +DocumentResizeEventListener::HandleEvent(nsIDOMEvent* aMouseEvent) +{ + nsCOMPtr<nsIHTMLObjectResizer> objectResizer = do_QueryReferent(mEditor); + if (objectResizer) { + return objectResizer->RefreshResizers(); + } + return NS_OK; +} + +/****************************************************************************** + * mozilla::ResizerSelectionListener + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(ResizerSelectionListener, nsISelectionListener) + +ResizerSelectionListener::ResizerSelectionListener(nsIHTMLEditor* aEditor) +{ + mEditor = do_GetWeakReference(aEditor); +} + +NS_IMETHODIMP +ResizerSelectionListener::NotifySelectionChanged(nsIDOMDocument* aDOMDocument, + nsISelection* aSelection, + int16_t aReason) +{ + 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 + nsCOMPtr<nsIHTMLEditor> editor = do_QueryReferent(mEditor); + if (editor) { + editor->CheckSelectionStateForAnonymousButtons(aSelection); + } + } + + return NS_OK; +} + +/****************************************************************************** + * mozilla::ResizerMouseMotionListener + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(ResizerMouseMotionListener, nsIDOMEventListener) + +ResizerMouseMotionListener::ResizerMouseMotionListener(nsIHTMLEditor* aEditor) +{ + mEditor = do_GetWeakReference(aEditor); +} + +NS_IMETHODIMP +ResizerMouseMotionListener::HandleEvent(nsIDOMEvent* aMouseEvent) +{ + nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) ); + if (!mouseEvent) { + //non-ui event passed in. bad things. + return NS_OK; + } + + // Don't do anything special if not an HTML object resizer editor + nsCOMPtr<nsIHTMLObjectResizer> objectResizer = do_QueryReferent(mEditor); + if (objectResizer) { + // check if we have to redisplay a resizing shadow + objectResizer->MouseMove(aMouseEvent); + } + + return NS_OK; +} + +/****************************************************************************** + * mozilla::HTMLEditor + ******************************************************************************/ + +already_AddRefed<Element> +HTMLEditor::CreateResizer(int16_t aLocation, + nsIDOMNode* aParentNode) +{ + nsCOMPtr<nsIDOMElement> retDOM; + nsresult rv = CreateAnonymousElement(NS_LITERAL_STRING("span"), + aParentNode, + NS_LITERAL_STRING("mozResizer"), + false, + getter_AddRefs(retDOM)); + + NS_ENSURE_SUCCESS(rv, nullptr); + NS_ENSURE_TRUE(retDOM, nullptr); + + // add the mouse listener so we can detect a click on a resizer + nsCOMPtr<nsIDOMEventTarget> evtTarget = do_QueryInterface(retDOM); + evtTarget->AddEventListener(NS_LITERAL_STRING("mousedown"), mEventListener, + true); + + nsAutoString locationStr; + switch (aLocation) { + case nsIHTMLObjectResizer::eTopLeft: + locationStr = kTopLeft; + break; + case nsIHTMLObjectResizer::eTop: + locationStr = kTop; + break; + case nsIHTMLObjectResizer::eTopRight: + locationStr = kTopRight; + break; + + case nsIHTMLObjectResizer::eLeft: + locationStr = kLeft; + break; + case nsIHTMLObjectResizer::eRight: + locationStr = kRight; + break; + + case nsIHTMLObjectResizer::eBottomLeft: + locationStr = kBottomLeft; + break; + case nsIHTMLObjectResizer::eBottom: + locationStr = kBottom; + break; + case nsIHTMLObjectResizer::eBottomRight: + locationStr = kBottomRight; + break; + } + + nsCOMPtr<Element> ret = do_QueryInterface(retDOM); + rv = ret->SetAttr(kNameSpaceID_None, nsGkAtoms::anonlocation, locationStr, + true); + NS_ENSURE_SUCCESS(rv, nullptr); + return ret.forget(); +} + +already_AddRefed<Element> +HTMLEditor::CreateShadow(nsIDOMNode* aParentNode, + nsIDOMElement* aOriginalObject) +{ + // let's create an image through the element factory + nsAutoString name; + if (HTMLEditUtils::IsImage(aOriginalObject)) { + name.AssignLiteral("img"); + } else { + name.AssignLiteral("span"); + } + nsCOMPtr<nsIDOMElement> retDOM; + CreateAnonymousElement(name, aParentNode, + NS_LITERAL_STRING("mozResizingShadow"), true, + getter_AddRefs(retDOM)); + + NS_ENSURE_TRUE(retDOM, nullptr); + + nsCOMPtr<Element> ret = do_QueryInterface(retDOM); + return ret.forget(); +} + +already_AddRefed<Element> +HTMLEditor::CreateResizingInfo(nsIDOMNode* aParentNode) +{ + // let's create an info box through the element factory + nsCOMPtr<nsIDOMElement> retDOM; + CreateAnonymousElement(NS_LITERAL_STRING("span"), aParentNode, + NS_LITERAL_STRING("mozResizingInfo"), true, + getter_AddRefs(retDOM)); + + nsCOMPtr<Element> ret = do_QueryInterface(retDOM); + return ret.forget(); +} + +nsresult +HTMLEditor::SetAllResizersPosition() +{ + NS_ENSURE_TRUE(mTopLeftHandle, NS_ERROR_FAILURE); + + int32_t x = mResizedObjectX; + int32_t y = mResizedObjectY; + int32_t w = mResizedObjectWidth; + int32_t h = mResizedObjectHeight; + + // now let's place all the resizers around the image + + // get the size of resizers + nsAutoString value; + float resizerWidth, resizerHeight; + nsCOMPtr<nsIAtom> dummyUnit; + mCSSEditUtils->GetComputedProperty(*mTopLeftHandle, *nsGkAtoms::width, + value); + mCSSEditUtils->ParseLength(value, &resizerWidth, getter_AddRefs(dummyUnit)); + mCSSEditUtils->GetComputedProperty(*mTopLeftHandle, *nsGkAtoms::height, + value); + mCSSEditUtils->ParseLength(value, &resizerHeight, getter_AddRefs(dummyUnit)); + + int32_t rw = (int32_t)((resizerWidth + 1) / 2); + int32_t rh = (int32_t)((resizerHeight+ 1) / 2); + + SetAnonymousElementPosition(x-rw, y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopLeftHandle))); + SetAnonymousElementPosition(x+w/2-rw, y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopHandle))); + SetAnonymousElementPosition(x+w-rw-1, y-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mTopRightHandle))); + + SetAnonymousElementPosition(x-rw, y+h/2-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mLeftHandle))); + SetAnonymousElementPosition(x+w-rw-1, y+h/2-rh, static_cast<nsIDOMElement*>(GetAsDOMNode(mRightHandle))); + + SetAnonymousElementPosition(x-rw, y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomLeftHandle))); + SetAnonymousElementPosition(x+w/2-rw, y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomHandle))); + SetAnonymousElementPosition(x+w-rw-1, y+h-rh-1, static_cast<nsIDOMElement*>(GetAsDOMNode(mBottomRightHandle))); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RefreshResizers() +{ + // nothing to do if resizers are not displayed... + NS_ENSURE_TRUE(mResizedObject, NS_OK); + + nsresult rv = + GetPositionAndDimensions( + static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), + mResizedObjectX, + mResizedObjectY, + mResizedObjectWidth, + mResizedObjectHeight, + mResizedObjectBorderLeft, + mResizedObjectBorderTop, + mResizedObjectMarginLeft, + mResizedObjectMarginTop); + + NS_ENSURE_SUCCESS(rv, rv); + rv = SetAllResizersPosition(); + NS_ENSURE_SUCCESS(rv, rv); + return SetShadowPosition(mResizingShadow, mResizedObject, + mResizedObjectX, mResizedObjectY); +} + +NS_IMETHODIMP +HTMLEditor::ShowResizers(nsIDOMElement* aResizedElement) +{ + nsresult rv = ShowResizersInner(aResizedElement); + if (NS_FAILED(rv)) { + HideResizers(); + } + return rv; +} + +nsresult +HTMLEditor::ShowResizersInner(nsIDOMElement* aResizedElement) +{ + NS_ENSURE_ARG_POINTER(aResizedElement); + + nsCOMPtr<nsIDOMNode> parentNode; + nsresult rv = aResizedElement->GetParentNode(getter_AddRefs(parentNode)); + NS_ENSURE_SUCCESS(rv, rv); + + if (mResizedObject) { + NS_ERROR("call HideResizers first"); + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsINode> resizedNode = do_QueryInterface(aResizedElement); + if (NS_WARN_IF(!IsDescendantOfEditorRoot(resizedNode))) { + return NS_ERROR_UNEXPECTED; + } + + mResizedObject = do_QueryInterface(aResizedElement); + NS_ENSURE_STATE(mResizedObject); + + // The resizers and the shadow will be anonymous siblings of the element. + mTopLeftHandle = CreateResizer(nsIHTMLObjectResizer::eTopLeft, parentNode); + NS_ENSURE_TRUE(mTopLeftHandle, NS_ERROR_FAILURE); + mTopHandle = CreateResizer(nsIHTMLObjectResizer::eTop, parentNode); + NS_ENSURE_TRUE(mTopHandle, NS_ERROR_FAILURE); + mTopRightHandle = CreateResizer(nsIHTMLObjectResizer::eTopRight, parentNode); + NS_ENSURE_TRUE(mTopRightHandle, NS_ERROR_FAILURE); + + mLeftHandle = CreateResizer(nsIHTMLObjectResizer::eLeft, parentNode); + NS_ENSURE_TRUE(mLeftHandle, NS_ERROR_FAILURE); + mRightHandle = CreateResizer(nsIHTMLObjectResizer::eRight, parentNode); + NS_ENSURE_TRUE(mRightHandle, NS_ERROR_FAILURE); + + mBottomLeftHandle = CreateResizer(nsIHTMLObjectResizer::eBottomLeft, parentNode); + NS_ENSURE_TRUE(mBottomLeftHandle, NS_ERROR_FAILURE); + mBottomHandle = CreateResizer(nsIHTMLObjectResizer::eBottom, parentNode); + NS_ENSURE_TRUE(mBottomHandle, NS_ERROR_FAILURE); + mBottomRightHandle = CreateResizer(nsIHTMLObjectResizer::eBottomRight, parentNode); + NS_ENSURE_TRUE(mBottomRightHandle, NS_ERROR_FAILURE); + + rv = GetPositionAndDimensions(aResizedElement, + mResizedObjectX, + mResizedObjectY, + mResizedObjectWidth, + mResizedObjectHeight, + mResizedObjectBorderLeft, + mResizedObjectBorderTop, + mResizedObjectMarginLeft, + mResizedObjectMarginTop); + NS_ENSURE_SUCCESS(rv, rv); + + // and let's set their absolute positions in the document + rv = SetAllResizersPosition(); + NS_ENSURE_SUCCESS(rv, rv); + + // now, let's create the resizing shadow + mResizingShadow = CreateShadow(parentNode, aResizedElement); + NS_ENSURE_TRUE(mResizingShadow, NS_ERROR_FAILURE); + // and set its position + rv = SetShadowPosition(mResizingShadow, mResizedObject, + mResizedObjectX, mResizedObjectY); + NS_ENSURE_SUCCESS(rv, rv); + + // and then the resizing info tooltip + mResizingInfo = CreateResizingInfo(parentNode); + NS_ENSURE_TRUE(mResizingInfo, NS_ERROR_FAILURE); + + // and listen to the "resize" event on the window first, get the + // window from the document... + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(doc->GetWindow()); + if (!target) { + return NS_ERROR_NULL_POINTER; + } + + mResizeEventListenerP = new DocumentResizeEventListener(this); + if (!mResizeEventListenerP) { + return NS_ERROR_OUT_OF_MEMORY; + } + rv = target->AddEventListener(NS_LITERAL_STRING("resize"), + mResizeEventListenerP, false); + // XXX Even when it failed to add event listener, should we need to set + // _moz_resizing attribute? + aResizedElement->SetAttribute(NS_LITERAL_STRING("_moz_resizing"), NS_LITERAL_STRING("true")); + return rv; +} + +NS_IMETHODIMP +HTMLEditor::HideResizers() +{ + NS_ENSURE_TRUE(mResizedObject, NS_OK); + + // get the presshell's document observer interface. + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + // We allow the pres shell to be null; when it is, we presume there + // are no document observers to notify, but we still want to + // UnbindFromTree. + + nsCOMPtr<nsIContent> parentContent; + + if (mTopLeftHandle) { + parentContent = mTopLeftHandle->GetParent(); + } + + NS_NAMED_LITERAL_STRING(mousedown, "mousedown"); + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mTopLeftHandle, parentContent, ps); + mTopLeftHandle = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mTopHandle, parentContent, ps); + mTopHandle = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mTopRightHandle, parentContent, ps); + mTopRightHandle = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mLeftHandle, parentContent, ps); + mLeftHandle = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mRightHandle, parentContent, ps); + mRightHandle = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mBottomLeftHandle, parentContent, ps); + mBottomLeftHandle = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mBottomHandle, parentContent, ps); + mBottomHandle = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mBottomRightHandle, parentContent, ps); + mBottomRightHandle = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mResizingShadow, parentContent, ps); + mResizingShadow = nullptr; + + RemoveListenerAndDeleteRef(mousedown, mEventListener, true, + mResizingInfo, parentContent, ps); + mResizingInfo = nullptr; + + if (mActivatedHandle) { + mActivatedHandle->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated, + true); + mActivatedHandle = nullptr; + } + + // don't forget to remove the listeners ! + + nsCOMPtr<nsIDOMEventTarget> target = GetDOMEventTarget(); + + if (target && mMouseMotionListenerP) { + DebugOnly<nsresult> rv = + target->RemoveEventListener(NS_LITERAL_STRING("mousemove"), + mMouseMotionListenerP, true); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to remove mouse motion listener"); + } + mMouseMotionListenerP = nullptr; + + nsCOMPtr<nsIDocument> doc = GetDocument(); + if (!doc) { + return NS_ERROR_NULL_POINTER; + } + target = do_QueryInterface(doc->GetWindow()); + if (!target) { + return NS_ERROR_NULL_POINTER; + } + + if (mResizeEventListenerP) { + DebugOnly<nsresult> rv = + target->RemoveEventListener(NS_LITERAL_STRING("resize"), + mResizeEventListenerP, false); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to remove resize event listener"); + } + mResizeEventListenerP = nullptr; + + mResizedObject->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_resizing, true); + mResizedObject = nullptr; + + return NS_OK; +} + +void +HTMLEditor::HideShadowAndInfo() +{ + if (mResizingShadow) { + mResizingShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::_class, + NS_LITERAL_STRING("hidden"), true); + } + if (mResizingInfo) { + mResizingInfo->SetAttr(kNameSpaceID_None, nsGkAtoms::_class, + NS_LITERAL_STRING("hidden"), true); + } +} + +nsresult +HTMLEditor::StartResizing(nsIDOMElement* aHandle) +{ + // First notify the listeners if any + for (auto& listener : mObjectResizeEventListeners) { + listener->OnStartResizing(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject))); + } + + mIsResizing = true; + mActivatedHandle = do_QueryInterface(aHandle); + NS_ENSURE_STATE(mActivatedHandle || !aHandle); + mActivatedHandle->SetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated, + NS_LITERAL_STRING("true"), true); + + // do we want to preserve ratio or not? + bool preserveRatio = HTMLEditUtils::IsImage(mResizedObject) && + Preferences::GetBool("editor.resizing.preserve_ratio", true); + + // the way we change the position/size of the shadow depends on + // the handle + nsAutoString locationStr; + aHandle->GetAttribute(NS_LITERAL_STRING("anonlocation"), locationStr); + if (locationStr.Equals(kTopLeft)) { + SetResizeIncrements(1, 1, -1, -1, preserveRatio); + } else if (locationStr.Equals(kTop)) { + SetResizeIncrements(0, 1, 0, -1, false); + } else if (locationStr.Equals(kTopRight)) { + SetResizeIncrements(0, 1, 1, -1, preserveRatio); + } else if (locationStr.Equals(kLeft)) { + SetResizeIncrements(1, 0, -1, 0, false); + } else if (locationStr.Equals(kRight)) { + SetResizeIncrements(0, 0, 1, 0, false); + } else if (locationStr.Equals(kBottomLeft)) { + SetResizeIncrements(1, 0, -1, 1, preserveRatio); + } else if (locationStr.Equals(kBottom)) { + SetResizeIncrements(0, 0, 0, 1, false); + } else if (locationStr.Equals(kBottomRight)) { + SetResizeIncrements(0, 0, 1, 1, preserveRatio); + } + + // make the shadow appear + mResizingShadow->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true); + + // position it + mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::width, + mResizedObjectWidth); + mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::height, + mResizedObjectHeight); + + // add a mouse move listener to the editor + nsresult result = NS_OK; + if (!mMouseMotionListenerP) { + mMouseMotionListenerP = new ResizerMouseMotionListener(this); + if (!mMouseMotionListenerP) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsCOMPtr<nsIDOMEventTarget> target = GetDOMEventTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_FAILURE); + + result = target->AddEventListener(NS_LITERAL_STRING("mousemove"), + mMouseMotionListenerP, true); + NS_ASSERTION(NS_SUCCEEDED(result), + "failed to register mouse motion listener"); + } + return result; +} + +NS_IMETHODIMP +HTMLEditor::MouseDown(int32_t aClientX, + int32_t aClientY, + nsIDOMElement* aTarget, + nsIDOMEvent* aEvent) +{ + bool anonElement = false; + if (aTarget && NS_SUCCEEDED(aTarget->HasAttribute(NS_LITERAL_STRING("_moz_anonclass"), &anonElement))) + // we caught a click on an anonymous element + if (anonElement) { + nsAutoString anonclass; + nsresult rv = + aTarget->GetAttribute(NS_LITERAL_STRING("_moz_anonclass"), anonclass); + NS_ENSURE_SUCCESS(rv, rv); + if (anonclass.EqualsLiteral("mozResizer")) { + // and that element is a resizer, let's start resizing! + aEvent->PreventDefault(); + + mOriginalX = aClientX; + mOriginalY = aClientY; + return StartResizing(aTarget); + } + if (anonclass.EqualsLiteral("mozGrabber")) { + // and that element is a grabber, let's start moving the element! + mOriginalX = aClientX; + mOriginalY = aClientY; + return GrabberClicked(); + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::MouseUp(int32_t aClientX, + int32_t aClientY, + nsIDOMElement* aTarget) +{ + if (mIsResizing) { + // we are resizing and release the mouse button, so let's + // end the resizing process + mIsResizing = false; + HideShadowAndInfo(); + SetFinalSize(aClientX, aClientY); + } else if (mIsMoving || mGrabberClicked) { + if (mIsMoving) { + mPositioningShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::_class, + NS_LITERAL_STRING("hidden"), true); + SetFinalPosition(aClientX, aClientY); + } + if (mGrabberClicked) { + EndMoving(); + } + } + return NS_OK; +} + +void +HTMLEditor::SetResizeIncrements(int32_t aX, + int32_t aY, + int32_t aW, + int32_t aH, + bool aPreserveRatio) +{ + mXIncrementFactor = aX; + mYIncrementFactor = aY; + mWidthIncrementFactor = aW; + mHeightIncrementFactor = aH; + mPreserveRatio = aPreserveRatio; +} + +nsresult +HTMLEditor::SetResizingInfoPosition(int32_t aX, + int32_t aY, + int32_t aW, + int32_t aH) +{ + nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument(); + + // Determine the position of the resizing info box based upon the new + // position and size of the element (aX, aY, aW, aH), and which + // resizer is the "activated handle". For example, place the resizing + // info box at the bottom-right corner of the new element, if the element + // is being resized by the bottom-right resizer. + int32_t infoXPosition; + int32_t infoYPosition; + + if (mActivatedHandle == mTopLeftHandle || + mActivatedHandle == mLeftHandle || + mActivatedHandle == mBottomLeftHandle) { + infoXPosition = aX; + } else if (mActivatedHandle == mTopHandle || + mActivatedHandle == mBottomHandle) { + infoXPosition = aX + (aW / 2); + } else { + // should only occur when mActivatedHandle is one of the 3 right-side + // handles, but this is a reasonable default if it isn't any of them (?) + infoXPosition = aX + aW; + } + + if (mActivatedHandle == mTopLeftHandle || + mActivatedHandle == mTopHandle || + mActivatedHandle == mTopRightHandle) { + infoYPosition = aY; + } else if (mActivatedHandle == mLeftHandle || + mActivatedHandle == mRightHandle) { + infoYPosition = aY + (aH / 2); + } else { + // should only occur when mActivatedHandle is one of the 3 bottom-side + // handles, but this is a reasonable default if it isn't any of them (?) + infoYPosition = aY + aH; + } + + // Offset info box by 20 so it's not directly under the mouse cursor. + const int mouseCursorOffset = 20; + mCSSEditUtils->SetCSSPropertyPixels(*mResizingInfo, *nsGkAtoms::left, + infoXPosition + mouseCursorOffset); + mCSSEditUtils->SetCSSPropertyPixels(*mResizingInfo, *nsGkAtoms::top, + infoYPosition + mouseCursorOffset); + + nsCOMPtr<nsIContent> textInfo = mResizingInfo->GetFirstChild(); + ErrorResult erv; + if (textInfo) { + mResizingInfo->RemoveChild(*textInfo, erv); + NS_ENSURE_TRUE(!erv.Failed(), erv.StealNSResult()); + textInfo = nullptr; + } + + nsAutoString widthStr, heightStr, diffWidthStr, diffHeightStr; + widthStr.AppendInt(aW); + heightStr.AppendInt(aH); + int32_t diffWidth = aW - mResizedObjectWidth; + int32_t diffHeight = aH - mResizedObjectHeight; + if (diffWidth > 0) { + diffWidthStr.Assign('+'); + } + if (diffHeight > 0) { + diffHeightStr.Assign('+'); + } + diffWidthStr.AppendInt(diffWidth); + diffHeightStr.AppendInt(diffHeight); + + nsAutoString info(widthStr + NS_LITERAL_STRING(" x ") + heightStr + + NS_LITERAL_STRING(" (") + diffWidthStr + + NS_LITERAL_STRING(", ") + diffHeightStr + + NS_LITERAL_STRING(")")); + + nsCOMPtr<nsIDOMText> nodeAsText; + nsresult rv = domdoc->CreateTextNode(info, getter_AddRefs(nodeAsText)); + NS_ENSURE_SUCCESS(rv, rv); + textInfo = do_QueryInterface(nodeAsText); + mResizingInfo->AppendChild(*textInfo, erv); + NS_ENSURE_TRUE(!erv.Failed(), erv.StealNSResult()); + + return mResizingInfo->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true); +} + +nsresult +HTMLEditor::SetShadowPosition(Element* aShadow, + Element* aOriginalObject, + int32_t aOriginalObjectX, + int32_t aOriginalObjectY) +{ + SetAnonymousElementPosition(aOriginalObjectX, aOriginalObjectY, static_cast<nsIDOMElement*>(GetAsDOMNode(aShadow))); + + if (HTMLEditUtils::IsImage(aOriginalObject)) { + nsAutoString imageSource; + aOriginalObject->GetAttr(kNameSpaceID_None, nsGkAtoms::src, imageSource); + nsresult rv = aShadow->SetAttr(kNameSpaceID_None, nsGkAtoms::src, + imageSource, true); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +int32_t +HTMLEditor::GetNewResizingIncrement(int32_t aX, + int32_t aY, + int32_t aID) +{ + int32_t result = 0; + if (!mPreserveRatio) { + switch (aID) { + case kX: + case kWidth: + result = aX - mOriginalX; + break; + case kY: + case kHeight: + result = aY - mOriginalY; + break; + } + return result; + } + + int32_t xi = (aX - mOriginalX) * mWidthIncrementFactor; + int32_t yi = (aY - mOriginalY) * mHeightIncrementFactor; + float objectSizeRatio = + ((float)mResizedObjectWidth) / ((float)mResizedObjectHeight); + result = (xi > yi) ? xi : yi; + switch (aID) { + case kX: + case kWidth: + if (result == yi) + result = (int32_t) (((float) result) * objectSizeRatio); + result = (int32_t) (((float) result) * mWidthIncrementFactor); + break; + case kY: + case kHeight: + if (result == xi) + result = (int32_t) (((float) result) / objectSizeRatio); + result = (int32_t) (((float) result) * mHeightIncrementFactor); + break; + } + return result; +} + +int32_t +HTMLEditor::GetNewResizingX(int32_t aX, + int32_t aY) +{ + int32_t resized = mResizedObjectX + + GetNewResizingIncrement(aX, aY, kX) * mXIncrementFactor; + int32_t max = mResizedObjectX + mResizedObjectWidth; + return std::min(resized, max); +} + +int32_t +HTMLEditor::GetNewResizingY(int32_t aX, + int32_t aY) +{ + int32_t resized = mResizedObjectY + + GetNewResizingIncrement(aX, aY, kY) * mYIncrementFactor; + int32_t max = mResizedObjectY + mResizedObjectHeight; + return std::min(resized, max); +} + +int32_t +HTMLEditor::GetNewResizingWidth(int32_t aX, + int32_t aY) +{ + int32_t resized = mResizedObjectWidth + + GetNewResizingIncrement(aX, aY, kWidth) * + mWidthIncrementFactor; + return std::max(resized, 1); +} + +int32_t +HTMLEditor::GetNewResizingHeight(int32_t aX, + int32_t aY) +{ + int32_t resized = mResizedObjectHeight + + GetNewResizingIncrement(aX, aY, kHeight) * + mHeightIncrementFactor; + return std::max(resized, 1); +} + +NS_IMETHODIMP +HTMLEditor::MouseMove(nsIDOMEvent* aMouseEvent) +{ + NS_NAMED_LITERAL_STRING(leftStr, "left"); + NS_NAMED_LITERAL_STRING(topStr, "top"); + + if (mIsResizing) { + // we are resizing and the mouse pointer's position has changed + // we have to resdisplay the shadow + nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) ); + int32_t clientX, clientY; + mouseEvent->GetClientX(&clientX); + mouseEvent->GetClientY(&clientY); + + int32_t newX = GetNewResizingX(clientX, clientY); + int32_t newY = GetNewResizingY(clientX, clientY); + int32_t newWidth = GetNewResizingWidth(clientX, clientY); + int32_t newHeight = GetNewResizingHeight(clientX, clientY); + + mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::left, + newX); + mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::top, + newY); + mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::width, + newWidth); + mCSSEditUtils->SetCSSPropertyPixels(*mResizingShadow, *nsGkAtoms::height, + newHeight); + + return SetResizingInfoPosition(newX, newY, newWidth, newHeight); + } + + if (mGrabberClicked) { + nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) ); + int32_t clientX, clientY; + mouseEvent->GetClientX(&clientX); + mouseEvent->GetClientY(&clientY); + + int32_t xThreshold = + LookAndFeel::GetInt(LookAndFeel::eIntID_DragThresholdX, 1); + int32_t yThreshold = + LookAndFeel::GetInt(LookAndFeel::eIntID_DragThresholdY, 1); + + if (DeprecatedAbs(clientX - mOriginalX) * 2 >= xThreshold || + DeprecatedAbs(clientY - mOriginalY) * 2 >= yThreshold) { + mGrabberClicked = false; + StartMoving(nullptr); + } + } + if (mIsMoving) { + nsCOMPtr<nsIDOMMouseEvent> mouseEvent ( do_QueryInterface(aMouseEvent) ); + int32_t clientX, clientY; + mouseEvent->GetClientX(&clientX); + mouseEvent->GetClientY(&clientY); + + int32_t newX = mPositionedObjectX + clientX - mOriginalX; + int32_t newY = mPositionedObjectY + clientY - mOriginalY; + + SnapToGrid(newX, newY); + + mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::left, + newX); + mCSSEditUtils->SetCSSPropertyPixels(*mPositioningShadow, *nsGkAtoms::top, + newY); + } + return NS_OK; +} + +void +HTMLEditor::SetFinalSize(int32_t aX, + int32_t aY) +{ + if (!mResizedObject) { + // paranoia + return; + } + + if (mActivatedHandle) { + mActivatedHandle->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_moz_activated, true); + mActivatedHandle = nullptr; + } + + // we have now to set the new width and height of the resized object + // we don't set the x and y position because we don't control that in + // a normal HTML layout + int32_t left = GetNewResizingX(aX, aY); + int32_t top = GetNewResizingY(aX, aY); + int32_t width = GetNewResizingWidth(aX, aY); + int32_t height = GetNewResizingHeight(aX, aY); + bool setWidth = !mResizedObjectIsAbsolutelyPositioned || (width != mResizedObjectWidth); + bool setHeight = !mResizedObjectIsAbsolutelyPositioned || (height != mResizedObjectHeight); + + int32_t x, y; + x = left - ((mResizedObjectIsAbsolutelyPositioned) ? mResizedObjectBorderLeft+mResizedObjectMarginLeft : 0); + y = top - ((mResizedObjectIsAbsolutelyPositioned) ? mResizedObjectBorderTop+mResizedObjectMarginTop : 0); + + // we want one transaction only from a user's point of view + AutoEditBatch batchIt(this); + + NS_NAMED_LITERAL_STRING(widthStr, "width"); + NS_NAMED_LITERAL_STRING(heightStr, "height"); + + nsCOMPtr<Element> resizedObject = do_QueryInterface(mResizedObject); + NS_ENSURE_TRUE(resizedObject, ); + if (mResizedObjectIsAbsolutelyPositioned) { + if (setHeight) { + mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::top, y); + } + if (setWidth) { + mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::left, x); + } + } + if (IsCSSEnabled() || mResizedObjectIsAbsolutelyPositioned) { + if (setWidth && mResizedObject->HasAttr(kNameSpaceID_None, nsGkAtoms::width)) { + RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), widthStr); + } + + if (setHeight && mResizedObject->HasAttr(kNameSpaceID_None, + nsGkAtoms::height)) { + RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), heightStr); + } + + if (setWidth) { + mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::width, + width); + } + if (setHeight) { + mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::height, + height); + } + } else { + // we use HTML size and remove all equivalent CSS properties + + // we set the CSS width and height to remove it later, + // triggering an immediate reflow; otherwise, we have problems + // with asynchronous reflow + if (setWidth) { + mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::width, + width); + } + if (setHeight) { + mCSSEditUtils->SetCSSPropertyPixels(*resizedObject, *nsGkAtoms::height, + height); + } + if (setWidth) { + nsAutoString w; + w.AppendInt(width); + SetAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), widthStr, w); + } + if (setHeight) { + nsAutoString h; + h.AppendInt(height); + SetAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), heightStr, h); + } + + if (setWidth) { + mCSSEditUtils->RemoveCSSProperty(*resizedObject, *nsGkAtoms::width, + EmptyString()); + } + if (setHeight) { + mCSSEditUtils->RemoveCSSProperty(*resizedObject, *nsGkAtoms::height, + EmptyString()); + } + } + // finally notify the listeners if any + for (auto& listener : mObjectResizeEventListeners) { + listener->OnEndResizing(static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)), + mResizedObjectWidth, mResizedObjectHeight, width, + height); + } + + // keep track of that size + mResizedObjectWidth = width; + mResizedObjectHeight = height; + + RefreshResizers(); +} + +NS_IMETHODIMP +HTMLEditor::GetResizedObject(nsIDOMElement** aResizedObject) +{ + nsCOMPtr<nsIDOMElement> ret = static_cast<nsIDOMElement*>(GetAsDOMNode(mResizedObject)); + ret.forget(aResizedObject); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetObjectResizingEnabled(bool* aIsObjectResizingEnabled) +{ + *aIsObjectResizingEnabled = mIsObjectResizingEnabled; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::SetObjectResizingEnabled(bool aObjectResizingEnabled) +{ + mIsObjectResizingEnabled = aObjectResizingEnabled; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::AddObjectResizeEventListener(nsIHTMLObjectResizeListener* aListener) +{ + NS_ENSURE_ARG_POINTER(aListener); + if (mObjectResizeEventListeners.Contains(aListener)) { + /* listener already registered */ + NS_ASSERTION(false, + "trying to register an already registered object resize event listener"); + return NS_OK; + } + mObjectResizeEventListeners.AppendElement(*aListener); + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RemoveObjectResizeEventListener( + nsIHTMLObjectResizeListener* aListener) +{ + NS_ENSURE_ARG_POINTER(aListener); + if (!mObjectResizeEventListeners.Contains(aListener)) { + /* listener was not registered */ + NS_ASSERTION(false, + "trying to remove an object resize event listener that was not already registered"); + return NS_OK; + } + mObjectResizeEventListeners.RemoveElement(aListener); + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLEditorObjectResizerUtils.h b/editor/libeditor/HTMLEditorObjectResizerUtils.h new file mode 100644 index 000000000..2bebbe76b --- /dev/null +++ b/editor/libeditor/HTMLEditorObjectResizerUtils.h @@ -0,0 +1,82 @@ +/* -*- 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/. */ + +#ifndef HTMLEditorObjectResizerUtils_h +#define HTMLEditorObjectResizerUtils_h + +#include "nsIDOMEventListener.h" +#include "nsISelectionListener.h" +#include "nsISupportsImpl.h" +#include "nsIWeakReferenceUtils.h" +#include "nsLiteralString.h" + +class nsIHTMLEditor; + +namespace mozilla { + +#define kTopLeft NS_LITERAL_STRING("nw") +#define kTop NS_LITERAL_STRING("n") +#define kTopRight NS_LITERAL_STRING("ne") +#define kLeft NS_LITERAL_STRING("w") +#define kRight NS_LITERAL_STRING("e") +#define kBottomLeft NS_LITERAL_STRING("sw") +#define kBottom NS_LITERAL_STRING("s") +#define kBottomRight NS_LITERAL_STRING("se") + +/****************************************************************************** + * mozilla::ResizerSelectionListener + ******************************************************************************/ + +class ResizerSelectionListener final : public nsISelectionListener +{ +public: + explicit ResizerSelectionListener(nsIHTMLEditor* aEditor); + void Reset(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISELECTIONLISTENER + +protected: + virtual ~ResizerSelectionListener() {} + nsWeakPtr mEditor; +}; + +/****************************************************************************** + * mozilla::ResizerMouseMotionListener + ******************************************************************************/ + +class ResizerMouseMotionListener final : public nsIDOMEventListener +{ +public: + explicit ResizerMouseMotionListener(nsIHTMLEditor* aEditor); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + +protected: + virtual ~ResizerMouseMotionListener() {} + nsWeakPtr mEditor; +}; + +/****************************************************************************** + * mozilla::DocumentResizeEventListener + ******************************************************************************/ + +class DocumentResizeEventListener final : public nsIDOMEventListener +{ +public: + explicit DocumentResizeEventListener(nsIHTMLEditor* aEditor); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + +protected: + virtual ~DocumentResizeEventListener() {} + nsWeakPtr mEditor; +}; + +} // namespace mozilla + +#endif // #ifndef HTMLEditorObjectResizerUtils_h diff --git a/editor/libeditor/HTMLInlineTableEditor.cpp b/editor/libeditor/HTMLInlineTableEditor.cpp new file mode 100644 index 000000000..3da1876ec --- /dev/null +++ b/editor/libeditor/HTMLInlineTableEditor.cpp @@ -0,0 +1,283 @@ +/* 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/HTMLEditor.h" + +#include "HTMLEditUtils.h" +#include "mozilla/dom/Element.h" +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIContent.h" +#include "nsIDOMElement.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMHTMLElement.h" +#include "nsIDOMNode.h" +#include "nsIHTMLObjectResizer.h" +#include "nsIPresShell.h" +#include "nsLiteralString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nscore.h" + +namespace mozilla { + +// Uncomment the following line if you want to disable +// table deletion when the only column/row is removed +// #define DISABLE_TABLE_DELETION 1 + +NS_IMETHODIMP +HTMLEditor::SetInlineTableEditingEnabled(bool aIsEnabled) +{ + mIsInlineTableEditingEnabled = aIsEnabled; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetInlineTableEditingEnabled(bool* aIsEnabled) +{ + *aIsEnabled = mIsInlineTableEditingEnabled; + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::ShowInlineTableEditingUI(nsIDOMElement* aCell) +{ + NS_ENSURE_ARG_POINTER(aCell); + + // do nothing if aCell is not a table cell... + nsCOMPtr<Element> cell = do_QueryInterface(aCell); + if (!cell || !HTMLEditUtils::IsTableCell(cell)) { + return NS_OK; + } + + if (NS_WARN_IF(!IsDescendantOfEditorRoot(cell))) { + return NS_ERROR_UNEXPECTED; + } + + if (mInlineEditedCell) { + NS_ERROR("call HideInlineTableEditingUI first"); + return NS_ERROR_UNEXPECTED; + } + + // the resizers and the shadow will be anonymous children of the body + nsCOMPtr<nsIDOMElement> bodyElement = do_QueryInterface(GetRoot()); + NS_ENSURE_TRUE(bodyElement, NS_ERROR_NULL_POINTER); + + CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement, + NS_LITERAL_STRING("mozTableAddColumnBefore"), + false, getter_AddRefs(mAddColumnBeforeButton)); + CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement, + NS_LITERAL_STRING("mozTableRemoveColumn"), + false, getter_AddRefs(mRemoveColumnButton)); + CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement, + NS_LITERAL_STRING("mozTableAddColumnAfter"), + false, getter_AddRefs(mAddColumnAfterButton)); + + CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement, + NS_LITERAL_STRING("mozTableAddRowBefore"), + false, getter_AddRefs(mAddRowBeforeButton)); + CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement, + NS_LITERAL_STRING("mozTableRemoveRow"), + false, getter_AddRefs(mRemoveRowButton)); + CreateAnonymousElement(NS_LITERAL_STRING("a"), bodyElement, + NS_LITERAL_STRING("mozTableAddRowAfter"), + false, getter_AddRefs(mAddRowAfterButton)); + + AddMouseClickListener(mAddColumnBeforeButton); + AddMouseClickListener(mRemoveColumnButton); + AddMouseClickListener(mAddColumnAfterButton); + AddMouseClickListener(mAddRowBeforeButton); + AddMouseClickListener(mRemoveRowButton); + AddMouseClickListener(mAddRowAfterButton); + + mInlineEditedCell = aCell; + return RefreshInlineTableEditingUI(); +} + +NS_IMETHODIMP +HTMLEditor::HideInlineTableEditingUI() +{ + mInlineEditedCell = nullptr; + + RemoveMouseClickListener(mAddColumnBeforeButton); + RemoveMouseClickListener(mRemoveColumnButton); + RemoveMouseClickListener(mAddColumnAfterButton); + RemoveMouseClickListener(mAddRowBeforeButton); + RemoveMouseClickListener(mRemoveRowButton); + RemoveMouseClickListener(mAddRowAfterButton); + + // get the presshell's document observer interface. + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + // We allow the pres shell to be null; when it is, we presume there + // are no document observers to notify, but we still want to + // UnbindFromTree. + + // get the root content node. + nsCOMPtr<nsIContent> bodyContent = GetRoot(); + + DeleteRefToAnonymousNode(mAddColumnBeforeButton, bodyContent, ps); + mAddColumnBeforeButton = nullptr; + DeleteRefToAnonymousNode(mRemoveColumnButton, bodyContent, ps); + mRemoveColumnButton = nullptr; + DeleteRefToAnonymousNode(mAddColumnAfterButton, bodyContent, ps); + mAddColumnAfterButton = nullptr; + DeleteRefToAnonymousNode(mAddRowBeforeButton, bodyContent, ps); + mAddRowBeforeButton = nullptr; + DeleteRefToAnonymousNode(mRemoveRowButton, bodyContent, ps); + mRemoveRowButton = nullptr; + DeleteRefToAnonymousNode(mAddRowAfterButton, bodyContent, ps); + mAddRowAfterButton = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::DoInlineTableEditingAction(nsIDOMElement* aElement) +{ + NS_ENSURE_ARG_POINTER(aElement); + bool anonElement = false; + if (aElement && + NS_SUCCEEDED(aElement->HasAttribute(NS_LITERAL_STRING("_moz_anonclass"), &anonElement)) && + anonElement) { + nsAutoString anonclass; + nsresult rv = + aElement->GetAttribute(NS_LITERAL_STRING("_moz_anonclass"), anonclass); + NS_ENSURE_SUCCESS(rv, rv); + + if (!StringBeginsWith(anonclass, NS_LITERAL_STRING("mozTable"))) + return NS_OK; + + nsCOMPtr<nsIDOMNode> tableNode = GetEnclosingTable(mInlineEditedCell); + nsCOMPtr<nsIDOMElement> tableElement = do_QueryInterface(tableNode); + int32_t rowCount, colCount; + rv = GetTableSize(tableElement, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + bool hideUI = false; + bool hideResizersWithInlineTableUI = (GetAsDOMNode(mResizedObject) == tableElement); + + if (anonclass.EqualsLiteral("mozTableAddColumnBefore")) + InsertTableColumn(1, false); + else if (anonclass.EqualsLiteral("mozTableAddColumnAfter")) + InsertTableColumn(1, true); + else if (anonclass.EqualsLiteral("mozTableAddRowBefore")) + InsertTableRow(1, false); + else if (anonclass.EqualsLiteral("mozTableAddRowAfter")) + InsertTableRow(1, true); + else if (anonclass.EqualsLiteral("mozTableRemoveColumn")) { + DeleteTableColumn(1); +#ifndef DISABLE_TABLE_DELETION + hideUI = (colCount == 1); +#endif + } + else if (anonclass.EqualsLiteral("mozTableRemoveRow")) { + DeleteTableRow(1); +#ifndef DISABLE_TABLE_DELETION + hideUI = (rowCount == 1); +#endif + } + else + return NS_OK; + + if (hideUI) { + HideInlineTableEditingUI(); + if (hideResizersWithInlineTableUI) + HideResizers(); + } + } + + return NS_OK; +} + +void +HTMLEditor::AddMouseClickListener(nsIDOMElement* aElement) +{ + nsCOMPtr<nsIDOMEventTarget> evtTarget(do_QueryInterface(aElement)); + if (evtTarget) { + evtTarget->AddEventListener(NS_LITERAL_STRING("click"), + mEventListener, true); + } +} + +void +HTMLEditor::RemoveMouseClickListener(nsIDOMElement* aElement) +{ + nsCOMPtr<nsIDOMEventTarget> evtTarget(do_QueryInterface(aElement)); + if (evtTarget) { + evtTarget->RemoveEventListener(NS_LITERAL_STRING("click"), + mEventListener, true); + } +} + +NS_IMETHODIMP +HTMLEditor::RefreshInlineTableEditingUI() +{ + nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(mInlineEditedCell); + if (!htmlElement) { + return NS_ERROR_NULL_POINTER; + } + + int32_t xCell, yCell, wCell, hCell; + GetElementOrigin(mInlineEditedCell, xCell, yCell); + + nsresult rv = htmlElement->GetOffsetWidth(&wCell); + NS_ENSURE_SUCCESS(rv, rv); + rv = htmlElement->GetOffsetHeight(&hCell); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t xHoriz = xCell + wCell/2; + int32_t yVert = yCell + hCell/2; + + nsCOMPtr<nsIDOMNode> tableNode = GetEnclosingTable(mInlineEditedCell); + nsCOMPtr<nsIDOMElement> tableElement = do_QueryInterface(tableNode); + int32_t rowCount, colCount; + rv = GetTableSize(tableElement, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + SetAnonymousElementPosition(xHoriz-10, yCell-7, mAddColumnBeforeButton); +#ifdef DISABLE_TABLE_DELETION + NS_NAMED_LITERAL_STRING(classStr, "class"); + + if (colCount== 1) { + mRemoveColumnButton->SetAttribute(classStr, + NS_LITERAL_STRING("hidden")); + } + else { + bool hasClass = false; + rv = mRemoveColumnButton->HasAttribute(classStr, &hasClass); + if (NS_SUCCEEDED(rv) && hasClass) { + mRemoveColumnButton->RemoveAttribute(classStr); + } +#endif + SetAnonymousElementPosition(xHoriz-4, yCell-7, mRemoveColumnButton); +#ifdef DISABLE_TABLE_DELETION + } +#endif + SetAnonymousElementPosition(xHoriz+6, yCell-7, mAddColumnAfterButton); + + SetAnonymousElementPosition(xCell-7, yVert-10, mAddRowBeforeButton); +#ifdef DISABLE_TABLE_DELETION + if (rowCount== 1) { + mRemoveRowButton->SetAttribute(classStr, + NS_LITERAL_STRING("hidden")); + } + else { + bool hasClass = false; + rv = mRemoveRowButton->HasAttribute(classStr, &hasClass); + if (NS_SUCCEEDED(rv) && hasClass) { + mRemoveRowButton->RemoveAttribute(classStr); + } +#endif + SetAnonymousElementPosition(xCell-7, yVert-4, mRemoveRowButton); +#ifdef DISABLE_TABLE_DELETION + } +#endif + SetAnonymousElementPosition(xCell-7, yVert+6, mAddRowAfterButton); + + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLStyleEditor.cpp b/editor/libeditor/HTMLStyleEditor.cpp new file mode 100644 index 000000000..bc7141ad3 --- /dev/null +++ b/editor/libeditor/HTMLStyleEditor.cpp @@ -0,0 +1,1752 @@ +/* -*- 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/HTMLEditor.h" + +#include "HTMLEditUtils.h" +#include "TextEditUtils.h" +#include "TypeInState.h" +#include "mozilla/Assertions.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/SelectionState.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Element.h" +#include "mozilla/mozalloc.h" +#include "nsAString.h" +#include "nsAttrName.h" +#include "nsCOMPtr.h" +#include "nsCaseTreatment.h" +#include "nsComponentManagerUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsIContentIterator.h" +#include "nsIDOMElement.h" +#include "nsIEditor.h" +#include "nsIEditorIMESupport.h" +#include "nsIEditRules.h" +#include "nsNameSpaceManager.h" +#include "nsINode.h" +#include "nsISupportsImpl.h" +#include "nsLiteralString.h" +#include "nsRange.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsUnicharUtils.h" +#include "nscore.h" + +class nsISupports; + +namespace mozilla { + +using namespace dom; + +static bool +IsEmptyTextNode(HTMLEditor* aThis, nsINode* aNode) +{ + bool isEmptyTextNode = false; + return EditorBase::IsTextNode(aNode) && + NS_SUCCEEDED(aThis->IsEmptyNode(aNode, &isEmptyTextNode)) && + isEmptyTextNode; +} + +NS_IMETHODIMP +HTMLEditor::AddDefaultProperty(nsIAtom* aProperty, + const nsAString& aAttribute, + const nsAString& aValue) +{ + nsString outValue; + int32_t index; + nsString attr(aAttribute); + if (TypeInState::FindPropInList(aProperty, attr, &outValue, + mDefaultStyles, index)) { + PropItem *item = mDefaultStyles[index]; + item->value = aValue; + } else { + nsString value(aValue); + PropItem *propItem = new PropItem(aProperty, attr, value); + mDefaultStyles.AppendElement(propItem); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RemoveDefaultProperty(nsIAtom* aProperty, + const nsAString& aAttribute, + const nsAString& aValue) +{ + nsString outValue; + int32_t index; + nsString attr(aAttribute); + if (TypeInState::FindPropInList(aProperty, attr, &outValue, + mDefaultStyles, index)) { + delete mDefaultStyles[index]; + mDefaultStyles.RemoveElementAt(index); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::RemoveAllDefaultProperties() +{ + size_t defcon = mDefaultStyles.Length(); + for (size_t j = 0; j < defcon; j++) { + delete mDefaultStyles[j]; + } + mDefaultStyles.Clear(); + return NS_OK; +} + + +NS_IMETHODIMP +HTMLEditor::SetInlineProperty(nsIAtom* aProperty, + const nsAString& aAttribute, + const nsAString& aValue) +{ + NS_ENSURE_TRUE(aProperty, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + nsCOMPtr<nsIEditRules> rules(mRules); + ForceCompositionEnd(); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + if (selection->Collapsed()) { + // Manipulating text attributes on a collapsed selection only sets state + // for the next text insertion + mTypeInState->SetProp(aProperty, aAttribute, aValue); + return NS_OK; + } + + AutoEditBatch batchIt(this); + AutoRules beginRulesSniffing(this, EditAction::insertElement, + nsIEditor::eNext); + AutoSelectionRestorer selectionRestorer(selection, this); + AutoTransactionsConserveSelection dontSpazMySelection(this); + + bool cancel, handled; + TextRulesInfo ruleInfo(EditAction::setTextProperty); + // Protect the edit rules object from dying + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (!cancel && !handled) { + // Loop through the ranges in the selection + uint32_t rangeCount = selection->RangeCount(); + for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; rangeIdx++) { + RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); + + // Adjust range to include any ancestors whose children are entirely + // selected + rv = PromoteInlineRange(*range); + NS_ENSURE_SUCCESS(rv, rv); + + // Check for easy case: both range endpoints in same text node + nsCOMPtr<nsINode> startNode = range->GetStartParent(); + nsCOMPtr<nsINode> endNode = range->GetEndParent(); + if (startNode && startNode == endNode && startNode->GetAsText()) { + rv = SetInlinePropertyOnTextNode(*startNode->GetAsText(), + range->StartOffset(), + range->EndOffset(), + *aProperty, &aAttribute, aValue); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + + // Not the easy case. Range not contained in single text node. There + // are up to three phases here. There are all the nodes reported by the + // subtree iterator to be processed. And there are potentially a + // starting textnode and an ending textnode which are only partially + // contained by the range. + + // Let's handle the nodes reported by the iterator. These nodes are + // entirely contained in the selection range. We build up a list of them + // (since doing operations on the document during iteration would perturb + // the iterator). + + OwningNonNull<nsIContentIterator> iter = NS_NewContentSubtreeIterator(); + + nsTArray<OwningNonNull<nsIContent>> arrayOfNodes; + + // Iterate range and build up array + rv = iter->Init(range); + // Init returns an error if there are no nodes in range. This can easily + // happen with the subtree iterator if the selection doesn't contain any + // *whole* nodes. + if (NS_SUCCEEDED(rv)) { + for (; !iter->IsDone(); iter->Next()) { + OwningNonNull<nsINode> node = *iter->GetCurrentNode(); + + if (node->IsContent() && IsEditable(node)) { + arrayOfNodes.AppendElement(*node->AsContent()); + } + } + } + // First check the start parent of the range to see if it needs to be + // separately handled (it does if it's a text node, due to how the + // subtree iterator works - it will not have reported it). + if (startNode && startNode->GetAsText() && IsEditable(startNode)) { + rv = SetInlinePropertyOnTextNode(*startNode->GetAsText(), + range->StartOffset(), + startNode->Length(), *aProperty, + &aAttribute, aValue); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Then loop through the list, set the property on each node + for (auto& node : arrayOfNodes) { + rv = SetInlinePropertyOnNode(*node, *aProperty, &aAttribute, aValue); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Last check the end parent of the range to see if it needs to be + // separately handled (it does if it's a text node, due to how the + // subtree iterator works - it will not have reported it). + if (endNode && endNode->GetAsText() && IsEditable(endNode)) { + rv = SetInlinePropertyOnTextNode(*endNode->GetAsText(), 0, + range->EndOffset(), *aProperty, + &aAttribute, aValue); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + if (!cancel) { + // Post-process + return rules->DidDoAction(selection, &ruleInfo, rv); + } + return NS_OK; +} + +// Helper function for SetInlinePropertyOn*: is aNode a simple old <b>, <font>, +// <span style="">, etc. that we can reuse instead of creating a new one? +bool +HTMLEditor::IsSimpleModifiableNode(nsIContent* aContent, + nsIAtom* aProperty, + const nsAString* aAttribute, + const nsAString* aValue) +{ + // aContent can be null, in which case we'll return false in a few lines + MOZ_ASSERT(aProperty); + MOZ_ASSERT_IF(aAttribute, aValue); + + nsCOMPtr<dom::Element> element = do_QueryInterface(aContent); + if (!element) { + return false; + } + + // First check for <b>, <i>, etc. + if (element->IsHTMLElement(aProperty) && !element->GetAttrCount() && + (!aAttribute || aAttribute->IsEmpty())) { + return true; + } + + // Special cases for various equivalencies: <strong>, <em>, <s> + if (!element->GetAttrCount() && + ((aProperty == nsGkAtoms::b && + element->IsHTMLElement(nsGkAtoms::strong)) || + (aProperty == nsGkAtoms::i && + element->IsHTMLElement(nsGkAtoms::em)) || + (aProperty == nsGkAtoms::strike && + element->IsHTMLElement(nsGkAtoms::s)))) { + return true; + } + + // Now look for things like <font> + if (aAttribute && !aAttribute->IsEmpty()) { + nsCOMPtr<nsIAtom> atom = NS_Atomize(*aAttribute); + MOZ_ASSERT(atom); + + nsString attrValue; + if (element->IsHTMLElement(aProperty) && + IsOnlyAttribute(element, *aAttribute) && + element->GetAttr(kNameSpaceID_None, atom, attrValue) && + attrValue.Equals(*aValue, nsCaseInsensitiveStringComparator())) { + // This is not quite correct, because it excludes cases like + // <font face=000> being the same as <font face=#000000>. + // Property-specific handling is needed (bug 760211). + return true; + } + } + + // No luck so far. Now we check for a <span> with a single style="" + // attribute that sets only the style we're looking for, if this type of + // style supports it + if (!mCSSEditUtils->IsCSSEditableProperty(element, aProperty, aAttribute) || + !element->IsHTMLElement(nsGkAtoms::span) || + element->GetAttrCount() != 1 || + !element->HasAttr(kNameSpaceID_None, nsGkAtoms::style)) { + return false; + } + + // Some CSS styles are not so simple. For instance, underline is + // "text-decoration: underline", which decomposes into four different text-* + // properties. So for now, we just create a span, add the desired style, and + // see if it matches. + nsCOMPtr<Element> newSpan = CreateHTMLContent(nsGkAtoms::span); + NS_ASSERTION(newSpan, "CreateHTMLContent failed"); + NS_ENSURE_TRUE(newSpan, false); + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(newSpan, aProperty, + aAttribute, aValue, + /*suppress transaction*/ true); + + return mCSSEditUtils->ElementsSameStyle(newSpan, element); +} + +nsresult +HTMLEditor::SetInlinePropertyOnTextNode(Text& aText, + int32_t aStartOffset, + int32_t aEndOffset, + nsIAtom& aProperty, + const nsAString* aAttribute, + const nsAString& aValue) +{ + if (!aText.GetParentNode() || + !CanContainTag(*aText.GetParentNode(), aProperty)) { + return NS_OK; + } + + // Don't need to do anything if no characters actually selected + if (aStartOffset == aEndOffset) { + return NS_OK; + } + + // Don't need to do anything if property already set on node + if (mCSSEditUtils->IsCSSEditableProperty(&aText, &aProperty, aAttribute)) { + // The HTML styles defined by aProperty/aAttribute have a CSS equivalence + // for node; let's check if it carries those CSS styles + if (mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(&aText, &aProperty, + aAttribute, aValue, CSSEditUtils::eComputed)) { + return NS_OK; + } + } else if (IsTextPropertySetByContent(&aText, &aProperty, aAttribute, + &aValue)) { + return NS_OK; + } + + // Do we need to split the text node? + ErrorResult rv; + RefPtr<Text> text = &aText; + if (uint32_t(aEndOffset) != aText.Length()) { + // We need to split off back of text node + text = SplitNode(aText, aEndOffset, rv)->GetAsText(); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + } + + if (aStartOffset) { + // We need to split off front of text node + SplitNode(*text, aStartOffset, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + } + + if (aAttribute) { + // Look for siblings that are correct type of node + nsIContent* sibling = GetPriorHTMLSibling(text); + if (IsSimpleModifiableNode(sibling, &aProperty, aAttribute, &aValue)) { + // Previous sib is already right kind of inline node; slide this over + return MoveNode(text, sibling, -1); + } + sibling = GetNextHTMLSibling(text); + if (IsSimpleModifiableNode(sibling, &aProperty, aAttribute, &aValue)) { + // Following sib is already right kind of inline node; slide this over + return MoveNode(text, sibling, 0); + } + } + + // Reparent the node inside inline node with appropriate {attribute,value} + return SetInlinePropertyOnNode(*text, aProperty, aAttribute, aValue); +} + +nsresult +HTMLEditor::SetInlinePropertyOnNodeImpl(nsIContent& aNode, + nsIAtom& aProperty, + const nsAString* aAttribute, + const nsAString& aValue) +{ + nsCOMPtr<nsIAtom> attrAtom = aAttribute ? NS_Atomize(*aAttribute) : nullptr; + + // If this is an element that can't be contained in a span, we have to + // recurse to its children. + if (!TagCanContain(*nsGkAtoms::span, aNode)) { + if (aNode.HasChildren()) { + nsTArray<OwningNonNull<nsIContent>> arrayOfNodes; + + // Populate the list. + for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild(); + child; + child = child->GetNextSibling()) { + if (IsEditable(child) && !IsEmptyTextNode(this, child)) { + arrayOfNodes.AppendElement(*child); + } + } + + // Then loop through the list, set the property on each node. + for (auto& node : arrayOfNodes) { + nsresult rv = SetInlinePropertyOnNode(node, aProperty, aAttribute, + aValue); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; + } + + // First check if there's an adjacent sibling we can put our node into. + nsCOMPtr<nsIContent> previousSibling = GetPriorHTMLSibling(&aNode); + nsCOMPtr<nsIContent> nextSibling = GetNextHTMLSibling(&aNode); + if (IsSimpleModifiableNode(previousSibling, &aProperty, aAttribute, &aValue)) { + nsresult rv = MoveNode(&aNode, previousSibling, -1); + NS_ENSURE_SUCCESS(rv, rv); + if (IsSimpleModifiableNode(nextSibling, &aProperty, aAttribute, &aValue)) { + rv = JoinNodes(*previousSibling, *nextSibling); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; + } + if (IsSimpleModifiableNode(nextSibling, &aProperty, aAttribute, &aValue)) { + nsresult rv = MoveNode(&aNode, nextSibling, 0); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + // Don't need to do anything if property already set on node + if (mCSSEditUtils->IsCSSEditableProperty(&aNode, &aProperty, aAttribute)) { + if (mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet( + &aNode, &aProperty, aAttribute, aValue, CSSEditUtils::eComputed)) { + return NS_OK; + } + } else if (IsTextPropertySetByContent(&aNode, &aProperty, + aAttribute, &aValue)) { + return NS_OK; + } + + bool useCSS = (IsCSSEnabled() && + mCSSEditUtils->IsCSSEditableProperty(&aNode, &aProperty, + aAttribute)) || + // bgcolor is always done using CSS + aAttribute->EqualsLiteral("bgcolor"); + + if (useCSS) { + nsCOMPtr<dom::Element> tmp; + // We only add style="" to <span>s with no attributes (bug 746515). If we + // don't have one, we need to make one. + if (aNode.IsHTMLElement(nsGkAtoms::span) && + !aNode.AsElement()->GetAttrCount()) { + tmp = aNode.AsElement(); + } else { + tmp = InsertContainerAbove(&aNode, nsGkAtoms::span); + NS_ENSURE_STATE(tmp); + } + + // Add the CSS styles corresponding to the HTML style request + int32_t count; + nsresult rv = + mCSSEditUtils->SetCSSEquivalentToHTMLStyle(tmp->AsDOMNode(), + &aProperty, aAttribute, + &aValue, &count, false); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + // is it already the right kind of node, but with wrong attribute? + if (aNode.IsHTMLElement(&aProperty)) { + // Just set the attribute on it. + nsCOMPtr<nsIDOMElement> elem = do_QueryInterface(&aNode); + return SetAttribute(elem, *aAttribute, aValue); + } + + // ok, chuck it in its very own container + nsCOMPtr<Element> tmp = InsertContainerAbove(&aNode, &aProperty, attrAtom, + &aValue); + NS_ENSURE_STATE(tmp); + + return NS_OK; +} + +nsresult +HTMLEditor::SetInlinePropertyOnNode(nsIContent& aNode, + nsIAtom& aProperty, + const nsAString* aAttribute, + const nsAString& aValue) +{ + nsCOMPtr<nsIContent> previousSibling = aNode.GetPreviousSibling(), + nextSibling = aNode.GetNextSibling(); + NS_ENSURE_STATE(aNode.GetParentNode()); + OwningNonNull<nsINode> parent = *aNode.GetParentNode(); + + nsresult rv = RemoveStyleInside(aNode, &aProperty, aAttribute); + NS_ENSURE_SUCCESS(rv, rv); + + if (aNode.GetParentNode()) { + // The node is still where it was + return SetInlinePropertyOnNodeImpl(aNode, aProperty, + aAttribute, aValue); + } + + // It's vanished. Use the old siblings for reference to construct a + // list. But first, verify that the previous/next siblings are still + // where we expect them; otherwise we have to give up. + if ((previousSibling && previousSibling->GetParentNode() != parent) || + (nextSibling && nextSibling->GetParentNode() != parent)) { + return NS_ERROR_UNEXPECTED; + } + nsTArray<OwningNonNull<nsIContent>> nodesToSet; + nsCOMPtr<nsIContent> cur = previousSibling + ? previousSibling->GetNextSibling() : parent->GetFirstChild(); + for (; cur && cur != nextSibling; cur = cur->GetNextSibling()) { + if (IsEditable(cur)) { + nodesToSet.AppendElement(*cur); + } + } + + for (auto& node : nodesToSet) { + rv = SetInlinePropertyOnNodeImpl(node, aProperty, aAttribute, aValue); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +HTMLEditor::SplitStyleAboveRange(nsRange* inRange, + nsIAtom* aProperty, + const nsAString* aAttribute) +{ + NS_ENSURE_TRUE(inRange, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> startNode = inRange->GetStartParent(); + int32_t startOffset = inRange->StartOffset(); + nsCOMPtr<nsINode> endNode = inRange->GetEndParent(); + int32_t endOffset = inRange->EndOffset(); + + nsCOMPtr<nsINode> origStartNode = startNode; + + // split any matching style nodes above the start of range + { + AutoTrackDOMPoint tracker(mRangeUpdater, address_of(endNode), &endOffset); + nsresult rv = + SplitStyleAbovePoint(address_of(startNode), &startOffset, aProperty, + aAttribute); + NS_ENSURE_SUCCESS(rv, rv); + } + + // second verse, same as the first... + nsresult rv = + SplitStyleAbovePoint(address_of(endNode), &endOffset, aProperty, + aAttribute); + NS_ENSURE_SUCCESS(rv, rv); + + // reset the range + rv = inRange->SetStart(startNode, startOffset); + NS_ENSURE_SUCCESS(rv, rv); + return inRange->SetEnd(endNode, endOffset); +} + +nsresult +HTMLEditor::SplitStyleAbovePoint(nsCOMPtr<nsINode>* aNode, + int32_t* aOffset, + // null here means we split all properties + nsIAtom* aProperty, + const nsAString* aAttribute, + nsIContent** aOutLeftNode, + nsIContent** aOutRightNode) +{ + NS_ENSURE_TRUE(aNode && *aNode && aOffset, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE((*aNode)->IsContent(), NS_OK); + + // Split any matching style nodes above the node/offset + OwningNonNull<nsIContent> node = *(*aNode)->AsContent(); + + bool useCSS = IsCSSEnabled(); + + bool isSet; + while (!IsBlockNode(node) && node->GetParent() && + IsEditable(node->GetParent())) { + isSet = false; + if (useCSS && mCSSEditUtils->IsCSSEditableProperty(node, aProperty, + aAttribute)) { + // The HTML style defined by aProperty/aAttribute has a CSS equivalence + // in this implementation for the node; let's check if it carries those + // CSS styles + nsAutoString firstValue; + mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(GetAsDOMNode(node), + aProperty, aAttribute, isSet, firstValue, CSSEditUtils::eSpecified); + } + if (// node is the correct inline prop + (aProperty && node->IsHTMLElement(aProperty)) || + // node is href - test if really <a href=... + (aProperty == nsGkAtoms::href && HTMLEditUtils::IsLink(node)) || + // or node is any prop, and we asked to split them all + (!aProperty && NodeIsProperty(node)) || + // or the style is specified in the style attribute + isSet) { + // Found a style node we need to split + int32_t offset = SplitNodeDeep(*node, *(*aNode)->AsContent(), *aOffset, + EmptyContainers::yes, aOutLeftNode, + aOutRightNode); + NS_ENSURE_TRUE(offset != -1, NS_ERROR_FAILURE); + // reset startNode/startOffset + *aNode = node->GetParent(); + *aOffset = offset; + } + node = node->GetParent(); + } + + return NS_OK; +} + +nsresult +HTMLEditor::ClearStyle(nsCOMPtr<nsINode>* aNode, + int32_t* aOffset, + nsIAtom* aProperty, + const nsAString* aAttribute) +{ + nsCOMPtr<nsIContent> leftNode, rightNode; + nsresult rv = SplitStyleAbovePoint(aNode, aOffset, aProperty, + aAttribute, getter_AddRefs(leftNode), + getter_AddRefs(rightNode)); + NS_ENSURE_SUCCESS(rv, rv); + + if (leftNode) { + bool bIsEmptyNode; + IsEmptyNode(leftNode, &bIsEmptyNode, false, true); + if (bIsEmptyNode) { + // delete leftNode if it became empty + rv = DeleteNode(leftNode); + NS_ENSURE_SUCCESS(rv, rv); + } + } + if (rightNode) { + nsCOMPtr<nsINode> secondSplitParent = GetLeftmostChild(rightNode); + // don't try to split non-containers (br's, images, hr's, etc.) + if (!secondSplitParent) { + secondSplitParent = rightNode; + } + nsCOMPtr<Element> savedBR; + if (!IsContainer(secondSplitParent)) { + if (TextEditUtils::IsBreak(secondSplitParent)) { + savedBR = do_QueryInterface(secondSplitParent); + NS_ENSURE_STATE(savedBR); + } + + secondSplitParent = secondSplitParent->GetParentNode(); + } + *aOffset = 0; + rv = SplitStyleAbovePoint(address_of(secondSplitParent), + aOffset, aProperty, aAttribute, + getter_AddRefs(leftNode), + getter_AddRefs(rightNode)); + NS_ENSURE_SUCCESS(rv, rv); + + if (rightNode) { + bool bIsEmptyNode; + IsEmptyNode(rightNode, &bIsEmptyNode, false, true); + if (bIsEmptyNode) { + // delete rightNode if it became empty + rv = DeleteNode(rightNode); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + if (!leftNode) { + return NS_OK; + } + + // should be impossible to not get a new leftnode here + nsCOMPtr<nsINode> newSelParent = GetLeftmostChild(leftNode); + if (!newSelParent) { + newSelParent = leftNode; + } + // If rightNode starts with a br, suck it out of right node and into + // leftNode. This is so we you don't revert back to the previous style + // if you happen to click at the end of a line. + if (savedBR) { + rv = MoveNode(savedBR, newSelParent, 0); + NS_ENSURE_SUCCESS(rv, rv); + } + // remove the style on this new hierarchy + int32_t newSelOffset = 0; + { + // Track the point at the new hierarchy. This is so we can know where + // to put the selection after we call RemoveStyleInside(). + // RemoveStyleInside() could remove any and all of those nodes, so I + // have to use the range tracking system to find the right spot to put + // selection. + AutoTrackDOMPoint tracker(mRangeUpdater, + address_of(newSelParent), &newSelOffset); + rv = RemoveStyleInside(*leftNode, aProperty, aAttribute); + NS_ENSURE_SUCCESS(rv, rv); + } + // reset our node offset values to the resulting new sel point + *aNode = newSelParent; + *aOffset = newSelOffset; + } + + return NS_OK; +} + +bool +HTMLEditor::NodeIsProperty(nsINode& aNode) +{ + return IsContainer(&aNode) && IsEditable(&aNode) && !IsBlockNode(&aNode) && + !aNode.IsHTMLElement(nsGkAtoms::a); +} + +nsresult +HTMLEditor::ApplyDefaultProperties() +{ + size_t defcon = mDefaultStyles.Length(); + for (size_t j = 0; j < defcon; j++) { + PropItem *propItem = mDefaultStyles[j]; + NS_ENSURE_TRUE(propItem, NS_ERROR_NULL_POINTER); + nsresult rv = + SetInlineProperty(propItem->tag, propItem->attr, propItem->value); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult +HTMLEditor::RemoveStyleInside(nsIContent& aNode, + nsIAtom* aProperty, + const nsAString* aAttribute, + const bool aChildrenOnly /* = false */) +{ + if (aNode.GetAsText()) { + return NS_OK; + } + + // first process the children + RefPtr<nsIContent> child = aNode.GetFirstChild(); + while (child) { + // cache next sibling since we might remove child + nsCOMPtr<nsIContent> next = child->GetNextSibling(); + nsresult rv = RemoveStyleInside(*child, aProperty, aAttribute); + NS_ENSURE_SUCCESS(rv, rv); + child = next.forget(); + } + + // then process the node itself + if (!aChildrenOnly && + // node is prop we asked for + ((aProperty && aNode.NodeInfo()->NameAtom() == aProperty) || + // but check for link (<a href=...) + (aProperty == nsGkAtoms::href && HTMLEditUtils::IsLink(&aNode)) || + // and for named anchors + (aProperty == nsGkAtoms::name && HTMLEditUtils::IsNamedAnchor(&aNode)) || + // or node is any prop and we asked for that + (!aProperty && NodeIsProperty(aNode)))) { + // if we weren't passed an attribute, then we want to + // remove any matching inlinestyles entirely + if (!aAttribute || aAttribute->IsEmpty()) { + NS_NAMED_LITERAL_STRING(styleAttr, "style"); + NS_NAMED_LITERAL_STRING(classAttr, "class"); + + bool hasStyleAttr = aNode.HasAttr(kNameSpaceID_None, nsGkAtoms::style); + bool hasClassAttr = aNode.HasAttr(kNameSpaceID_None, nsGkAtoms::_class); + if (aProperty && (hasStyleAttr || hasClassAttr)) { + // aNode carries inline styles or a class attribute so we can't + // just remove the element... We need to create above the element + // a span that will carry those styles or class, then we can delete + // the node. + nsCOMPtr<Element> spanNode = + InsertContainerAbove(&aNode, nsGkAtoms::span); + NS_ENSURE_STATE(spanNode); + nsresult rv = + CloneAttribute(styleAttr, spanNode->AsDOMNode(), aNode.AsDOMNode()); + NS_ENSURE_SUCCESS(rv, rv); + rv = + CloneAttribute(classAttr, spanNode->AsDOMNode(), aNode.AsDOMNode()); + NS_ENSURE_SUCCESS(rv, rv); + } + nsresult rv = RemoveContainer(&aNode); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // otherwise we just want to eliminate the attribute + nsCOMPtr<nsIAtom> attribute = NS_Atomize(*aAttribute); + if (aNode.HasAttr(kNameSpaceID_None, attribute)) { + // if this matching attribute is the ONLY one on the node, + // then remove the whole node. Otherwise just nix the attribute. + if (IsOnlyAttribute(&aNode, *aAttribute)) { + nsresult rv = RemoveContainer(&aNode); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsCOMPtr<nsIDOMElement> elem = do_QueryInterface(&aNode); + NS_ENSURE_TRUE(elem, NS_ERROR_NULL_POINTER); + nsresult rv = RemoveAttribute(elem, *aAttribute); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + } + } + + if (!aChildrenOnly && + mCSSEditUtils->IsCSSEditableProperty(&aNode, aProperty, aAttribute)) { + // the HTML style defined by aProperty/aAttribute has a CSS equivalence in + // this implementation for the node aNode; let's check if it carries those + // css styles + nsAutoString propertyValue; + bool isSet = mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(&aNode, + aProperty, aAttribute, propertyValue, CSSEditUtils::eSpecified); + if (isSet && aNode.IsElement()) { + // yes, tmp has the corresponding css declarations in its style attribute + // let's remove them + mCSSEditUtils->RemoveCSSEquivalentToHTMLStyle(aNode.AsElement(), + aProperty, + aAttribute, + &propertyValue, + false); + // remove the node if it is a span or font, if its style attribute is + // empty or absent, and if it does not have a class nor an id + RemoveElementIfNoStyleOrIdOrClass(*aNode.AsElement()); + } + } + + // Or node is big or small and we are setting font size + if (aChildrenOnly) { + return NS_OK; + } + if (aProperty == nsGkAtoms::font && + (aNode.IsHTMLElement(nsGkAtoms::big) || + aNode.IsHTMLElement(nsGkAtoms::small)) && + aAttribute && aAttribute->LowerCaseEqualsLiteral("size")) { + // if we are setting font size, remove any nested bigs and smalls + return RemoveContainer(&aNode); + } + return NS_OK; +} + +bool +HTMLEditor::IsOnlyAttribute(const nsIContent* aContent, + const nsAString& aAttribute) +{ + MOZ_ASSERT(aContent); + + uint32_t attrCount = aContent->GetAttrCount(); + for (uint32_t i = 0; i < attrCount; ++i) { + const nsAttrName* name = aContent->GetAttrNameAt(i); + if (!name->NamespaceEquals(kNameSpaceID_None)) { + return false; + } + + nsAutoString attrString; + name->LocalName()->ToString(attrString); + // if it's the attribute we know about, or a special _moz attribute, + // keep looking + if (!attrString.Equals(aAttribute, nsCaseInsensitiveStringComparator()) && + !StringBeginsWith(attrString, NS_LITERAL_STRING("_moz"))) { + return false; + } + } + // if we made it through all of them without finding a real attribute + // other than aAttribute, then return true + return true; +} + +nsresult +HTMLEditor::PromoteRangeIfStartsOrEndsInNamedAnchor(nsRange& aRange) +{ + // We assume that <a> is not nested. + nsCOMPtr<nsINode> startNode = aRange.GetStartParent(); + int32_t startOffset = aRange.StartOffset(); + nsCOMPtr<nsINode> endNode = aRange.GetEndParent(); + int32_t endOffset = aRange.EndOffset(); + + nsCOMPtr<nsINode> parent = startNode; + + while (parent && !parent->IsHTMLElement(nsGkAtoms::body) && + !HTMLEditUtils::IsNamedAnchor(parent)) { + parent = parent->GetParentNode(); + } + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + + if (HTMLEditUtils::IsNamedAnchor(parent)) { + startNode = parent->GetParentNode(); + startOffset = startNode ? startNode->IndexOf(parent) : -1; + } + + parent = endNode; + while (parent && !parent->IsHTMLElement(nsGkAtoms::body) && + !HTMLEditUtils::IsNamedAnchor(parent)) { + parent = parent->GetParentNode(); + } + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + + if (HTMLEditUtils::IsNamedAnchor(parent)) { + endNode = parent->GetParentNode(); + endOffset = endNode ? endNode->IndexOf(parent) + 1 : 0; + } + + nsresult rv = aRange.SetStart(startNode, startOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = aRange.SetEnd(endNode, endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +HTMLEditor::PromoteInlineRange(nsRange& aRange) +{ + nsCOMPtr<nsINode> startNode = aRange.GetStartParent(); + int32_t startOffset = aRange.StartOffset(); + nsCOMPtr<nsINode> endNode = aRange.GetEndParent(); + int32_t endOffset = aRange.EndOffset(); + + while (startNode && !startNode->IsHTMLElement(nsGkAtoms::body) && + IsEditable(startNode) && IsAtFrontOfNode(*startNode, startOffset)) { + nsCOMPtr<nsINode> parent = startNode->GetParentNode(); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + startOffset = parent->IndexOf(startNode); + startNode = parent; + } + + while (endNode && !endNode->IsHTMLElement(nsGkAtoms::body) && + IsEditable(endNode) && IsAtEndOfNode(*endNode, endOffset)) { + nsCOMPtr<nsINode> parent = endNode->GetParentNode(); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + // We are AFTER this node + endOffset = 1 + parent->IndexOf(endNode); + endNode = parent; + } + + nsresult rv = aRange.SetStart(startNode, startOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = aRange.SetEnd(endNode, endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +bool +HTMLEditor::IsAtFrontOfNode(nsINode& aNode, + int32_t aOffset) +{ + if (!aOffset) { + return true; + } + + if (IsTextNode(&aNode)) { + return false; + } + + nsCOMPtr<nsIContent> firstNode = GetFirstEditableChild(aNode); + NS_ENSURE_TRUE(firstNode, true); + if (aNode.IndexOf(firstNode) < aOffset) { + return false; + } + return true; +} + +bool +HTMLEditor::IsAtEndOfNode(nsINode& aNode, + int32_t aOffset) +{ + if (aOffset == (int32_t)aNode.Length()) { + return true; + } + + if (IsTextNode(&aNode)) { + return false; + } + + nsCOMPtr<nsIContent> lastNode = GetLastEditableChild(aNode); + NS_ENSURE_TRUE(lastNode, true); + if (aNode.IndexOf(lastNode) < aOffset) { + return true; + } + return false; +} + + +nsresult +HTMLEditor::GetInlinePropertyBase(nsIAtom& aProperty, + const nsAString* aAttribute, + const nsAString* aValue, + bool* aFirst, + bool* aAny, + bool* aAll, + nsAString* outValue, + bool aCheckDefaults) +{ + *aAny = false; + *aAll = true; + *aFirst = false; + bool first = true; + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + bool isCollapsed = selection->Collapsed(); + RefPtr<nsRange> range = selection->GetRangeAt(0); + // XXX: Should be a while loop, to get each separate range + // XXX: ERROR_HANDLING can currentItem be null? + if (range) { + // For each range, set a flag + bool firstNodeInRange = true; + + if (isCollapsed) { + nsCOMPtr<nsINode> collapsedNode = range->GetStartParent(); + NS_ENSURE_TRUE(collapsedNode, NS_ERROR_FAILURE); + bool isSet, theSetting; + nsString tOutString; + if (aAttribute) { + nsString tString(*aAttribute); + mTypeInState->GetTypingState(isSet, theSetting, &aProperty, tString, + &tOutString); + if (outValue) { + outValue->Assign(tOutString); + } + } else { + mTypeInState->GetTypingState(isSet, theSetting, &aProperty); + } + if (isSet) { + *aFirst = *aAny = *aAll = theSetting; + return NS_OK; + } + + if (mCSSEditUtils->IsCSSEditableProperty(collapsedNode, &aProperty, + aAttribute)) { + if (aValue) { + tOutString.Assign(*aValue); + } + *aFirst = *aAny = *aAll = + mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(collapsedNode, + &aProperty, aAttribute, tOutString, CSSEditUtils::eComputed); + if (outValue) { + outValue->Assign(tOutString); + } + return NS_OK; + } + + isSet = IsTextPropertySetByContent(collapsedNode, &aProperty, + aAttribute, aValue, outValue); + *aFirst = *aAny = *aAll = isSet; + + if (!isSet && aCheckDefaults) { + // Style not set, but if it is a default then it will appear if content + // is inserted, so we should report it as set (analogous to + // TypeInState). + int32_t index; + if (aAttribute && TypeInState::FindPropInList(&aProperty, *aAttribute, + outValue, mDefaultStyles, + index)) { + *aFirst = *aAny = *aAll = true; + if (outValue) { + outValue->Assign(mDefaultStyles[index]->value); + } + } + } + return NS_OK; + } + + // Non-collapsed selection + nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator(); + + nsAutoString firstValue, theValue; + + nsCOMPtr<nsINode> endNode = range->GetEndParent(); + int32_t endOffset = range->EndOffset(); + + for (iter->Init(range); !iter->IsDone(); iter->Next()) { + if (!iter->GetCurrentNode()->IsContent()) { + continue; + } + nsCOMPtr<nsIContent> content = iter->GetCurrentNode()->AsContent(); + + if (content->IsHTMLElement(nsGkAtoms::body)) { + break; + } + + // just ignore any non-editable nodes + if (content->GetAsText() && (!IsEditable(content) || + IsEmptyTextNode(this, content))) { + continue; + } + if (content->GetAsText()) { + if (!isCollapsed && first && firstNodeInRange) { + firstNodeInRange = false; + if (range->StartOffset() == (int32_t)content->Length()) { + continue; + } + } else if (content == endNode && !endOffset) { + continue; + } + } else if (content->IsElement()) { + // handle non-text leaf nodes here + continue; + } + + bool isSet = false; + if (first) { + if (mCSSEditUtils->IsCSSEditableProperty(content, &aProperty, + aAttribute)) { + // The HTML styles defined by aProperty/aAttribute have a CSS + // equivalence in this implementation for node; let's check if it + // carries those CSS styles + if (aValue) { + firstValue.Assign(*aValue); + } + isSet = mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(content, + &aProperty, aAttribute, firstValue, CSSEditUtils::eComputed); + } else { + isSet = IsTextPropertySetByContent(content, &aProperty, aAttribute, + aValue, &firstValue); + } + *aFirst = isSet; + first = false; + if (outValue) { + *outValue = firstValue; + } + } else { + if (mCSSEditUtils->IsCSSEditableProperty(content, &aProperty, + aAttribute)) { + // The HTML styles defined by aProperty/aAttribute have a CSS + // equivalence in this implementation for node; let's check if it + // carries those CSS styles + if (aValue) { + theValue.Assign(*aValue); + } + isSet = mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(content, + &aProperty, aAttribute, theValue, CSSEditUtils::eComputed); + } else { + isSet = IsTextPropertySetByContent(content, &aProperty, aAttribute, + aValue, &theValue); + } + if (firstValue != theValue) { + *aAll = false; + } + } + + if (isSet) { + *aAny = true; + } else { + *aAll = false; + } + } + } + if (!*aAny) { + // make sure that if none of the selection is set, we don't report all is + // set + *aAll = false; + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetInlineProperty(nsIAtom* aProperty, + const nsAString& aAttribute, + const nsAString& aValue, + bool* aFirst, + bool* aAny, + bool* aAll) +{ + NS_ENSURE_TRUE(aProperty && aFirst && aAny && aAll, NS_ERROR_NULL_POINTER); + const nsAString *att = nullptr; + if (!aAttribute.IsEmpty()) + att = &aAttribute; + const nsAString *val = nullptr; + if (!aValue.IsEmpty()) + val = &aValue; + return GetInlinePropertyBase(*aProperty, att, val, aFirst, aAny, aAll, nullptr); +} + +NS_IMETHODIMP +HTMLEditor::GetInlinePropertyWithAttrValue(nsIAtom* aProperty, + const nsAString& aAttribute, + const nsAString& aValue, + bool* aFirst, + bool* aAny, + bool* aAll, + nsAString& outValue) +{ + NS_ENSURE_TRUE(aProperty && aFirst && aAny && aAll, NS_ERROR_NULL_POINTER); + const nsAString *att = nullptr; + if (!aAttribute.IsEmpty()) + att = &aAttribute; + const nsAString *val = nullptr; + if (!aValue.IsEmpty()) + val = &aValue; + return GetInlinePropertyBase(*aProperty, att, val, aFirst, aAny, aAll, &outValue); +} + +NS_IMETHODIMP +HTMLEditor::RemoveAllInlineProperties() +{ + AutoEditBatch batchIt(this); + AutoRules beginRulesSniffing(this, EditAction::resetTextProperties, + nsIEditor::eNext); + + nsresult rv = RemoveInlinePropertyImpl(nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + return ApplyDefaultProperties(); +} + +NS_IMETHODIMP +HTMLEditor::RemoveInlineProperty(nsIAtom* aProperty, + const nsAString& aAttribute) +{ + return RemoveInlinePropertyImpl(aProperty, &aAttribute); +} + +nsresult +HTMLEditor::RemoveInlinePropertyImpl(nsIAtom* aProperty, + const nsAString* aAttribute) +{ + MOZ_ASSERT_IF(aProperty, aAttribute); + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + ForceCompositionEnd(); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + if (selection->Collapsed()) { + // Manipulating text attributes on a collapsed selection only sets state + // for the next text insertion + + // For links, aProperty uses "href", use "a" instead + if (aProperty == nsGkAtoms::href || aProperty == nsGkAtoms::name) { + aProperty = nsGkAtoms::a; + } + + if (aProperty) { + mTypeInState->ClearProp(aProperty, *aAttribute); + } else { + mTypeInState->ClearAllProps(); + } + return NS_OK; + } + + AutoEditBatch batchIt(this); + AutoRules beginRulesSniffing(this, EditAction::removeTextProperty, + nsIEditor::eNext); + AutoSelectionRestorer selectionRestorer(selection, this); + AutoTransactionsConserveSelection dontSpazMySelection(this); + + bool cancel, handled; + TextRulesInfo ruleInfo(EditAction::removeTextProperty); + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (!cancel && !handled) { + // Loop through the ranges in the selection + uint32_t rangeCount = selection->RangeCount(); + // Since ranges might be modified by SplitStyleAboveRange, we need hold + // current ranges + AutoTArray<OwningNonNull<nsRange>, 8> arrayOfRanges; + for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) { + arrayOfRanges.AppendElement(*selection->GetRangeAt(rangeIdx)); + } + for (auto& range : arrayOfRanges) { + if (aProperty == nsGkAtoms::name) { + // Promote range if it starts or end in a named anchor and we want to + // remove named anchors + rv = PromoteRangeIfStartsOrEndsInNamedAnchor(range); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // Adjust range to include any ancestors whose children are entirely + // selected + rv = PromoteInlineRange(range); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Remove this style from ancestors of our range endpoints, splitting + // them as appropriate + rv = SplitStyleAboveRange(range, aProperty, aAttribute); + NS_ENSURE_SUCCESS(rv, rv); + + // Check for easy case: both range endpoints in same text node + nsCOMPtr<nsINode> startNode = range->GetStartParent(); + nsCOMPtr<nsINode> endNode = range->GetEndParent(); + if (startNode && startNode == endNode && startNode->GetAsText()) { + // We're done with this range! + if (IsCSSEnabled() && + mCSSEditUtils->IsCSSEditableProperty(startNode, aProperty, + aAttribute)) { + // The HTML style defined by aProperty/aAttribute has a CSS + // equivalence in this implementation for startNode + if (mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(startNode, + aProperty, aAttribute, EmptyString(), + CSSEditUtils::eComputed)) { + // startNode's computed style indicates the CSS equivalence to the + // HTML style to remove is applied; but we found no element in the + // ancestors of startNode carrying specified styles; assume it + // comes from a rule and try to insert a span "inverting" the style + if (mCSSEditUtils->IsCSSInvertible(*aProperty, aAttribute)) { + NS_NAMED_LITERAL_STRING(value, "-moz-editor-invert-value"); + SetInlinePropertyOnTextNode(*startNode->GetAsText(), + range->StartOffset(), + range->EndOffset(), *aProperty, + aAttribute, value); + } + } + } + } else { + // Not the easy case. Range not contained in single text node. + nsCOMPtr<nsIContentIterator> iter = NS_NewContentSubtreeIterator(); + + nsTArray<OwningNonNull<nsIContent>> arrayOfNodes; + + // Iterate range and build up array + for (iter->Init(range); !iter->IsDone(); iter->Next()) { + nsCOMPtr<nsINode> node = iter->GetCurrentNode(); + NS_ENSURE_TRUE(node, NS_ERROR_FAILURE); + + if (IsEditable(node) && node->IsContent()) { + arrayOfNodes.AppendElement(*node->AsContent()); + } + } + + // Loop through the list, remove the property on each node + for (auto& node : arrayOfNodes) { + rv = RemoveStyleInside(node, aProperty, aAttribute); + NS_ENSURE_SUCCESS(rv, rv); + if (IsCSSEnabled() && + mCSSEditUtils->IsCSSEditableProperty(node, aProperty, + aAttribute) && + mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(node, + aProperty, aAttribute, EmptyString(), + CSSEditUtils::eComputed) && + // startNode's computed style indicates the CSS equivalence to + // the HTML style to remove is applied; but we found no element + // in the ancestors of startNode carrying specified styles; + // assume it comes from a rule and let's try to insert a span + // "inverting" the style + mCSSEditUtils->IsCSSInvertible(*aProperty, aAttribute)) { + NS_NAMED_LITERAL_STRING(value, "-moz-editor-invert-value"); + SetInlinePropertyOnNode(node, *aProperty, aAttribute, value); + } + } + } + } + } + if (!cancel) { + // Post-process + rv = rules->DidDoAction(selection, &ruleInfo, rv); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::IncreaseFontSize() +{ + return RelativeFontChange(FontSize::incr); +} + +NS_IMETHODIMP +HTMLEditor::DecreaseFontSize() +{ + return RelativeFontChange(FontSize::decr); +} + +nsresult +HTMLEditor::RelativeFontChange(FontSize aDir) +{ + ForceCompositionEnd(); + + // Get the selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + // If selection is collapsed, set typing state + if (selection->Collapsed()) { + nsIAtom& atom = aDir == FontSize::incr ? *nsGkAtoms::big : + *nsGkAtoms::small; + + // Let's see in what kind of element the selection is + NS_ENSURE_TRUE(selection->RangeCount() && + selection->GetRangeAt(0)->GetStartParent(), NS_OK); + OwningNonNull<nsINode> selectedNode = + *selection->GetRangeAt(0)->GetStartParent(); + if (IsTextNode(selectedNode)) { + NS_ENSURE_TRUE(selectedNode->GetParentNode(), NS_OK); + selectedNode = *selectedNode->GetParentNode(); + } + if (!CanContainTag(selectedNode, atom)) { + return NS_OK; + } + + // Manipulating text attributes on a collapsed selection only sets state + // for the next text insertion + mTypeInState->SetProp(&atom, EmptyString(), EmptyString()); + return NS_OK; + } + + // Wrap with txn batching, rules sniffing, and selection preservation code + AutoEditBatch batchIt(this); + AutoRules beginRulesSniffing(this, EditAction::setTextProperty, + nsIEditor::eNext); + AutoSelectionRestorer selectionRestorer(selection, this); + AutoTransactionsConserveSelection dontSpazMySelection(this); + + // Loop through the ranges in the selection + uint32_t rangeCount = selection->RangeCount(); + for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) { + RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); + + // Adjust range to include any ancestors with entirely selected children + nsresult rv = PromoteInlineRange(*range); + NS_ENSURE_SUCCESS(rv, rv); + + // Check for easy case: both range endpoints in same text node + nsCOMPtr<nsINode> startNode = range->GetStartParent(); + nsCOMPtr<nsINode> endNode = range->GetEndParent(); + if (startNode == endNode && IsTextNode(startNode)) { + rv = RelativeFontChangeOnTextNode(aDir, *startNode->GetAsText(), + range->StartOffset(), + range->EndOffset()); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Not the easy case. Range not contained in single text node. There + // are up to three phases here. There are all the nodes reported by the + // subtree iterator to be processed. And there are potentially a + // starting textnode and an ending textnode which are only partially + // contained by the range. + + // Let's handle the nodes reported by the iterator. These nodes are + // entirely contained in the selection range. We build up a list of them + // (since doing operations on the document during iteration would perturb + // the iterator). + + OwningNonNull<nsIContentIterator> iter = NS_NewContentSubtreeIterator(); + + // Iterate range and build up array + rv = iter->Init(range); + if (NS_SUCCEEDED(rv)) { + nsTArray<OwningNonNull<nsIContent>> arrayOfNodes; + for (; !iter->IsDone(); iter->Next()) { + NS_ENSURE_TRUE(iter->GetCurrentNode()->IsContent(), NS_ERROR_FAILURE); + OwningNonNull<nsIContent> node = *iter->GetCurrentNode()->AsContent(); + + if (IsEditable(node)) { + arrayOfNodes.AppendElement(node); + } + } + + // Now that we have the list, do the font size change on each node + for (auto& node : arrayOfNodes) { + rv = RelativeFontChangeOnNode(aDir == FontSize::incr ? +1 : -1, node); + NS_ENSURE_SUCCESS(rv, rv); + } + } + // Now check the start and end parents of the range to see if they need + // to be separately handled (they do if they are text nodes, due to how + // the subtree iterator works - it will not have reported them). + if (IsTextNode(startNode) && IsEditable(startNode)) { + rv = RelativeFontChangeOnTextNode(aDir, *startNode->GetAsText(), + range->StartOffset(), + startNode->Length()); + NS_ENSURE_SUCCESS(rv, rv); + } + if (IsTextNode(endNode) && IsEditable(endNode)) { + rv = RelativeFontChangeOnTextNode(aDir, *endNode->GetAsText(), 0, + range->EndOffset()); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + return NS_OK; +} + +nsresult +HTMLEditor::RelativeFontChangeOnTextNode(FontSize aDir, + Text& aTextNode, + int32_t aStartOffset, + int32_t aEndOffset) +{ + // Don't need to do anything if no characters actually selected + if (aStartOffset == aEndOffset) { + return NS_OK; + } + + if (!aTextNode.GetParentNode() || + !CanContainTag(*aTextNode.GetParentNode(), *nsGkAtoms::big)) { + return NS_OK; + } + + OwningNonNull<nsIContent> node = aTextNode; + + // Do we need to split the text node? + + // -1 is a magic value meaning to the end of node + if (aEndOffset == -1) { + aEndOffset = aTextNode.Length(); + } + + ErrorResult rv; + if ((uint32_t)aEndOffset != aTextNode.Length()) { + // We need to split off back of text node + node = SplitNode(node, aEndOffset, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + } + if (aStartOffset) { + // We need to split off front of text node + SplitNode(node, aStartOffset, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + } + + // Look for siblings that are correct type of node + nsIAtom* nodeType = aDir == FontSize::incr ? nsGkAtoms::big + : nsGkAtoms::small; + nsCOMPtr<nsIContent> sibling = GetPriorHTMLSibling(node); + if (sibling && sibling->IsHTMLElement(nodeType)) { + // Previous sib is already right kind of inline node; slide this over + nsresult rv = MoveNode(node, sibling, -1); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + sibling = GetNextHTMLSibling(node); + if (sibling && sibling->IsHTMLElement(nodeType)) { + // Following sib is already right kind of inline node; slide this over + nsresult rv = MoveNode(node, sibling, 0); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + // Else reparent the node inside font node with appropriate relative size + nsCOMPtr<Element> newElement = InsertContainerAbove(node, nodeType); + NS_ENSURE_STATE(newElement); + + return NS_OK; +} + +nsresult +HTMLEditor::RelativeFontChangeHelper(int32_t aSizeChange, + nsINode* aNode) +{ + MOZ_ASSERT(aNode); + + /* This routine looks for all the font nodes in the tree rooted by aNode, + including aNode itself, looking for font nodes that have the size attr + set. Any such nodes need to have big or small put inside them, since + they override any big/small that are above them. + */ + + // Can only change font size by + or - 1 + if (aSizeChange != 1 && aSizeChange != -1) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // If this is a font node with size, put big/small inside it. + if (aNode->IsHTMLElement(nsGkAtoms::font) && + aNode->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::size)) { + // Cycle through children and adjust relative font size. + for (uint32_t i = aNode->GetChildCount(); i--; ) { + nsresult rv = RelativeFontChangeOnNode(aSizeChange, aNode->GetChildAt(i)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // RelativeFontChangeOnNode already calls us recursively, + // so we don't need to check our children again. + return NS_OK; + } + + // Otherwise cycle through the children. + for (uint32_t i = aNode->GetChildCount(); i--; ) { + nsresult rv = RelativeFontChangeHelper(aSizeChange, aNode->GetChildAt(i)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +HTMLEditor::RelativeFontChangeOnNode(int32_t aSizeChange, + nsIContent* aNode) +{ + MOZ_ASSERT(aNode); + // Can only change font size by + or - 1 + if (aSizeChange != 1 && aSizeChange != -1) { + return NS_ERROR_ILLEGAL_VALUE; + } + + nsIAtom* atom; + if (aSizeChange == 1) { + atom = nsGkAtoms::big; + } else { + atom = nsGkAtoms::small; + } + + // Is it the opposite of what we want? + if ((aSizeChange == 1 && aNode->IsHTMLElement(nsGkAtoms::small)) || + (aSizeChange == -1 && aNode->IsHTMLElement(nsGkAtoms::big))) { + // first populate any nested font tags that have the size attr set + nsresult rv = RelativeFontChangeHelper(aSizeChange, aNode); + NS_ENSURE_SUCCESS(rv, rv); + // in that case, just remove this node and pull up the children + return RemoveContainer(aNode); + } + + // can it be put inside a "big" or "small"? + if (TagCanContain(*atom, *aNode)) { + // first populate any nested font tags that have the size attr set + nsresult rv = RelativeFontChangeHelper(aSizeChange, aNode); + NS_ENSURE_SUCCESS(rv, rv); + + // ok, chuck it in. + // first look at siblings of aNode for matching bigs or smalls. + // if we find one, move aNode into it. + nsIContent* sibling = GetPriorHTMLSibling(aNode); + if (sibling && sibling->IsHTMLElement(atom)) { + // previous sib is already right kind of inline node; slide this over into it + return MoveNode(aNode, sibling, -1); + } + + sibling = GetNextHTMLSibling(aNode); + if (sibling && sibling->IsHTMLElement(atom)) { + // following sib is already right kind of inline node; slide this over into it + return MoveNode(aNode, sibling, 0); + } + + // else insert it above aNode + nsCOMPtr<Element> newElement = InsertContainerAbove(aNode, atom); + NS_ENSURE_STATE(newElement); + + return NS_OK; + } + + // none of the above? then cycle through the children. + // MOOSE: we should group the children together if possible + // into a single "big" or "small". For the moment they are + // each getting their own. + for (uint32_t i = aNode->GetChildCount(); i--; ) { + nsresult rv = RelativeFontChangeOnNode(aSizeChange, aNode->GetChildAt(i)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetFontFaceState(bool* aMixed, + nsAString& outFace) +{ + NS_ENSURE_TRUE(aMixed, NS_ERROR_FAILURE); + *aMixed = true; + outFace.Truncate(); + + bool first, any, all; + + NS_NAMED_LITERAL_STRING(attr, "face"); + nsresult rv = + GetInlinePropertyBase(*nsGkAtoms::font, &attr, nullptr, &first, &any, + &all, &outFace); + NS_ENSURE_SUCCESS(rv, rv); + if (any && !all) { + return NS_OK; // mixed + } + if (all) { + *aMixed = false; + return NS_OK; + } + + // if there is no font face, check for tt + rv = GetInlinePropertyBase(*nsGkAtoms::tt, nullptr, nullptr, &first, &any, + &all,nullptr); + NS_ENSURE_SUCCESS(rv, rv); + if (any && !all) { + return rv; // mixed + } + if (all) { + *aMixed = false; + outFace.AssignLiteral("tt"); + } + + if (!any) { + // there was no font face attrs of any kind. We are in normal font. + outFace.Truncate(); + *aMixed = false; + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetFontColorState(bool* aMixed, + nsAString& aOutColor) +{ + NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER); + *aMixed = true; + aOutColor.Truncate(); + + NS_NAMED_LITERAL_STRING(colorStr, "color"); + bool first, any, all; + + nsresult rv = + GetInlinePropertyBase(*nsGkAtoms::font, &colorStr, nullptr, &first, + &any, &all, &aOutColor); + NS_ENSURE_SUCCESS(rv, rv); + if (any && !all) { + return NS_OK; // mixed + } + if (all) { + *aMixed = false; + return NS_OK; + } + + if (!any) { + // there was no font color attrs of any kind.. + aOutColor.Truncate(); + *aMixed = false; + } + return NS_OK; +} + +// the return value is true only if the instance of the HTML editor we created +// can handle CSS styles (for instance, Composer can, Messenger can't) and if +// the CSS preference is checked +nsresult +HTMLEditor::GetIsCSSEnabled(bool* aIsCSSEnabled) +{ + *aIsCSSEnabled = IsCSSEnabled(); + return NS_OK; +} + +static bool +HasNonEmptyAttribute(Element* aElement, + nsIAtom* aName) +{ + MOZ_ASSERT(aElement); + + nsAutoString value; + return aElement->GetAttr(kNameSpaceID_None, aName, value) && !value.IsEmpty(); +} + +bool +HTMLEditor::HasStyleOrIdOrClass(Element* aElement) +{ + MOZ_ASSERT(aElement); + + // remove the node if its style attribute is empty or absent, + // and if it does not have a class nor an id + return HasNonEmptyAttribute(aElement, nsGkAtoms::style) || + HasNonEmptyAttribute(aElement, nsGkAtoms::_class) || + HasNonEmptyAttribute(aElement, nsGkAtoms::id); +} + +nsresult +HTMLEditor::RemoveElementIfNoStyleOrIdOrClass(Element& aElement) +{ + // early way out if node is not the right kind of element + if ((!aElement.IsHTMLElement(nsGkAtoms::span) && + !aElement.IsHTMLElement(nsGkAtoms::font)) || + HasStyleOrIdOrClass(&aElement)) { + return NS_OK; + } + + return RemoveContainer(&aElement); +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLTableEditor.cpp b/editor/libeditor/HTMLTableEditor.cpp new file mode 100644 index 000000000..3da0cfe0c --- /dev/null +++ b/editor/libeditor/HTMLTableEditor.cpp @@ -0,0 +1,3458 @@ +/* -*- 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 <stdio.h> + +#include "mozilla/HTMLEditor.h" + +#include "HTMLEditUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Element.h" +#include "nsAString.h" +#include "nsAlgorithm.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsIDOMElement.h" +#include "nsIDOMNode.h" +#include "nsIEditor.h" +#include "nsIFrame.h" +#include "nsINode.h" +#include "nsIPresShell.h" +#include "nsISupportsUtils.h" +#include "nsITableCellLayout.h" // For efficient access to table cell +#include "nsITableEditor.h" +#include "nsLiteralString.h" +#include "nsQueryFrame.h" +#include "nsRange.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsTableCellFrame.h" +#include "nsTableWrapperFrame.h" +#include "nscore.h" +#include <algorithm> + +namespace mozilla { + +using namespace dom; + +/** + * Stack based helper class for restoring selection after table edit. + */ +class MOZ_STACK_CLASS AutoSelectionSetterAfterTableEdit final +{ +private: + nsCOMPtr<nsITableEditor> mTableEditor; + nsCOMPtr<nsIDOMElement> mTable; + int32_t mCol, mRow, mDirection, mSelected; + +public: + AutoSelectionSetterAfterTableEdit(nsITableEditor* aTableEditor, + nsIDOMElement* aTable, + int32_t aRow, + int32_t aCol, + int32_t aDirection, + bool aSelected) + : mTableEditor(aTableEditor) + , mTable(aTable) + , mCol(aCol) + , mRow(aRow) + , mDirection(aDirection) + , mSelected(aSelected) + { + } + + ~AutoSelectionSetterAfterTableEdit() + { + if (mTableEditor) { + mTableEditor->SetSelectionAfterTableEdit(mTable, mRow, mCol, mDirection, + mSelected); + } + } + + // This is needed to abort the caret reset in the destructor + // when one method yields control to another + void CancelSetCaret() + { + mTableEditor = nullptr; + mTable = nullptr; + } +}; + +NS_IMETHODIMP +HTMLEditor::InsertCell(nsIDOMElement* aCell, + int32_t aRowSpan, + int32_t aColSpan, + bool aAfter, + bool aIsHeader, + nsIDOMElement** aNewCell) +{ + NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER); + if (aNewCell) { + *aNewCell = nullptr; + } + + // And the parent and offsets needed to do an insert + nsCOMPtr<nsIDOMNode> cellParent; + nsresult rv = aCell->GetParentNode(getter_AddRefs(cellParent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(cellParent, NS_ERROR_NULL_POINTER); + + int32_t cellOffset = GetChildOffset(aCell, cellParent); + + nsCOMPtr<nsIDOMElement> newCell; + rv = CreateElementWithDefaults(aIsHeader ? NS_LITERAL_STRING("th") : + NS_LITERAL_STRING("tb"), + getter_AddRefs(newCell)); + if (NS_FAILED(rv)) { + return rv; + } + if (!newCell) { + return NS_ERROR_FAILURE; + } + + //Optional: return new cell created + if (aNewCell) { + *aNewCell = newCell.get(); + NS_ADDREF(*aNewCell); + } + + if (aRowSpan > 1) { + // Note: Do NOT use editor transaction for this + nsAutoString newRowSpan; + newRowSpan.AppendInt(aRowSpan, 10); + newCell->SetAttribute(NS_LITERAL_STRING("rowspan"), newRowSpan); + } + if (aColSpan > 1) { + // Note: Do NOT use editor transaction for this + nsAutoString newColSpan; + newColSpan.AppendInt(aColSpan, 10); + newCell->SetAttribute(NS_LITERAL_STRING("colspan"), newColSpan); + } + if (aAfter) { + cellOffset++; + } + + //Don't let Rules System change the selection + AutoTransactionsConserveSelection dontChangeSelection(this); + return InsertNode(newCell, cellParent, cellOffset); +} + +NS_IMETHODIMP +HTMLEditor::SetColSpan(nsIDOMElement* aCell, + int32_t aColSpan) +{ + NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER); + nsAutoString newSpan; + newSpan.AppendInt(aColSpan, 10); + return SetAttribute(aCell, NS_LITERAL_STRING("colspan"), newSpan); +} + +NS_IMETHODIMP +HTMLEditor::SetRowSpan(nsIDOMElement* aCell, + int32_t aRowSpan) +{ + NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER); + nsAutoString newSpan; + newSpan.AppendInt(aRowSpan, 10); + return SetAttribute(aCell, NS_LITERAL_STRING("rowspan"), newSpan); +} + +NS_IMETHODIMP +HTMLEditor::InsertTableCell(int32_t aNumber, + bool aAfter) +{ + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> curCell; + nsCOMPtr<nsIDOMNode> cellParent; + int32_t cellOffset, startRowIndex, startColIndex; + nsresult rv = GetCellContext(nullptr, + getter_AddRefs(table), + getter_AddRefs(curCell), + getter_AddRefs(cellParent), &cellOffset, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if no cell found + NS_ENSURE_TRUE(curCell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + // Get more data for current cell in row we are inserting at (we need COLSPAN) + int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + rv = GetCellDataAt(table, startRowIndex, startColIndex, + getter_AddRefs(curCell), + &curStartRowIndex, &curStartColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(curCell, NS_ERROR_FAILURE); + int32_t newCellIndex = aAfter ? (startColIndex+colSpan) : startColIndex; + //We control selection resetting after the insert... + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + newCellIndex, ePreviousColumn, + false); + //...so suppress Rules System selection munging + AutoTransactionsConserveSelection dontChangeSelection(this); + + for (int32_t i = 0; i < aNumber; i++) { + nsCOMPtr<nsIDOMElement> newCell; + rv = CreateElementWithDefaults(NS_LITERAL_STRING("td"), + getter_AddRefs(newCell)); + if (NS_SUCCEEDED(rv) && newCell) { + if (aAfter) { + cellOffset++; + } + rv = InsertNode(newCell, cellParent, cellOffset); + if (NS_FAILED(rv)) { + break; + } + } + } + // XXX This is perhaps the result of the last call of InsertNode() or + // CreateElementWithDefaults(). + return rv; +} + +NS_IMETHODIMP +HTMLEditor::GetFirstRow(nsIDOMElement* aTableElement, + nsIDOMNode** aRowNode) +{ + NS_ENSURE_TRUE(aRowNode, NS_ERROR_NULL_POINTER); + + *aRowNode = nullptr; + + NS_ENSURE_TRUE(aTableElement, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMElement> tableElement; + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), + aTableElement, + getter_AddRefs(tableElement)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(tableElement, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMNode> tableChild; + rv = tableElement->GetFirstChild(getter_AddRefs(tableChild)); + NS_ENSURE_SUCCESS(rv, rv); + + while (tableChild) { + nsCOMPtr<nsIContent> content = do_QueryInterface(tableChild); + if (content) { + if (content->IsHTMLElement(nsGkAtoms::tr)) { + // Found a row directly under <table> + *aRowNode = tableChild; + NS_ADDREF(*aRowNode); + return NS_OK; + } + // Look for row in one of the row container elements + if (content->IsAnyOfHTMLElements(nsGkAtoms::tbody, + nsGkAtoms::thead, + nsGkAtoms::tfoot)) { + nsCOMPtr<nsIDOMNode> rowNode; + rv = tableChild->GetFirstChild(getter_AddRefs(rowNode)); + NS_ENSURE_SUCCESS(rv, rv); + + // We can encounter textnodes here -- must find a row + while (rowNode && !HTMLEditUtils::IsTableRow(rowNode)) { + nsCOMPtr<nsIDOMNode> nextNode; + rv = rowNode->GetNextSibling(getter_AddRefs(nextNode)); + NS_ENSURE_SUCCESS(rv, rv); + + rowNode = nextNode; + } + if (rowNode) { + *aRowNode = rowNode.get(); + NS_ADDREF(*aRowNode); + return NS_OK; + } + } + } + // Here if table child was a CAPTION or COLGROUP + // or child of a row parent wasn't a row (bad HTML?), + // or first child was a textnode + // Look in next table child + nsCOMPtr<nsIDOMNode> nextChild; + rv = tableChild->GetNextSibling(getter_AddRefs(nextChild)); + NS_ENSURE_SUCCESS(rv, rv); + + tableChild = nextChild; + } + // If here, row was not found + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; +} + +NS_IMETHODIMP +HTMLEditor::GetNextRow(nsIDOMNode* aCurrentRowNode, + nsIDOMNode** aRowNode) +{ + NS_ENSURE_TRUE(aRowNode, NS_ERROR_NULL_POINTER); + + *aRowNode = nullptr; + + NS_ENSURE_TRUE(aCurrentRowNode, NS_ERROR_NULL_POINTER); + + if (!HTMLEditUtils::IsTableRow(aCurrentRowNode)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMNode> nextRow; + nsresult rv = aCurrentRowNode->GetNextSibling(getter_AddRefs(nextRow)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> nextNode; + + // Skip over any textnodes here + while (nextRow && !HTMLEditUtils::IsTableRow(nextRow)) { + rv = nextRow->GetNextSibling(getter_AddRefs(nextNode)); + NS_ENSURE_SUCCESS(rv, rv); + + nextRow = nextNode; + } + if (nextRow) { + *aRowNode = nextRow.get(); + NS_ADDREF(*aRowNode); + return NS_OK; + } + + // No row found, search for rows in other table sections + nsCOMPtr<nsIDOMNode> rowParent; + rv = aCurrentRowNode->GetParentNode(getter_AddRefs(rowParent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(rowParent, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMNode> parentSibling; + rv = rowParent->GetNextSibling(getter_AddRefs(parentSibling)); + NS_ENSURE_SUCCESS(rv, rv); + + while (parentSibling) { + rv = parentSibling->GetFirstChild(getter_AddRefs(nextRow)); + NS_ENSURE_SUCCESS(rv, rv); + + // We can encounter textnodes here -- must find a row + while (nextRow && !HTMLEditUtils::IsTableRow(nextRow)) { + rv = nextRow->GetNextSibling(getter_AddRefs(nextNode)); + NS_ENSURE_SUCCESS(rv, rv); + + nextRow = nextNode; + } + if (nextRow) { + *aRowNode = nextRow.get(); + NS_ADDREF(*aRowNode); + return NS_OK; + } + + // We arrive here only if a table section has no children + // or first child of section is not a row (bad HTML or more "_moz_text" nodes!) + // So look for another section sibling + rv = parentSibling->GetNextSibling(getter_AddRefs(nextNode)); + NS_ENSURE_SUCCESS(rv, rv); + + parentSibling = nextNode; + } + // If here, row was not found + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; +} + +nsresult +HTMLEditor::GetLastCellInRow(nsIDOMNode* aRowNode, + nsIDOMNode** aCellNode) +{ + NS_ENSURE_TRUE(aCellNode, NS_ERROR_NULL_POINTER); + + *aCellNode = nullptr; + + NS_ENSURE_TRUE(aRowNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMNode> rowChild; + nsresult rv = aRowNode->GetLastChild(getter_AddRefs(rowChild)); + NS_ENSURE_SUCCESS(rv, rv); + + while (rowChild && !HTMLEditUtils::IsTableCell(rowChild)) { + // Skip over textnodes + nsCOMPtr<nsIDOMNode> previousChild; + rv = rowChild->GetPreviousSibling(getter_AddRefs(previousChild)); + NS_ENSURE_SUCCESS(rv, rv); + + rowChild = previousChild; + } + if (rowChild) { + *aCellNode = rowChild.get(); + NS_ADDREF(*aCellNode); + return NS_OK; + } + // If here, cell was not found + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; +} + +NS_IMETHODIMP +HTMLEditor::InsertTableColumn(int32_t aNumber, + bool aAfter) +{ + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> curCell; + int32_t startRowIndex, startColIndex; + nsresult rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + getter_AddRefs(curCell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if no cell found + NS_ENSURE_TRUE(curCell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + // Get more data for current cell (we need ROWSPAN) + int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + rv = GetCellDataAt(table, startRowIndex, startColIndex, + getter_AddRefs(curCell), + &curStartRowIndex, &curStartColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(curCell, NS_ERROR_FAILURE); + + AutoEditBatch beginBatching(this); + // Prevent auto insertion of BR in new cell until we're done + AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext); + + // Use column after current cell if requested + if (aAfter) { + startColIndex += actualColSpan; + //Detect when user is adding after a COLSPAN=0 case + // Assume they want to stop the "0" behavior and + // really add a new column. Thus we set the + // colspan to its true value + if (!colSpan) { + SetColSpan(curCell, actualColSpan); + } + } + + int32_t rowCount, colCount, rowIndex; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + //We reset caret in destructor... + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + startColIndex, ePreviousRow, + false); + //.. so suppress Rules System selection munging + AutoTransactionsConserveSelection dontChangeSelection(this); + + // If we are inserting after all existing columns + // Make sure table is "well formed" + // before appending new column + if (startColIndex >= colCount) { + NormalizeTable(table); + } + + nsCOMPtr<nsIDOMNode> rowNode; + for (rowIndex = 0; rowIndex < rowCount; rowIndex++) { + if (startColIndex < colCount) { + // We are inserting before an existing column + rv = GetCellDataAt(table, rowIndex, startColIndex, + getter_AddRefs(curCell), + &curStartRowIndex, &curStartColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't fail entire process if we fail to find a cell + // (may fail just in particular rows with < adequate cells per row) + if (curCell) { + if (curStartColIndex < startColIndex) { + // We have a cell spanning this location + // Simply increase its colspan to keep table rectangular + // Note: we do nothing if colsSpan=0, + // since it should automatically span the new column + if (colSpan > 0) { + SetColSpan(curCell, colSpan+aNumber); + } + } else { + // Simply set selection to the current cell + // so we can let InsertTableCell() do the work + // Insert a new cell before current one + selection->Collapse(curCell, 0); + rv = InsertTableCell(aNumber, false); + } + } + } else { + // Get current row and append new cells after last cell in row + if (!rowIndex) { + rv = GetFirstRow(table.get(), getter_AddRefs(rowNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsCOMPtr<nsIDOMNode> nextRow; + rv = GetNextRow(rowNode.get(), getter_AddRefs(nextRow)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rowNode = nextRow; + } + + if (rowNode) { + nsCOMPtr<nsIDOMNode> lastCell; + rv = GetLastCellInRow(rowNode, getter_AddRefs(lastCell)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(lastCell, NS_ERROR_FAILURE); + + curCell = do_QueryInterface(lastCell); + if (curCell) { + // Simply add same number of cells to each row + // Although tempted to check cell indexes for curCell, + // the effects of COLSPAN>1 in some cells makes this futile! + // We must use NormalizeTable first to assure + // that there are cells in each cellmap location + selection->Collapse(curCell, 0); + rv = InsertTableCell(aNumber, true); + } + } + } + } + // XXX This is perhaps the result of the last call of InsertTableCell(). + return rv; +} + +NS_IMETHODIMP +HTMLEditor::InsertTableRow(int32_t aNumber, + bool aAfter) +{ + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> curCell; + + int32_t startRowIndex, startColIndex; + nsresult rv = GetCellContext(nullptr, + getter_AddRefs(table), + getter_AddRefs(curCell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if no cell found + NS_ENSURE_TRUE(curCell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + // Get more data for current cell in row we are inserting at (we need COLSPAN) + int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + rv = GetCellDataAt(table, startRowIndex, startColIndex, + getter_AddRefs(curCell), + &curStartRowIndex, &curStartColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(curCell, NS_ERROR_FAILURE); + + int32_t rowCount, colCount; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + AutoEditBatch beginBatching(this); + // Prevent auto insertion of BR in new cell until we're done + AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext); + + if (aAfter) { + // Use row after current cell + startRowIndex += actualRowSpan; + + //Detect when user is adding after a ROWSPAN=0 case + // Assume they want to stop the "0" behavior and + // really add a new row. Thus we set the + // rowspan to its true value + if (!rowSpan) { + SetRowSpan(curCell, actualRowSpan); + } + } + + //We control selection resetting after the insert... + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + startColIndex, ePreviousColumn, + false); + //...so suppress Rules System selection munging + AutoTransactionsConserveSelection dontChangeSelection(this); + + nsCOMPtr<nsIDOMElement> cellForRowParent; + int32_t cellsInRow = 0; + if (startRowIndex < rowCount) { + // We are inserting above an existing row + // Get each cell in the insert row to adjust for COLSPAN effects while we + // count how many cells are needed + int32_t colIndex = 0; + while (NS_SUCCEEDED(GetCellDataAt(table, startRowIndex, colIndex, + getter_AddRefs(curCell), + &curStartRowIndex, &curStartColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, + &isSelected))) { + if (curCell) { + if (curStartRowIndex < startRowIndex) { + // We have a cell spanning this location + // Simply increase its rowspan + //Note that if rowSpan == 0, we do nothing, + // since that cell should automatically extend into the new row + if (rowSpan > 0) { + SetRowSpan(curCell, rowSpan+aNumber); + } + } else { + // We have a cell in the insert row + + // Count the number of cells we need to add to the new row + cellsInRow += actualColSpan; + + // Save cell we will use below + if (!cellForRowParent) { + cellForRowParent = curCell; + } + } + // Next cell in row + colIndex += actualColSpan; + } else { + colIndex++; + } + } + } else { + // We are adding a new row after all others + // If it weren't for colspan=0 effect, + // we could simply use colCount for number of new cells... + // XXX colspan=0 support has now been removed in table layout so maybe this can be cleaned up now? (bug 1243183) + cellsInRow = colCount; + + // ...but we must compensate for all cells with rowSpan = 0 in the last row + int32_t lastRow = rowCount-1; + int32_t tempColIndex = 0; + while (NS_SUCCEEDED(GetCellDataAt(table, lastRow, tempColIndex, + getter_AddRefs(curCell), + &curStartRowIndex, &curStartColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, + &isSelected))) { + if (!rowSpan) { + cellsInRow -= actualColSpan; + } + + tempColIndex += actualColSpan; + + // Save cell from the last row that we will use below + if (!cellForRowParent && curStartRowIndex == lastRow) { + cellForRowParent = curCell; + } + } + } + + if (cellsInRow > 0) { + // The row parent and offset where we will insert new row + nsCOMPtr<nsIDOMNode> parentOfRow; + int32_t newRowOffset; + + NS_NAMED_LITERAL_STRING(trStr, "tr"); + if (!cellForRowParent) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMElement> parentRow; + rv = GetElementOrParentByTagName(trStr, cellForRowParent, + getter_AddRefs(parentRow)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parentRow, NS_ERROR_NULL_POINTER); + + parentRow->GetParentNode(getter_AddRefs(parentOfRow)); + NS_ENSURE_TRUE(parentOfRow, NS_ERROR_NULL_POINTER); + + newRowOffset = GetChildOffset(parentRow, parentOfRow); + + // Adjust for when adding past the end + if (aAfter && startRowIndex >= rowCount) { + newRowOffset++; + } + + for (int32_t row = 0; row < aNumber; row++) { + // Create a new row + nsCOMPtr<nsIDOMElement> newRow; + rv = CreateElementWithDefaults(trStr, getter_AddRefs(newRow)); + if (NS_SUCCEEDED(rv)) { + NS_ENSURE_TRUE(newRow, NS_ERROR_FAILURE); + + for (int32_t i = 0; i < cellsInRow; i++) { + nsCOMPtr<nsIDOMElement> newCell; + rv = CreateElementWithDefaults(NS_LITERAL_STRING("td"), + getter_AddRefs(newCell)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(newCell, NS_ERROR_FAILURE); + + // Don't use transaction system yet! (not until entire row is inserted) + nsCOMPtr<nsIDOMNode>resultNode; + rv = newRow->AppendChild(newCell, getter_AddRefs(resultNode)); + NS_ENSURE_SUCCESS(rv, rv); + } + // Use transaction system to insert the entire row+cells + // (Note that rows are inserted at same childoffset each time) + rv = InsertNode(newRow, parentOfRow, newRowOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + // XXX This might be the result of the last call of + // CreateElementWithDefaults(), otherwise, NS_OK. + return rv; +} + +// Editor helper only +// XXX Code changed for bug 217717 and now we don't need aSelection param +// TODO: Remove aSelection param +nsresult +HTMLEditor::DeleteTable2(nsIDOMElement* aTable, + Selection* aSelection) +{ + NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER); + + // Select the table + nsresult rv = ClearSelection(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = AppendNodeToSelectionAsRange(aTable); + NS_ENSURE_SUCCESS(rv, rv); + + return DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip); +} + +NS_IMETHODIMP +HTMLEditor::DeleteTable() +{ + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> table; + nsresult rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + nullptr, nullptr, nullptr, nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + AutoEditBatch beginBatching(this); + return DeleteTable2(table, selection); +} + +NS_IMETHODIMP +HTMLEditor::DeleteTableCell(int32_t aNumber) +{ + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex; + + + nsresult rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + getter_AddRefs(cell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if we didn't find a table or cell + NS_ENSURE_TRUE(table && cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + AutoEditBatch beginBatching(this); + // Prevent rules testing until we're done + AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext); + + nsCOMPtr<nsIDOMElement> firstCell; + nsCOMPtr<nsIDOMRange> range; + rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(firstCell)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t rangeCount; + rv = selection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (firstCell && rangeCount > 1) { + // When > 1 selected cell, + // ignore aNumber and use selected cells + cell = firstCell; + + int32_t rowCount, colCount; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Get indexes -- may be different than original cell + rv = GetCellIndexes(cell, &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + + // The setCaret object will call AutoSelectionSetterAfterTableEdit in its + // destructor + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + startColIndex, ePreviousColumn, + false); + AutoTransactionsConserveSelection dontChangeSelection(this); + + bool checkToDeleteRow = true; + bool checkToDeleteColumn = true; + while (cell) { + bool deleteRow = false; + bool deleteCol = false; + + if (checkToDeleteRow) { + // Optimize to delete an entire row + // Clear so we don't repeat AllCellsInRowSelected within the same row + checkToDeleteRow = false; + + deleteRow = AllCellsInRowSelected(table, startRowIndex, colCount); + if (deleteRow) { + // First, find the next cell in a different row + // to continue after we delete this row + int32_t nextRow = startRowIndex; + while (nextRow == startRowIndex) { + rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + if (!cell) { + break; + } + rv = GetCellIndexes(cell, &nextRow, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + // Delete entire row + rv = DeleteRow(table, startRowIndex); + NS_ENSURE_SUCCESS(rv, rv); + + if (cell) { + // For the next cell: Subtract 1 for row we deleted + startRowIndex = nextRow - 1; + // Set true since we know we will look at a new row next + checkToDeleteRow = true; + } + } + } + if (!deleteRow) { + if (checkToDeleteColumn) { + // Optimize to delete an entire column + // Clear this so we don't repeat AllCellsInColSelected within the same Col + checkToDeleteColumn = false; + + deleteCol = AllCellsInColumnSelected(table, startColIndex, colCount); + if (deleteCol) { + // First, find the next cell in a different column + // to continue after we delete this column + int32_t nextCol = startColIndex; + while (nextCol == startColIndex) { + rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + if (!cell) { + break; + } + rv = GetCellIndexes(cell, &startRowIndex, &nextCol); + NS_ENSURE_SUCCESS(rv, rv); + } + // Delete entire Col + rv = DeleteColumn(table, startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + if (cell) { + // For the next cell, subtract 1 for col. deleted + startColIndex = nextCol - 1; + // Set true since we know we will look at a new column next + checkToDeleteColumn = true; + } + } + } + if (!deleteCol) { + // First get the next cell to delete + nsCOMPtr<nsIDOMElement> nextCell; + rv = GetNextSelectedCell(getter_AddRefs(range), + getter_AddRefs(nextCell)); + NS_ENSURE_SUCCESS(rv, rv); + + // Then delete the cell + rv = DeleteNode(cell); + NS_ENSURE_SUCCESS(rv, rv); + + // The next cell to delete + cell = nextCell; + if (cell) { + rv = GetCellIndexes(cell, &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + } + } else { + for (int32_t i = 0; i < aNumber; i++) { + rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + getter_AddRefs(cell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if no cell found + NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + if (GetNumberOfCellsInRow(table, startRowIndex) == 1) { + nsCOMPtr<nsIDOMElement> parentRow; + rv = GetElementOrParentByTagName(NS_LITERAL_STRING("tr"), cell, + getter_AddRefs(parentRow)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parentRow, NS_ERROR_NULL_POINTER); + + // We should delete the row instead, + // but first check if its the only row left + // so we can delete the entire table + int32_t rowCount, colCount; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (rowCount == 1) { + return DeleteTable2(table, selection); + } + + // We need to call DeleteTableRow to handle cells with rowspan + rv = DeleteTableRow(1); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // More than 1 cell in the row + + // The setCaret object will call AutoSelectionSetterAfterTableEdit in its + // destructor + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + startColIndex, ePreviousColumn, + false); + AutoTransactionsConserveSelection dontChangeSelection(this); + + rv = DeleteNode(cell); + // If we fail, don't try to delete any more cells??? + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::DeleteTableCellContents() +{ + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex; + nsresult rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + getter_AddRefs(cell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if no cell found + NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + + AutoEditBatch beginBatching(this); + // Prevent rules testing until we're done + AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext); + //Don't let Rules System change the selection + AutoTransactionsConserveSelection dontChangeSelection(this); + + + nsCOMPtr<nsIDOMElement> firstCell; + nsCOMPtr<nsIDOMRange> range; + rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(firstCell)); + NS_ENSURE_SUCCESS(rv, rv); + + + if (firstCell) { + cell = firstCell; + rv = GetCellIndexes(cell, &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + startColIndex, ePreviousColumn, + false); + + while (cell) { + DeleteCellContents(cell); + if (firstCell) { + // We doing a selected cells, so do all of them + rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + } else { + cell = nullptr; + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::DeleteCellContents(nsIDOMElement* aCell) +{ + NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER); + + // Prevent rules testing until we're done + AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext); + + nsCOMPtr<nsIDOMNode> child; + bool hasChild; + aCell->HasChildNodes(&hasChild); + + while (hasChild) { + aCell->GetLastChild(getter_AddRefs(child)); + nsresult rv = DeleteNode(child); + NS_ENSURE_SUCCESS(rv, rv); + aCell->HasChildNodes(&hasChild); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::DeleteTableColumn(int32_t aNumber) +{ + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex, rowCount, colCount; + nsresult rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + getter_AddRefs(cell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if no cell found + NS_ENSURE_TRUE(table && cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Shortcut the case of deleting all columns in table + if (!startColIndex && aNumber >= colCount) { + return DeleteTable2(table, selection); + } + + // Check for counts too high + aNumber = std::min(aNumber,(colCount-startColIndex)); + + AutoEditBatch beginBatching(this); + // Prevent rules testing until we're done + AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext); + + // Test if deletion is controlled by selected cells + nsCOMPtr<nsIDOMElement> firstCell; + nsCOMPtr<nsIDOMRange> range; + rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(firstCell)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t rangeCount; + rv = selection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (firstCell && rangeCount > 1) { + // Fetch indexes again - may be different for selected cells + rv = GetCellIndexes(firstCell, &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + //We control selection resetting after the insert... + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + startColIndex, ePreviousRow, + false); + + if (firstCell && rangeCount > 1) { + // Use selected cells to determine what rows to delete + cell = firstCell; + + while (cell) { + if (cell != firstCell) { + rv = GetCellIndexes(cell, &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + // Find the next cell in a different column + // to continue after we delete this column + int32_t nextCol = startColIndex; + while (nextCol == startColIndex) { + rv = GetNextSelectedCell(getter_AddRefs(range), getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + if (!cell) { + break; + } + rv = GetCellIndexes(cell, &startRowIndex, &nextCol); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = DeleteColumn(table, startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + for (int32_t i = 0; i < aNumber; i++) { + rv = DeleteColumn(table, startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::DeleteColumn(nsIDOMElement* aTable, + int32_t aColIndex) +{ + NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + int32_t rowIndex = 0; + + do { + nsresult rv = + GetCellDataAt(aTable, rowIndex, aColIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, rv); + + if (cell) { + // Find cells that don't start in column we are deleting + if (startColIndex < aColIndex || colSpan > 1 || !colSpan) { + // We have a cell spanning this location + // Decrease its colspan to keep table rectangular, + // but if colSpan=0, it will adjust automatically + if (colSpan > 0) { + NS_ASSERTION((colSpan > 1),"Bad COLSPAN in DeleteTableColumn"); + SetColSpan(cell, colSpan-1); + } + if (startColIndex == aColIndex) { + // Cell is in column to be deleted, but must have colspan > 1, + // so delete contents of cell instead of cell itself + // (We must have reset colspan above) + DeleteCellContents(cell); + } + // To next cell in column + rowIndex += actualRowSpan; + } else { + // Delete the cell + if (GetNumberOfCellsInRow(aTable, rowIndex) == 1) { + // Only 1 cell in row - delete the row + nsCOMPtr<nsIDOMElement> parentRow; + rv = GetElementOrParentByTagName(NS_LITERAL_STRING("tr"), cell, + getter_AddRefs(parentRow)); + NS_ENSURE_SUCCESS(rv, rv); + if (!parentRow) { + return NS_ERROR_NULL_POINTER; + } + + // But first check if its the only row left + // so we can delete the entire table + // (This should never happen but it's the safe thing to do) + int32_t rowCount, colCount; + rv = GetTableSize(aTable, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (rowCount == 1) { + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + return DeleteTable2(aTable, selection); + } + + // Delete the row by placing caret in cell we were to delete + // We need to call DeleteTableRow to handle cells with rowspan + rv = DeleteRow(aTable, startRowIndex); + NS_ENSURE_SUCCESS(rv, rv); + + // Note that we don't incremenet rowIndex + // since a row was deleted and "next" + // row now has current rowIndex + } else { + // A more "normal" deletion + rv = DeleteNode(cell); + NS_ENSURE_SUCCESS(rv, rv); + + //Skip over any rows spanned by this cell + rowIndex += actualRowSpan; + } + } + } + } while (cell); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::DeleteTableRow(int32_t aNumber) +{ + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex; + int32_t rowCount, colCount; + nsresult rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + getter_AddRefs(cell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if no cell found + NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Shortcut the case of deleting all rows in table + if (!startRowIndex && aNumber >= rowCount) { + return DeleteTable2(table, selection); + } + + AutoEditBatch beginBatching(this); + // Prevent rules testing until we're done + AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext); + + nsCOMPtr<nsIDOMElement> firstCell; + nsCOMPtr<nsIDOMRange> range; + rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(firstCell)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t rangeCount; + rv = selection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (firstCell && rangeCount > 1) { + // Fetch indexes again - may be different for selected cells + rv = GetCellIndexes(firstCell, &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + + //We control selection resetting after the insert... + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + startColIndex, ePreviousRow, + false); + // Don't change selection during deletions + AutoTransactionsConserveSelection dontChangeSelection(this); + + if (firstCell && rangeCount > 1) { + // Use selected cells to determine what rows to delete + cell = firstCell; + + while (cell) { + if (cell != firstCell) { + rv = GetCellIndexes(cell, &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + // Find the next cell in a different row + // to continue after we delete this row + int32_t nextRow = startRowIndex; + while (nextRow == startRowIndex) { + rv = GetNextSelectedCell(getter_AddRefs(range), getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + if (!cell) break; + rv = GetCellIndexes(cell, &nextRow, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + // Delete entire row + rv = DeleteRow(table, startRowIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Check for counts too high + aNumber = std::min(aNumber,(rowCount-startRowIndex)); + for (int32_t i = 0; i < aNumber; i++) { + rv = DeleteRow(table, startRowIndex); + // If failed in current row, try the next + if (NS_FAILED(rv)) { + startRowIndex++; + } + + // Check if there's a cell in the "next" row + rv = GetCellAt(table, startRowIndex, startColIndex, getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + if (!cell) { + break; + } + } + } + return NS_OK; +} + +// Helper that doesn't batch or change the selection +NS_IMETHODIMP +HTMLEditor::DeleteRow(nsIDOMElement* aTable, + int32_t aRowIndex) +{ + NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMElement> cell; + nsCOMPtr<nsIDOMElement> cellInDeleteRow; + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + int32_t colIndex = 0; + + // Prevent rules testing until we're done + AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext); + + // The list of cells we will change rowspan in + // and the new rowspan values for each + nsTArray<nsCOMPtr<nsIDOMElement> > spanCellList; + nsTArray<int32_t> newSpanList; + + int32_t rowCount, colCount; + nsresult rv = GetTableSize(aTable, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Scan through cells in row to do rowspan adjustments + // Note that after we delete row, startRowIndex will point to the + // cells in the next row to be deleted + do { + if (aRowIndex >= rowCount || colIndex >= colCount) { + break; + } + + rv = GetCellDataAt(aTable, aRowIndex, colIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + // We don't fail if we don't find a cell, so this must be real bad + if (NS_FAILED(rv)) { + return rv; + } + + // Compensate for cells that don't start or extend below the row we are deleting + if (cell) { + if (startRowIndex < aRowIndex) { + // Cell starts in row above us + // Decrease its rowspan to keep table rectangular + // but we don't need to do this if rowspan=0, + // since it will automatically adjust + if (rowSpan > 0) { + // Build list of cells to change rowspan + // We can't do it now since it upsets cell map, + // so we will do it after deleting the row + spanCellList.AppendElement(cell); + newSpanList.AppendElement(std::max((aRowIndex - startRowIndex), actualRowSpan-1)); + } + } else { + if (rowSpan > 1) { + // Cell spans below row to delete, so we must insert new cells to + // keep rows below. Note that we test "rowSpan" so we don't do this + // if rowSpan = 0 (automatic readjustment). + int32_t aboveRowToInsertNewCellInto = aRowIndex - startRowIndex + 1; + int32_t numOfRawSpanRemainingBelow = actualRowSpan - 1; + rv = SplitCellIntoRows(aTable, startRowIndex, startColIndex, + aboveRowToInsertNewCellInto, + numOfRawSpanRemainingBelow, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + if (!cellInDeleteRow) { + cellInDeleteRow = cell; // Reference cell to find row to delete + } + } + // Skip over other columns spanned by this cell + colIndex += actualColSpan; + } + } while (cell); + + // Things are messed up if we didn't find a cell in the row! + NS_ENSURE_TRUE(cellInDeleteRow, NS_ERROR_FAILURE); + + // Delete the entire row + nsCOMPtr<nsIDOMElement> parentRow; + rv = GetElementOrParentByTagName(NS_LITERAL_STRING("tr"), cellInDeleteRow, + getter_AddRefs(parentRow)); + NS_ENSURE_SUCCESS(rv, rv); + + if (parentRow) { + rv = DeleteNode(parentRow); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Now we can set new rowspans for cells stored above + for (uint32_t i = 0, n = spanCellList.Length(); i < n; i++) { + nsIDOMElement *cellPtr = spanCellList[i]; + if (cellPtr) { + rv = SetRowSpan(cellPtr, newSpanList[i]); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + + +NS_IMETHODIMP +HTMLEditor::SelectTable() +{ + nsCOMPtr<nsIDOMElement> table; + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), nullptr, + getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if we didn't find a table + NS_ENSURE_TRUE(table, NS_OK); + + rv = ClearSelection(); + if (NS_FAILED(rv)) { + return rv; + } + return AppendNodeToSelectionAsRange(table); +} + +NS_IMETHODIMP +HTMLEditor::SelectTableCell() +{ + nsCOMPtr<nsIDOMElement> cell; + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr, + getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + rv = ClearSelection(); + if (NS_FAILED(rv)) { + return rv; + } + return AppendNodeToSelectionAsRange(cell); +} + +NS_IMETHODIMP +HTMLEditor::SelectBlockOfCells(nsIDOMElement* aStartCell, + nsIDOMElement* aEndCell) +{ + NS_ENSURE_TRUE(aStartCell && aEndCell, NS_ERROR_NULL_POINTER); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + NS_NAMED_LITERAL_STRING(tableStr, "table"); + nsCOMPtr<nsIDOMElement> table; + nsresult rv = GetElementOrParentByTagName(tableStr, aStartCell, + getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(table, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMElement> endTable; + rv = GetElementOrParentByTagName(tableStr, aEndCell, + getter_AddRefs(endTable)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(endTable, NS_ERROR_FAILURE); + + // We can only select a block if within the same table, + // so do nothing if not within one table + if (table != endTable) { + return NS_OK; + } + + int32_t startRowIndex, startColIndex, endRowIndex, endColIndex; + + // Get starting and ending cells' location in the cellmap + rv = GetCellIndexes(aStartCell, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + return rv; + } + + rv = GetCellIndexes(aEndCell, &endRowIndex, &endColIndex); + if (NS_FAILED(rv)) { + return rv; + } + + // Suppress nsISelectionListener notification + // until all selection changes are finished + SelectionBatcher selectionBatcher(selection); + + // Examine all cell nodes in current selection and + // remove those outside the new block cell region + int32_t minColumn = std::min(startColIndex, endColIndex); + int32_t minRow = std::min(startRowIndex, endRowIndex); + int32_t maxColumn = std::max(startColIndex, endColIndex); + int32_t maxRow = std::max(startRowIndex, endRowIndex); + + nsCOMPtr<nsIDOMElement> cell; + int32_t currentRowIndex, currentColIndex; + nsCOMPtr<nsIDOMRange> range; + rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + if (rv == NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND) { + return NS_OK; + } + + while (cell) { + rv = GetCellIndexes(cell, ¤tRowIndex, ¤tColIndex); + NS_ENSURE_SUCCESS(rv, rv); + + if (currentRowIndex < maxRow || currentRowIndex > maxRow || + currentColIndex < maxColumn || currentColIndex > maxColumn) { + selection->RemoveRange(range); + // Since we've removed the range, decrement pointer to next range + mSelectedCellIndex--; + } + rv = GetNextSelectedCell(getter_AddRefs(range), getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + } + + int32_t rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + for (int32_t row = minRow; row <= maxRow; row++) { + for (int32_t col = minColumn; col <= maxColumn; + col += std::max(actualColSpan, 1)) { + rv = GetCellDataAt(table, row, col, getter_AddRefs(cell), + ¤tRowIndex, ¤tColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + if (NS_FAILED(rv)) { + break; + } + // Skip cells that already selected or are spanned from previous locations + if (!isSelected && cell && + row == currentRowIndex && col == currentColIndex) { + rv = AppendNodeToSelectionAsRange(cell); + if (NS_FAILED(rv)) { + break; + } + } + } + } + // NS_OK, otherwise, the last failure of GetCellDataAt() or + // AppendNodeToSelectionAsRange(). + return rv; +} + +NS_IMETHODIMP +HTMLEditor::SelectAllTableCells() +{ + nsCOMPtr<nsIDOMElement> cell; + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr, + getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't fail if we didn't find a cell + NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + nsCOMPtr<nsIDOMElement> startCell = cell; + + // Get parent table + nsCOMPtr<nsIDOMElement> table; + rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), cell, + getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + if (!table) { + return NS_ERROR_NULL_POINTER; + } + + int32_t rowCount, colCount; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + // Suppress nsISelectionListener notification + // until all selection changes are finished + SelectionBatcher selectionBatcher(selection); + + // It is now safe to clear the selection + // BE SURE TO RESET IT BEFORE LEAVING! + rv = ClearSelection(); + + // Select all cells in the same column as current cell + bool cellSelected = false; + int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex; + bool isSelected; + for (int32_t row = 0; row < rowCount; row++) { + for (int32_t col = 0; col < colCount; col += std::max(actualColSpan, 1)) { + rv = GetCellDataAt(table, row, col, getter_AddRefs(cell), + ¤tRowIndex, ¤tColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + if (NS_FAILED(rv)) { + break; + } + // Skip cells that are spanned from previous rows or columns + if (cell && row == currentRowIndex && col == currentColIndex) { + rv = AppendNodeToSelectionAsRange(cell); + if (NS_FAILED(rv)) { + break; + } + cellSelected = true; + } + } + } + // Safety code to select starting cell if nothing else was selected + if (!cellSelected) { + return AppendNodeToSelectionAsRange(startCell); + } + // NS_OK, otherwise, the error of ClearSelection() when there is no column or + // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange(). + return rv; +} + +NS_IMETHODIMP +HTMLEditor::SelectTableRow() +{ + nsCOMPtr<nsIDOMElement> cell; + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr, + getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't fail if we didn't find a cell + NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + nsCOMPtr<nsIDOMElement> startCell = cell; + + // Get table and location of cell: + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> table; + int32_t startRowIndex, startColIndex; + + rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + getter_AddRefs(cell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(table, NS_ERROR_FAILURE); + + int32_t rowCount, colCount; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + //Note: At this point, we could get first and last cells in row, + // then call SelectBlockOfCells, but that would take just + // a little less code, so the following is more efficient + + // Suppress nsISelectionListener notification + // until all selection changes are finished + SelectionBatcher selectionBatcher(selection); + + // It is now safe to clear the selection + // BE SURE TO RESET IT BEFORE LEAVING! + rv = ClearSelection(); + + // Select all cells in the same row as current cell + bool cellSelected = false; + int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex; + bool isSelected; + for (int32_t col = 0; col < colCount; col += std::max(actualColSpan, 1)) { + rv = GetCellDataAt(table, startRowIndex, col, getter_AddRefs(cell), + ¤tRowIndex, ¤tColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + if (NS_FAILED(rv)) { + break; + } + // Skip cells that are spanned from previous rows or columns + if (cell && currentRowIndex == startRowIndex && currentColIndex == col) { + rv = AppendNodeToSelectionAsRange(cell); + if (NS_FAILED(rv)) { + break; + } + cellSelected = true; + } + } + // Safety code to select starting cell if nothing else was selected + if (!cellSelected) { + return AppendNodeToSelectionAsRange(startCell); + } + // NS_OK, otherwise, the error of ClearSelection() when there is no column or + // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange(). + return rv; +} + +NS_IMETHODIMP +HTMLEditor::SelectTableColumn() +{ + nsCOMPtr<nsIDOMElement> cell; + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr, + getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't fail if we didn't find a cell + NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + nsCOMPtr<nsIDOMElement> startCell = cell; + + // Get location of cell: + RefPtr<Selection> selection; + nsCOMPtr<nsIDOMElement> table; + int32_t startRowIndex, startColIndex; + + rv = GetCellContext(getter_AddRefs(selection), + getter_AddRefs(table), + getter_AddRefs(cell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(table, NS_ERROR_FAILURE); + + int32_t rowCount, colCount; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Suppress nsISelectionListener notification + // until all selection changes are finished + SelectionBatcher selectionBatcher(selection); + + // It is now safe to clear the selection + // BE SURE TO RESET IT BEFORE LEAVING! + rv = ClearSelection(); + + // Select all cells in the same column as current cell + bool cellSelected = false; + int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex; + bool isSelected; + for (int32_t row = 0; row < rowCount; row += std::max(actualRowSpan, 1)) { + rv = GetCellDataAt(table, row, startColIndex, getter_AddRefs(cell), + ¤tRowIndex, ¤tColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + if (NS_FAILED(rv)) { + break; + } + // Skip cells that are spanned from previous rows or columns + if (cell && currentRowIndex == row && currentColIndex == startColIndex) { + rv = AppendNodeToSelectionAsRange(cell); + if (NS_FAILED(rv)) { + break; + } + cellSelected = true; + } + } + // Safety code to select starting cell if nothing else was selected + if (!cellSelected) { + return AppendNodeToSelectionAsRange(startCell); + } + // NS_OK, otherwise, the error of ClearSelection() when there is no row or + // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange(). + return rv; +} + +NS_IMETHODIMP +HTMLEditor::SplitTableCell() +{ + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex, actualRowSpan, actualColSpan; + // Get cell, table, etc. at selection anchor node + nsresult rv = GetCellContext(nullptr, + getter_AddRefs(table), + getter_AddRefs(cell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + if (!table || !cell) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + // We need rowspan and colspan data + rv = GetCellSpansAt(table, startRowIndex, startColIndex, + actualRowSpan, actualColSpan); + NS_ENSURE_SUCCESS(rv, rv); + + // Must have some span to split + if (actualRowSpan <= 1 && actualColSpan <= 1) { + return NS_OK; + } + + AutoEditBatch beginBatching(this); + // Prevent auto insertion of BR in new cell until we're done + AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext); + + // We reset selection + AutoSelectionSetterAfterTableEdit setCaret(this, table, startRowIndex, + startColIndex, ePreviousColumn, + false); + //...so suppress Rules System selection munging + AutoTransactionsConserveSelection dontChangeSelection(this); + + nsCOMPtr<nsIDOMElement> newCell; + int32_t rowIndex = startRowIndex; + int32_t rowSpanBelow, colSpanAfter; + + // Split up cell row-wise first into rowspan=1 above, and the rest below, + // whittling away at the cell below until no more extra span + for (rowSpanBelow = actualRowSpan-1; rowSpanBelow >= 0; rowSpanBelow--) { + // We really split row-wise only if we had rowspan > 1 + if (rowSpanBelow > 0) { + rv = SplitCellIntoRows(table, rowIndex, startColIndex, 1, rowSpanBelow, + getter_AddRefs(newCell)); + NS_ENSURE_SUCCESS(rv, rv); + CopyCellBackgroundColor(newCell, cell); + } + int32_t colIndex = startColIndex; + // Now split the cell with rowspan = 1 into cells if it has colSpan > 1 + for (colSpanAfter = actualColSpan-1; colSpanAfter > 0; colSpanAfter--) { + rv = SplitCellIntoColumns(table, rowIndex, colIndex, 1, colSpanAfter, + getter_AddRefs(newCell)); + NS_ENSURE_SUCCESS(rv, rv); + CopyCellBackgroundColor(newCell, cell); + colIndex++; + } + // Point to the new cell and repeat + rowIndex++; + } + return NS_OK; +} + +nsresult +HTMLEditor::CopyCellBackgroundColor(nsIDOMElement* destCell, + nsIDOMElement* sourceCell) +{ + NS_ENSURE_TRUE(destCell && sourceCell, NS_ERROR_NULL_POINTER); + + // Copy backgournd color to new cell + NS_NAMED_LITERAL_STRING(bgcolor, "bgcolor"); + nsAutoString color; + bool isSet; + nsresult rv = GetAttributeValue(sourceCell, bgcolor, color, &isSet); + if (NS_FAILED(rv)) { + return rv; + } + if (!isSet) { + return NS_OK; + } + return SetAttribute(destCell, bgcolor, color); +} + +NS_IMETHODIMP +HTMLEditor::SplitCellIntoColumns(nsIDOMElement* aTable, + int32_t aRowIndex, + int32_t aColIndex, + int32_t aColSpanLeft, + int32_t aColSpanRight, + nsIDOMElement** aNewCell) +{ + NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER); + if (aNewCell) { + *aNewCell = nullptr; + } + + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + nsresult rv = + GetCellDataAt(aTable, aRowIndex, aColIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(cell, NS_ERROR_NULL_POINTER); + + // We can't split! + if (actualColSpan <= 1 || (aColSpanLeft + aColSpanRight) > actualColSpan) { + return NS_OK; + } + + // Reduce colspan of cell to split + rv = SetColSpan(cell, aColSpanLeft); + NS_ENSURE_SUCCESS(rv, rv); + + // Insert new cell after using the remaining span + // and always get the new cell so we can copy the background color; + nsCOMPtr<nsIDOMElement> newCell; + rv = InsertCell(cell, actualRowSpan, aColSpanRight, true, false, + getter_AddRefs(newCell)); + NS_ENSURE_SUCCESS(rv, rv); + if (!newCell) { + return NS_OK; + } + if (aNewCell) { + NS_ADDREF(*aNewCell = newCell.get()); + } + return CopyCellBackgroundColor(newCell, cell); +} + +NS_IMETHODIMP +HTMLEditor::SplitCellIntoRows(nsIDOMElement* aTable, + int32_t aRowIndex, + int32_t aColIndex, + int32_t aRowSpanAbove, + int32_t aRowSpanBelow, + nsIDOMElement** aNewCell) +{ + NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER); + if (aNewCell) *aNewCell = nullptr; + + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + nsresult rv = + GetCellDataAt(aTable, aRowIndex, aColIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(cell, NS_ERROR_NULL_POINTER); + + // We can't split! + if (actualRowSpan <= 1 || (aRowSpanAbove + aRowSpanBelow) > actualRowSpan) { + return NS_OK; + } + + int32_t rowCount, colCount; + rv = GetTableSize(aTable, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMElement> cell2; + nsCOMPtr<nsIDOMElement> lastCellFound; + int32_t startRowIndex2, startColIndex2, rowSpan2, colSpan2, actualRowSpan2, actualColSpan2; + bool isSelected2; + int32_t colIndex = 0; + bool insertAfter = (startColIndex > 0); + // This is the row we will insert new cell into + int32_t rowBelowIndex = startRowIndex+aRowSpanAbove; + + // Find a cell to insert before or after + for (;;) { + // Search for a cell to insert before + rv = GetCellDataAt(aTable, rowBelowIndex, + colIndex, getter_AddRefs(cell2), + &startRowIndex2, &startColIndex2, &rowSpan2, &colSpan2, + &actualRowSpan2, &actualColSpan2, &isSelected2); + // If we fail here, it could be because row has bad rowspan values, + // such as all cells having rowspan > 1 (Call FixRowSpan first!) + if (NS_FAILED(rv) || !cell) { + return NS_ERROR_FAILURE; + } + + // Skip over cells spanned from above (like the one we are splitting!) + if (cell2 && startRowIndex2 == rowBelowIndex) { + if (!insertAfter) { + // Inserting before, so stop at first cell in row we want to insert + // into. + break; + } + // New cell isn't first in row, + // so stop after we find the cell just before new cell's column + if (startColIndex2 + actualColSpan2 == startColIndex) { + break; + } + // If cell found is AFTER desired new cell colum, + // we have multiple cells with rowspan > 1 that + // prevented us from finding a cell to insert after... + if (startColIndex2 > startColIndex) { + // ... so instead insert before the cell we found + insertAfter = false; + break; + } + lastCellFound = cell2; + } + // Skip to next available cellmap location + colIndex += std::max(actualColSpan2, 1); + + // Done when past end of total number of columns + if (colIndex > colCount) { + break; + } + } + + if (!cell2 && lastCellFound) { + // Edge case where we didn't find a cell to insert after + // or before because column(s) before desired column + // and all columns after it are spanned from above. + // We can insert after the last cell we found + cell2 = lastCellFound; + insertAfter = true; // Should always be true, but let's be sure + } + + // Reduce rowspan of cell to split + rv = SetRowSpan(cell, aRowSpanAbove); + NS_ENSURE_SUCCESS(rv, rv); + + + // Insert new cell after using the remaining span + // and always get the new cell so we can copy the background color; + nsCOMPtr<nsIDOMElement> newCell; + rv = InsertCell(cell2, aRowSpanBelow, actualColSpan, insertAfter, false, + getter_AddRefs(newCell)); + NS_ENSURE_SUCCESS(rv, rv); + if (!newCell) { + return NS_OK; + } + if (aNewCell) { + NS_ADDREF(*aNewCell = newCell.get()); + } + return CopyCellBackgroundColor(newCell, cell2); +} + +NS_IMETHODIMP +HTMLEditor::SwitchTableCellHeaderType(nsIDOMElement* aSourceCell, + nsIDOMElement** aNewCell) +{ + nsCOMPtr<Element> sourceCell = do_QueryInterface(aSourceCell); + NS_ENSURE_TRUE(sourceCell, NS_ERROR_NULL_POINTER); + + AutoEditBatch beginBatching(this); + // Prevent auto insertion of BR in new cell created by ReplaceContainer + AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext); + + // Save current selection to restore when done + // This is needed so ReplaceContainer can monitor selection + // when replacing nodes + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + AutoSelectionRestorer selectionRestorer(selection, this); + + // Set to the opposite of current type + nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(aSourceCell); + nsIAtom* newCellType = atom == nsGkAtoms::td ? nsGkAtoms::th : nsGkAtoms::td; + + // This creates new node, moves children, copies attributes (true) + // and manages the selection! + nsCOMPtr<Element> newNode = ReplaceContainer(sourceCell, newCellType, + nullptr, nullptr, EditorBase::eCloneAttributes); + NS_ENSURE_TRUE(newNode, NS_ERROR_FAILURE); + + // Return the new cell + if (aNewCell) { + nsCOMPtr<nsIDOMElement> newElement = do_QueryInterface(newNode); + *aNewCell = newElement.get(); + NS_ADDREF(*aNewCell); + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::JoinTableCells(bool aMergeNonContiguousContents) +{ + nsCOMPtr<nsIDOMElement> table; + nsCOMPtr<nsIDOMElement> targetCell; + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + nsCOMPtr<nsIDOMElement> cell2; + int32_t startRowIndex2, startColIndex2, rowSpan2, colSpan2, actualRowSpan2, actualColSpan2; + bool isSelected2; + + // Get cell, table, etc. at selection anchor node + nsresult rv = GetCellContext(nullptr, + getter_AddRefs(table), + getter_AddRefs(targetCell), + nullptr, nullptr, + &startRowIndex, &startColIndex); + NS_ENSURE_SUCCESS(rv, rv); + if (!table || !targetCell) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + AutoEditBatch beginBatching(this); + //Don't let Rules System change the selection + AutoTransactionsConserveSelection dontChangeSelection(this); + + // Note: We dont' use AutoSelectionSetterAfterTableEdit here so the selection + // is retained after joining. This leaves the target cell selected + // as well as the "non-contiguous" cells, so user can see what happened. + + nsCOMPtr<nsIDOMElement> firstCell; + int32_t firstRowIndex, firstColIndex; + rv = GetFirstSelectedCellInTable(&firstRowIndex, &firstColIndex, + getter_AddRefs(firstCell)); + NS_ENSURE_SUCCESS(rv, rv); + + bool joinSelectedCells = false; + if (firstCell) { + nsCOMPtr<nsIDOMElement> secondCell; + rv = GetNextSelectedCell(nullptr, getter_AddRefs(secondCell)); + NS_ENSURE_SUCCESS(rv, rv); + + // If only one cell is selected, join with cell to the right + joinSelectedCells = (secondCell != nullptr); + } + + if (joinSelectedCells) { + // We have selected cells: Join just contiguous cells + // and just merge contents if not contiguous + + int32_t rowCount, colCount; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Get spans for cell we will merge into + int32_t firstRowSpan, firstColSpan; + rv = GetCellSpansAt(table, firstRowIndex, firstColIndex, + firstRowSpan, firstColSpan); + NS_ENSURE_SUCCESS(rv, rv); + + // This defines the last indexes along the "edges" + // of the contiguous block of cells, telling us + // that we can join adjacent cells to the block + // Start with same as the first values, + // then expand as we find adjacent selected cells + int32_t lastRowIndex = firstRowIndex; + int32_t lastColIndex = firstColIndex; + int32_t rowIndex, colIndex; + + // First pass: Determine boundaries of contiguous rectangular block + // that we will join into one cell, + // favoring adjacent cells in the same row + for (rowIndex = firstRowIndex; rowIndex <= lastRowIndex; rowIndex++) { + int32_t currentRowCount = rowCount; + // Be sure each row doesn't have rowspan errors + rv = FixBadRowSpan(table, rowIndex, rowCount); + NS_ENSURE_SUCCESS(rv, rv); + // Adjust rowcount by number of rows we removed + lastRowIndex -= (currentRowCount-rowCount); + + bool cellFoundInRow = false; + bool lastRowIsSet = false; + int32_t lastColInRow = 0; + int32_t firstColInRow = firstColIndex; + for (colIndex = firstColIndex; colIndex < colCount; + colIndex += std::max(actualColSpan2, 1)) { + rv = GetCellDataAt(table, rowIndex, colIndex, getter_AddRefs(cell2), + &startRowIndex2, &startColIndex2, + &rowSpan2, &colSpan2, + &actualRowSpan2, &actualColSpan2, &isSelected2); + NS_ENSURE_SUCCESS(rv, rv); + + if (isSelected2) { + if (!cellFoundInRow) { + // We've just found the first selected cell in this row + firstColInRow = colIndex; + } + if (rowIndex > firstRowIndex && firstColInRow != firstColIndex) { + // We're in at least the second row, + // but left boundary is "ragged" (not the same as 1st row's start) + //Let's just end block on previous row + // and keep previous lastColIndex + //TODO: We could try to find the Maximum firstColInRow + // so our block can still extend down more rows? + lastRowIndex = std::max(0,rowIndex - 1); + lastRowIsSet = true; + break; + } + // Save max selected column in this row, including extra colspan + lastColInRow = colIndex + (actualColSpan2-1); + cellFoundInRow = true; + } else if (cellFoundInRow) { + // No cell or not selected, but at least one cell in row was found + if (rowIndex > (firstRowIndex + 1) && colIndex <= lastColIndex) { + // Cell is in a column less than current right border in + // the third or higher selected row, so stop block at the previous row + lastRowIndex = std::max(0,rowIndex - 1); + lastRowIsSet = true; + } + // We're done with this row + break; + } + } // End of column loop + + // Done with this row + if (cellFoundInRow) { + if (rowIndex == firstRowIndex) { + // First row always initializes the right boundary + lastColIndex = lastColInRow; + } + + // If we didn't determine last row above... + if (!lastRowIsSet) { + if (colIndex < lastColIndex) { + // (don't think we ever get here?) + // Cell is in a column less than current right boundary, + // so stop block at the previous row + lastRowIndex = std::max(0,rowIndex - 1); + } else { + // Go on to examine next row + lastRowIndex = rowIndex+1; + } + } + // Use the minimum col we found so far for right boundary + lastColIndex = std::min(lastColIndex, lastColInRow); + } else { + // No selected cells in this row -- stop at row above + // and leave last column at its previous value + lastRowIndex = std::max(0,rowIndex - 1); + } + } + + // The list of cells we will delete after joining + nsTArray<nsCOMPtr<nsIDOMElement> > deleteList; + + // 2nd pass: Do the joining and merging + for (rowIndex = 0; rowIndex < rowCount; rowIndex++) { + for (colIndex = 0; colIndex < colCount; + colIndex += std::max(actualColSpan2, 1)) { + rv = GetCellDataAt(table, rowIndex, colIndex, getter_AddRefs(cell2), + &startRowIndex2, &startColIndex2, + &rowSpan2, &colSpan2, + &actualRowSpan2, &actualColSpan2, &isSelected2); + NS_ENSURE_SUCCESS(rv, rv); + + // If this is 0, we are past last cell in row, so exit the loop + if (!actualColSpan2) { + break; + } + + // Merge only selected cells (skip cell we're merging into, of course) + if (isSelected2 && cell2 != firstCell) { + if (rowIndex >= firstRowIndex && rowIndex <= lastRowIndex && + colIndex >= firstColIndex && colIndex <= lastColIndex) { + // We are within the join region + // Problem: It is very tricky to delete cells as we merge, + // since that will upset the cellmap + // Instead, build a list of cells to delete and do it later + NS_ASSERTION(startRowIndex2 == rowIndex, "JoinTableCells: StartRowIndex is in row above"); + + if (actualColSpan2 > 1) { + //Check if cell "hangs" off the boundary because of colspan > 1 + // Use split methods to chop off excess + int32_t extraColSpan = (startColIndex2 + actualColSpan2) - (lastColIndex+1); + if ( extraColSpan > 0) { + rv = SplitCellIntoColumns(table, startRowIndex2, startColIndex2, + actualColSpan2 - extraColSpan, + extraColSpan, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + rv = MergeCells(firstCell, cell2, false); + NS_ENSURE_SUCCESS(rv, rv); + + // Add cell to list to delete + deleteList.AppendElement(cell2.get()); + } else if (aMergeNonContiguousContents) { + // Cell is outside join region -- just merge the contents + rv = MergeCells(firstCell, cell2, false); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + } + + // All cell contents are merged. Delete the empty cells we accumulated + // Prevent rules testing until we're done + AutoRules beginRulesSniffing(this, EditAction::deleteNode, + nsIEditor::eNext); + + for (uint32_t i = 0, n = deleteList.Length(); i < n; i++) { + nsIDOMElement *elementPtr = deleteList[i]; + if (elementPtr) { + nsCOMPtr<nsIDOMNode> node = do_QueryInterface(elementPtr); + rv = DeleteNode(node); + NS_ENSURE_SUCCESS(rv, rv); + } + } + // Cleanup selection: remove ranges where cells were deleted + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + int32_t rangeCount; + rv = selection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsRange> range; + for (int32_t i = 0; i < rangeCount; i++) { + range = selection->GetRangeAt(i); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMElement> deletedCell; + GetCellFromRange(range, getter_AddRefs(deletedCell)); + if (!deletedCell) { + selection->RemoveRange(range); + rangeCount--; + i--; + } + } + + // Set spans for the cell everthing merged into + rv = SetRowSpan(firstCell, lastRowIndex-firstRowIndex+1); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetColSpan(firstCell, lastColIndex-firstColIndex+1); + NS_ENSURE_SUCCESS(rv, rv); + + + // Fixup disturbances in table layout + NormalizeTable(table); + } else { + // Joining with cell to the right -- get rowspan and colspan data of target cell + rv = GetCellDataAt(table, startRowIndex, startColIndex, + getter_AddRefs(targetCell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(targetCell, NS_ERROR_NULL_POINTER); + + // Get data for cell to the right + rv = GetCellDataAt(table, startRowIndex, startColIndex + actualColSpan, + getter_AddRefs(cell2), + &startRowIndex2, &startColIndex2, &rowSpan2, &colSpan2, + &actualRowSpan2, &actualColSpan2, &isSelected2); + NS_ENSURE_SUCCESS(rv, rv); + if (!cell2) { + return NS_OK; // Don't fail if there's no cell + } + + // sanity check + NS_ASSERTION((startRowIndex >= startRowIndex2),"JoinCells: startRowIndex < startRowIndex2"); + + // Figure out span of merged cell starting from target's starting row + // to handle case of merged cell starting in a row above + int32_t spanAboveMergedCell = startRowIndex - startRowIndex2; + int32_t effectiveRowSpan2 = actualRowSpan2 - spanAboveMergedCell; + + if (effectiveRowSpan2 > actualRowSpan) { + // Cell to the right spans into row below target + // Split off portion below target cell's bottom-most row + rv = SplitCellIntoRows(table, startRowIndex2, startColIndex2, + spanAboveMergedCell+actualRowSpan, + effectiveRowSpan2-actualRowSpan, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Move contents from cell to the right + // Delete the cell now only if it starts in the same row + // and has enough row "height" + rv = MergeCells(targetCell, cell2, + (startRowIndex2 == startRowIndex) && + (effectiveRowSpan2 >= actualRowSpan)); + NS_ENSURE_SUCCESS(rv, rv); + + if (effectiveRowSpan2 < actualRowSpan) { + // Merged cell is "shorter" + // (there are cells(s) below it that are row-spanned by target cell) + // We could try splitting those cells, but that's REAL messy, + // so the safest thing to do is NOT really join the cells + return NS_OK; + } + + if (spanAboveMergedCell > 0) { + // Cell we merged started in a row above the target cell + // Reduce rowspan to give room where target cell will extend its colspan + rv = SetRowSpan(cell2, spanAboveMergedCell); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Reset target cell's colspan to encompass cell to the right + rv = SetColSpan(targetCell, actualColSpan+actualColSpan2); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::MergeCells(nsCOMPtr<nsIDOMElement> aTargetCell, + nsCOMPtr<nsIDOMElement> aCellToMerge, + bool aDeleteCellToMerge) +{ + nsCOMPtr<dom::Element> targetCell = do_QueryInterface(aTargetCell); + nsCOMPtr<dom::Element> cellToMerge = do_QueryInterface(aCellToMerge); + NS_ENSURE_TRUE(targetCell && cellToMerge, NS_ERROR_NULL_POINTER); + + // Prevent rules testing until we're done + AutoRules beginRulesSniffing(this, EditAction::deleteNode, nsIEditor::eNext); + + // Don't need to merge if cell is empty + if (!IsEmptyCell(cellToMerge)) { + // Get index of last child in target cell + // If we fail or don't have children, + // we insert at index 0 + int32_t insertIndex = 0; + + // Start inserting just after last child + uint32_t len = targetCell->GetChildCount(); + if (len == 1 && IsEmptyCell(targetCell)) { + // Delete the empty node + nsIContent* cellChild = targetCell->GetFirstChild(); + nsresult rv = DeleteNode(cellChild->AsDOMNode()); + NS_ENSURE_SUCCESS(rv, rv); + insertIndex = 0; + } else { + insertIndex = (int32_t)len; + } + + // Move the contents + while (cellToMerge->HasChildren()) { + nsCOMPtr<nsIDOMNode> cellChild = cellToMerge->GetLastChild()->AsDOMNode(); + nsresult rv = DeleteNode(cellChild); + NS_ENSURE_SUCCESS(rv, rv); + + rv = InsertNode(cellChild, aTargetCell, insertIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Delete cells whose contents were moved + if (aDeleteCellToMerge) { + return DeleteNode(aCellToMerge); + } + + return NS_OK; +} + + +NS_IMETHODIMP +HTMLEditor::FixBadRowSpan(nsIDOMElement* aTable, + int32_t aRowIndex, + int32_t& aNewRowCount) +{ + NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER); + + int32_t rowCount, colCount; + nsresult rv = GetTableSize(aTable, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMElement>cell; + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + + int32_t minRowSpan = -1; + int32_t colIndex; + + for (colIndex = 0; colIndex < colCount; + colIndex += std::max(actualColSpan, 1)) { + rv = GetCellDataAt(aTable, aRowIndex, colIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + // NOTE: This is a *real* failure. + // GetCellDataAt passes if cell is missing from cellmap + if (NS_FAILED(rv)) { + return rv; + } + if (!cell) { + break; + } + if (rowSpan > 0 && + startRowIndex == aRowIndex && + (rowSpan < minRowSpan || minRowSpan == -1)) { + minRowSpan = rowSpan; + } + NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in FixBadRowSpan"); + } + if (minRowSpan > 1) { + // The amount to reduce everyone's rowspan + // so at least one cell has rowspan = 1 + int32_t rowsReduced = minRowSpan - 1; + for (colIndex = 0; colIndex < colCount; + colIndex += std::max(actualColSpan, 1)) { + rv = GetCellDataAt(aTable, aRowIndex, colIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + if (NS_FAILED(rv)) { + return rv; + } + // Fixup rowspans only for cells starting in current row + if (cell && rowSpan > 0 && + startRowIndex == aRowIndex && + startColIndex == colIndex ) { + rv = SetRowSpan(cell, rowSpan-rowsReduced); + if (NS_FAILED(rv)) { + return rv; + } + } + NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in FixBadRowSpan"); + } + } + return GetTableSize(aTable, &aNewRowCount, &colCount); +} + +NS_IMETHODIMP +HTMLEditor::FixBadColSpan(nsIDOMElement* aTable, + int32_t aColIndex, + int32_t& aNewColCount) +{ + NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER); + + int32_t rowCount, colCount; + nsresult rv = GetTableSize(aTable, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + + int32_t minColSpan = -1; + int32_t rowIndex; + + for (rowIndex = 0; rowIndex < rowCount; + rowIndex += std::max(actualRowSpan, 1)) { + rv = GetCellDataAt(aTable, rowIndex, aColIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + // NOTE: This is a *real* failure. + // GetCellDataAt passes if cell is missing from cellmap + if (NS_FAILED(rv)) { + return rv; + } + if (!cell) { + break; + } + if (colSpan > 0 && + startColIndex == aColIndex && + (colSpan < minColSpan || minColSpan == -1)) { + minColSpan = colSpan; + } + NS_ASSERTION((actualRowSpan > 0),"ActualRowSpan = 0 in FixBadColSpan"); + } + if (minColSpan > 1) { + // The amount to reduce everyone's colspan + // so at least one cell has colspan = 1 + int32_t colsReduced = minColSpan - 1; + for (rowIndex = 0; rowIndex < rowCount; + rowIndex += std::max(actualRowSpan, 1)) { + rv = GetCellDataAt(aTable, rowIndex, aColIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + if (NS_FAILED(rv)) { + return rv; + } + // Fixup colspans only for cells starting in current column + if (cell && colSpan > 0 && + startColIndex == aColIndex && + startRowIndex == rowIndex) { + rv = SetColSpan(cell, colSpan-colsReduced); + if (NS_FAILED(rv)) { + return rv; + } + } + NS_ASSERTION((actualRowSpan > 0),"ActualRowSpan = 0 in FixBadColSpan"); + } + } + return GetTableSize(aTable, &rowCount, &aNewColCount); +} + +NS_IMETHODIMP +HTMLEditor::NormalizeTable(nsIDOMElement* aTable) +{ + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMElement> table; + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), + aTable, getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + // Don't fail if we didn't find a table + NS_ENSURE_TRUE(table, NS_OK); + + int32_t rowCount, colCount, rowIndex, colIndex; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Save current selection + AutoSelectionRestorer selectionRestorer(selection, this); + + AutoEditBatch beginBatching(this); + // Prevent auto insertion of BR in new cell until we're done + AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext); + + nsCOMPtr<nsIDOMElement> cell; + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + + // Scan all cells in each row to detect bad rowspan values + for (rowIndex = 0; rowIndex < rowCount; rowIndex++) { + rv = FixBadRowSpan(table, rowIndex, rowCount); + NS_ENSURE_SUCCESS(rv, rv); + } + // and same for colspans + for (colIndex = 0; colIndex < colCount; colIndex++) { + rv = FixBadColSpan(table, colIndex, colCount); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Fill in missing cellmap locations with empty cells + for (rowIndex = 0; rowIndex < rowCount; rowIndex++) { + nsCOMPtr<nsIDOMElement> previousCellInRow; + for (colIndex = 0; colIndex < colCount; colIndex++) { + rv = GetCellDataAt(table, rowIndex, colIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + // NOTE: This is a *real* failure. + // GetCellDataAt passes if cell is missing from cellmap + if (NS_FAILED(rv)) { + return rv; + } + if (!cell) { + //We are missing a cell at a cellmap location +#ifdef DEBUG + printf("NormalizeTable found missing cell at row=%d, col=%d\n", + rowIndex, colIndex); +#endif + // Add a cell after the previous Cell in the current row + if (!previousCellInRow) { + // We don't have any cells in this row -- We are really messed up! +#ifdef DEBUG + printf("NormalizeTable found no cells in row=%d, col=%d\n", + rowIndex, colIndex); +#endif + return NS_ERROR_FAILURE; + } + + // Insert a new cell after (true), and return the new cell to us + rv = InsertCell(previousCellInRow, 1, 1, true, false, + getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set this so we use returned new "cell" to set previousCellInRow below + if (cell) { + startRowIndex = rowIndex; + } + } + // Save the last cell found in the same row we are scanning + if (startRowIndex == rowIndex) { + previousCellInRow = cell; + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetCellIndexes(nsIDOMElement* aCell, + int32_t* aRowIndex, + int32_t* aColIndex) +{ + NS_ENSURE_ARG_POINTER(aRowIndex); + *aColIndex=0; // initialize out params + NS_ENSURE_ARG_POINTER(aColIndex); + *aRowIndex=0; + if (!aCell) { + // Get the selected cell or the cell enclosing the selection anchor + nsCOMPtr<nsIDOMElement> cell; + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("td"), nullptr, + getter_AddRefs(cell)); + if (NS_FAILED(rv) || !cell) { + return NS_ERROR_FAILURE; + } + aCell = cell; + } + + NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED); + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsIContent> nodeAsContent( do_QueryInterface(aCell) ); + NS_ENSURE_TRUE(nodeAsContent, NS_ERROR_FAILURE); + // frames are not ref counted, so don't use an nsCOMPtr + nsIFrame *layoutObject = nodeAsContent->GetPrimaryFrame(); + NS_ENSURE_TRUE(layoutObject, NS_ERROR_FAILURE); + + nsITableCellLayout *cellLayoutObject = do_QueryFrame(layoutObject); + NS_ENSURE_TRUE(cellLayoutObject, NS_ERROR_FAILURE); + return cellLayoutObject->GetCellIndexes(*aRowIndex, *aColIndex); +} + +nsTableWrapperFrame* +HTMLEditor::GetTableFrame(nsIDOMElement* aTable) +{ + NS_ENSURE_TRUE(aTable, nullptr); + + nsCOMPtr<nsIContent> nodeAsContent( do_QueryInterface(aTable) ); + NS_ENSURE_TRUE(nodeAsContent, nullptr); + return do_QueryFrame(nodeAsContent->GetPrimaryFrame()); +} + +//Return actual number of cells (a cell with colspan > 1 counts as just 1) +int32_t +HTMLEditor::GetNumberOfCellsInRow(nsIDOMElement* aTable, + int32_t rowIndex) +{ + int32_t cellCount = 0; + nsCOMPtr<nsIDOMElement> cell; + int32_t colIndex = 0; + do { + int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + nsresult rv = + GetCellDataAt(aTable, rowIndex, colIndex, getter_AddRefs(cell), + &startRowIndex, &startColIndex, &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + NS_ENSURE_SUCCESS(rv, 0); + if (cell) { + // Only count cells that start in row we are working with + if (startRowIndex == rowIndex) { + cellCount++; + } + //Next possible location for a cell + colIndex += actualColSpan; + } else { + colIndex++; + } + } while (cell); + + return cellCount; +} + +NS_IMETHODIMP +HTMLEditor::GetTableSize(nsIDOMElement* aTable, + int32_t* aRowCount, + int32_t* aColCount) +{ + NS_ENSURE_ARG_POINTER(aRowCount); + NS_ENSURE_ARG_POINTER(aColCount); + *aRowCount = 0; + *aColCount = 0; + nsCOMPtr<nsIDOMElement> table; + // Get the selected talbe or the table enclosing the selection anchor + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), aTable, + getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(table, NS_ERROR_FAILURE); + + nsTableWrapperFrame* tableFrame = GetTableFrame(table.get()); + NS_ENSURE_TRUE(tableFrame, NS_ERROR_FAILURE); + + *aRowCount = tableFrame->GetRowCount(); + *aColCount = tableFrame->GetColCount(); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetCellDataAt(nsIDOMElement* aTable, + int32_t aRowIndex, + int32_t aColIndex, + nsIDOMElement** aCell, + int32_t* aStartRowIndex, + int32_t* aStartColIndex, + int32_t* aRowSpan, + int32_t* aColSpan, + int32_t* aActualRowSpan, + int32_t* aActualColSpan, + bool* aIsSelected) +{ + NS_ENSURE_ARG_POINTER(aStartRowIndex); + NS_ENSURE_ARG_POINTER(aStartColIndex); + NS_ENSURE_ARG_POINTER(aRowSpan); + NS_ENSURE_ARG_POINTER(aColSpan); + NS_ENSURE_ARG_POINTER(aActualRowSpan); + NS_ENSURE_ARG_POINTER(aActualColSpan); + NS_ENSURE_ARG_POINTER(aIsSelected); + NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER); + + *aStartRowIndex = 0; + *aStartColIndex = 0; + *aRowSpan = 0; + *aColSpan = 0; + *aActualRowSpan = 0; + *aActualColSpan = 0; + *aIsSelected = false; + + *aCell = nullptr; + + if (!aTable) { + // Get the selected table or the table enclosing the selection anchor + nsCOMPtr<nsIDOMElement> table; + nsresult rv = + GetElementOrParentByTagName(NS_LITERAL_STRING("table"), nullptr, + getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + if (!table) { + return NS_ERROR_FAILURE; + } + aTable = table; + } + + nsTableWrapperFrame* tableFrame = GetTableFrame(aTable); + NS_ENSURE_TRUE(tableFrame, NS_ERROR_FAILURE); + + nsTableCellFrame* cellFrame = + tableFrame->GetCellFrameAt(aRowIndex, aColIndex); + if (!cellFrame) { + return NS_ERROR_FAILURE; + } + + *aIsSelected = cellFrame->IsSelected(); + cellFrame->GetRowIndex(*aStartRowIndex); + cellFrame->GetColIndex(*aStartColIndex); + *aRowSpan = cellFrame->GetRowSpan(); + *aColSpan = cellFrame->GetColSpan(); + *aActualRowSpan = tableFrame->GetEffectiveRowSpanAt(aRowIndex, aColIndex); + *aActualColSpan = tableFrame->GetEffectiveColSpanAt(aRowIndex, aColIndex); + nsCOMPtr<nsIDOMElement> domCell = do_QueryInterface(cellFrame->GetContent()); + domCell.forget(aCell); + + return NS_OK; +} + +// When all you want is the cell +NS_IMETHODIMP +HTMLEditor::GetCellAt(nsIDOMElement* aTable, + int32_t aRowIndex, + int32_t aColIndex, + nsIDOMElement** aCell) +{ + NS_ENSURE_ARG_POINTER(aCell); + *aCell = nullptr; + + if (!aTable) { + // Get the selected table or the table enclosing the selection anchor + nsCOMPtr<nsIDOMElement> table; + nsresult rv = + GetElementOrParentByTagName(NS_LITERAL_STRING("table"), nullptr, + getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(table, NS_ERROR_FAILURE); + aTable = table; + } + + nsTableWrapperFrame* tableFrame = GetTableFrame(aTable); + if (!tableFrame) { + *aCell = nullptr; + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + nsCOMPtr<nsIDOMElement> domCell = + do_QueryInterface(tableFrame->GetCellAt(aRowIndex, aColIndex)); + domCell.forget(aCell); + + return NS_OK; +} + +// When all you want are the rowspan and colspan (not exposed in nsITableEditor) +NS_IMETHODIMP +HTMLEditor::GetCellSpansAt(nsIDOMElement* aTable, + int32_t aRowIndex, + int32_t aColIndex, + int32_t& aActualRowSpan, + int32_t& aActualColSpan) +{ + nsTableWrapperFrame* tableFrame = GetTableFrame(aTable); + if (!tableFrame) { + return NS_ERROR_FAILURE; + } + aActualRowSpan = tableFrame->GetEffectiveRowSpanAt(aRowIndex, aColIndex); + aActualColSpan = tableFrame->GetEffectiveColSpanAt(aRowIndex, aColIndex); + + return NS_OK; +} + +nsresult +HTMLEditor::GetCellContext(Selection** aSelection, + nsIDOMElement** aTable, + nsIDOMElement** aCell, + nsIDOMNode** aCellParent, + int32_t* aCellOffset, + int32_t* aRowIndex, + int32_t* aColIndex) +{ + // Initialize return pointers + if (aSelection) { + *aSelection = nullptr; + } + if (aTable) { + *aTable = nullptr; + } + if (aCell) { + *aCell = nullptr; + } + if (aCellParent) { + *aCellParent = nullptr; + } + if (aCellOffset) { + *aCellOffset = 0; + } + if (aRowIndex) { + *aRowIndex = 0; + } + if (aColIndex) { + *aColIndex = 0; + } + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + if (aSelection) { + *aSelection = selection.get(); + NS_ADDREF(*aSelection); + } + nsCOMPtr <nsIDOMElement> table; + nsCOMPtr <nsIDOMElement> cell; + + // Caller may supply the cell... + if (aCell && *aCell) { + cell = *aCell; + } + + // ...but if not supplied, + // get cell if it's the child of selection anchor node, + // or get the enclosing by a cell + if (!cell) { + // Find a selected or enclosing table element + nsCOMPtr<nsIDOMElement> cellOrTableElement; + int32_t selectedCount; + nsAutoString tagName; + nsresult rv = + GetSelectedOrParentTableElement(tagName, &selectedCount, + getter_AddRefs(cellOrTableElement)); + NS_ENSURE_SUCCESS(rv, rv); + if (tagName.EqualsLiteral("table")) { + // We have a selected table, not a cell + if (aTable) { + *aTable = cellOrTableElement.get(); + NS_ADDREF(*aTable); + } + return NS_OK; + } + if (!tagName.EqualsLiteral("td")) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + // We found a cell + cell = cellOrTableElement; + } + if (aCell) { + *aCell = cell.get(); + NS_ADDREF(*aCell); + } + + // Get containing table + nsresult rv = GetElementOrParentByTagName(NS_LITERAL_STRING("table"), cell, + getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + // Cell must be in a table, so fail if not found + NS_ENSURE_TRUE(table, NS_ERROR_FAILURE); + if (aTable) { + *aTable = table.get(); + NS_ADDREF(*aTable); + } + + // Get the rest of the related data only if requested + if (aRowIndex || aColIndex) { + int32_t rowIndex, colIndex; + // Get current cell location so we can put caret back there when done + rv = GetCellIndexes(cell, &rowIndex, &colIndex); + if (NS_FAILED(rv)) { + return rv; + } + if (aRowIndex) { + *aRowIndex = rowIndex; + } + if (aColIndex) { + *aColIndex = colIndex; + } + } + if (aCellParent) { + nsCOMPtr <nsIDOMNode> cellParent; + // Get the immediate parent of the cell + rv = cell->GetParentNode(getter_AddRefs(cellParent)); + NS_ENSURE_SUCCESS(rv, rv); + // Cell has to have a parent, so fail if not found + NS_ENSURE_TRUE(cellParent, NS_ERROR_FAILURE); + + *aCellParent = cellParent.get(); + NS_ADDREF(*aCellParent); + + if (aCellOffset) { + *aCellOffset = GetChildOffset(cell, cellParent); + } + } + + return NS_OK; +} + +nsresult +HTMLEditor::GetCellFromRange(nsRange* aRange, + nsIDOMElement** aCell) +{ + // Note: this might return a node that is outside of the range. + // Use carefully. + NS_ENSURE_TRUE(aRange && aCell, NS_ERROR_NULL_POINTER); + + *aCell = nullptr; + + nsCOMPtr<nsIDOMNode> startParent; + nsresult rv = aRange->GetStartContainer(getter_AddRefs(startParent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(startParent, NS_ERROR_FAILURE); + + int32_t startOffset; + rv = aRange->GetStartOffset(&startOffset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> childNode = GetChildAt(startParent, startOffset); + // This means selection is probably at a text node (or end of doc?) + if (!childNode) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMNode> endParent; + rv = aRange->GetEndContainer(getter_AddRefs(endParent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(startParent, NS_ERROR_FAILURE); + + int32_t endOffset; + rv = aRange->GetEndOffset(&endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // If a cell is deleted, the range is collapse + // (startOffset == endOffset) + // so tell caller the cell wasn't found + if (startParent == endParent && + endOffset == startOffset+1 && + HTMLEditUtils::IsTableCell(childNode)) { + // Should we also test if frame is selected? (Use GetCellDataAt()) + // (Let's not for now -- more efficient) + nsCOMPtr<nsIDOMElement> cellElement = do_QueryInterface(childNode); + *aCell = cellElement.get(); + NS_ADDREF(*aCell); + return NS_OK; + } + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; +} + +NS_IMETHODIMP +HTMLEditor::GetFirstSelectedCell(nsIDOMRange** aRange, + nsIDOMElement** aCell) +{ + NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER); + *aCell = nullptr; + if (aRange) { + *aRange = nullptr; + } + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + RefPtr<nsRange> range = selection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + mSelectedCellIndex = 0; + + nsresult rv = GetCellFromRange(range, aCell); + // Failure here probably means selection is in a text node, + // so there's no selected cell + if (NS_FAILED(rv)) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + // No cell means range was collapsed (cell was deleted) + if (!*aCell) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + if (aRange) { + *aRange = range.get(); + NS_ADDREF(*aRange); + } + + // Setup for next cell + mSelectedCellIndex = 1; + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetNextSelectedCell(nsIDOMRange** aRange, + nsIDOMElement** aCell) +{ + NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER); + *aCell = nullptr; + if (aRange) { + *aRange = nullptr; + } + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + int32_t rangeCount = selection->RangeCount(); + + // Don't even try if index exceeds range count + if (mSelectedCellIndex >= rangeCount) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + // Scan through ranges to find next valid selected cell + RefPtr<nsRange> range; + for (; mSelectedCellIndex < rangeCount; mSelectedCellIndex++) { + range = selection->GetRangeAt(mSelectedCellIndex); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + nsresult rv = GetCellFromRange(range, aCell); + // Failure here means the range doesn't contain a cell + NS_ENSURE_SUCCESS(rv, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + // We found a selected cell + if (*aCell) { + break; + } + + // If we didn't find a cell, continue to next range in selection + } + // No cell means all remaining ranges were collapsed (cells were deleted) + NS_ENSURE_TRUE(*aCell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + if (aRange) { + *aRange = range.get(); + NS_ADDREF(*aRange); + } + + // Setup for next cell + mSelectedCellIndex++; + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetFirstSelectedCellInTable(int32_t* aRowIndex, + int32_t* aColIndex, + nsIDOMElement** aCell) +{ + NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER); + *aCell = nullptr; + if (aRowIndex) { + *aRowIndex = 0; + } + if (aColIndex) { + *aColIndex = 0; + } + + nsCOMPtr<nsIDOMElement> cell; + nsresult rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND); + + *aCell = cell.get(); + NS_ADDREF(*aCell); + + // Also return the row and/or column if requested + if (aRowIndex || aColIndex) { + int32_t startRowIndex, startColIndex; + rv = GetCellIndexes(cell, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + return rv; + } + + if (aRowIndex) { + *aRowIndex = startRowIndex; + } + if (aColIndex) { + *aColIndex = startColIndex; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::SetSelectionAfterTableEdit(nsIDOMElement* aTable, + int32_t aRow, + int32_t aCol, + int32_t aDirection, + bool aSelected) +{ + NS_ENSURE_TRUE(aTable, NS_ERROR_NOT_INITIALIZED); + + RefPtr<Selection> selection = GetSelection(); + + if (!selection) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMElement> cell; + bool done = false; + do { + nsresult rv = GetCellAt(aTable, aRow, aCol, getter_AddRefs(cell)); + if (NS_FAILED(rv)) { + break; + } + + if (cell) { + if (aSelected) { + // Reselect the cell + return SelectElement(cell); + } else { + // Set the caret to deepest first child + // but don't go into nested tables + // TODO: Should we really be placing the caret at the END + // of the cell content? + nsCOMPtr<nsINode> cellNode = do_QueryInterface(cell); + if (cellNode) { + CollapseSelectionToDeepestNonTableFirstChild(selection, cellNode); + } + return NS_OK; + } + } else { + // Setup index to find another cell in the + // direction requested, but move in + // other direction if already at beginning of row or column + switch (aDirection) { + case ePreviousColumn: + if (!aCol) { + if (aRow > 0) { + aRow--; + } else { + done = true; + } + } else { + aCol--; + } + break; + case ePreviousRow: + if (!aRow) { + if (aCol > 0) { + aCol--; + } else { + done = true; + } + } else { + aRow--; + } + break; + default: + done = true; + } + } + } while (!done); + + // We didn't find a cell + // Set selection to just before the table + nsCOMPtr<nsIDOMNode> tableParent; + nsresult rv = aTable->GetParentNode(getter_AddRefs(tableParent)); + if (NS_SUCCEEDED(rv) && tableParent) { + int32_t tableOffset = GetChildOffset(aTable, tableParent); + return selection->Collapse(tableParent, tableOffset); + } + // Last resort: Set selection to start of doc + // (it's very bad to not have a valid selection!) + return SetSelectionAtDocumentStart(selection); +} + +NS_IMETHODIMP +HTMLEditor::GetSelectedOrParentTableElement(nsAString& aTagName, + int32_t* aSelectedCount, + nsIDOMElement** aTableElement) +{ + NS_ENSURE_ARG_POINTER(aTableElement); + NS_ENSURE_ARG_POINTER(aSelectedCount); + *aTableElement = nullptr; + aTagName.Truncate(); + *aSelectedCount = 0; + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + // Try to get the first selected cell + nsCOMPtr<nsIDOMElement> tableOrCellElement; + nsresult rv = GetFirstSelectedCell(nullptr, + getter_AddRefs(tableOrCellElement)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_NAMED_LITERAL_STRING(tdName, "td"); + + if (tableOrCellElement) { + // Each cell is in its own selection range, + // so count signals multiple-cell selection + rv = selection->GetRangeCount(aSelectedCount); + NS_ENSURE_SUCCESS(rv, rv); + aTagName = tdName; + } else { + nsCOMPtr<nsIDOMNode> anchorNode; + rv = selection->GetAnchorNode(getter_AddRefs(anchorNode)); + if (NS_FAILED(rv)) { + return rv; + } + NS_ENSURE_TRUE(anchorNode, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMNode> selectedNode; + + // Get child of anchor node, if exists + bool hasChildren; + anchorNode->HasChildNodes(&hasChildren); + + if (hasChildren) { + int32_t anchorOffset; + rv = selection->GetAnchorOffset(&anchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + selectedNode = GetChildAt(anchorNode, anchorOffset); + if (!selectedNode) { + selectedNode = anchorNode; + // If anchor doesn't have a child, we can't be selecting a table element, + // so don't do the following: + } else { + nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(selectedNode); + + if (atom == nsGkAtoms::td) { + tableOrCellElement = do_QueryInterface(selectedNode); + aTagName = tdName; + // Each cell is in its own selection range, + // so count signals multiple-cell selection + rv = selection->GetRangeCount(aSelectedCount); + NS_ENSURE_SUCCESS(rv, rv); + } else if (atom == nsGkAtoms::table) { + tableOrCellElement = do_QueryInterface(selectedNode); + aTagName.AssignLiteral("table"); + *aSelectedCount = 1; + } else if (atom == nsGkAtoms::tr) { + tableOrCellElement = do_QueryInterface(selectedNode); + aTagName.AssignLiteral("tr"); + *aSelectedCount = 1; + } + } + } + if (!tableOrCellElement) { + // Didn't find a table element -- find a cell parent + rv = GetElementOrParentByTagName(tdName, anchorNode, + getter_AddRefs(tableOrCellElement)); + if (NS_FAILED(rv)) { + return rv; + } + if (tableOrCellElement) { + aTagName = tdName; + } + } + } + if (tableOrCellElement) { + *aTableElement = tableOrCellElement.get(); + NS_ADDREF(*aTableElement); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLEditor::GetSelectedCellsType(nsIDOMElement* aElement, + uint32_t* aSelectionType) +{ + NS_ENSURE_ARG_POINTER(aSelectionType); + *aSelectionType = 0; + + // Be sure we have a table element + // (if aElement is null, this uses selection's anchor node) + nsCOMPtr<nsIDOMElement> table; + + nsresult rv = + GetElementOrParentByTagName(NS_LITERAL_STRING("table"), aElement, + getter_AddRefs(table)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t rowCount, colCount; + rv = GetTableSize(table, &rowCount, &colCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Traverse all selected cells + nsCOMPtr<nsIDOMElement> selectedCell; + rv = GetFirstSelectedCell(nullptr, getter_AddRefs(selectedCell)); + NS_ENSURE_SUCCESS(rv, rv); + if (rv == NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND) { + return NS_OK; + } + + // We have at least one selected cell, so set return value + *aSelectionType = nsISelectionPrivate::TABLESELECTION_CELL; + + // Store indexes of each row/col to avoid duplication of searches + nsTArray<int32_t> indexArray; + + bool allCellsInRowAreSelected = false; + bool allCellsInColAreSelected = false; + while (NS_SUCCEEDED(rv) && selectedCell) { + // Get the cell's location in the cellmap + int32_t startRowIndex, startColIndex; + rv = GetCellIndexes(selectedCell, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + return rv; + } + + if (!indexArray.Contains(startColIndex)) { + indexArray.AppendElement(startColIndex); + allCellsInRowAreSelected = AllCellsInRowSelected(table, startRowIndex, colCount); + // We're done as soon as we fail for any row + if (!allCellsInRowAreSelected) { + break; + } + } + rv = GetNextSelectedCell(nullptr, getter_AddRefs(selectedCell)); + } + + if (allCellsInRowAreSelected) { + *aSelectionType = nsISelectionPrivate::TABLESELECTION_ROW; + return NS_OK; + } + // Test for columns + + // Empty the indexArray + indexArray.Clear(); + + // Start at first cell again + rv = GetFirstSelectedCell(nullptr, getter_AddRefs(selectedCell)); + while (NS_SUCCEEDED(rv) && selectedCell) { + // Get the cell's location in the cellmap + int32_t startRowIndex, startColIndex; + rv = GetCellIndexes(selectedCell, &startRowIndex, &startColIndex); + if (NS_FAILED(rv)) { + return rv; + } + + if (!indexArray.Contains(startRowIndex)) { + indexArray.AppendElement(startColIndex); + allCellsInColAreSelected = AllCellsInColumnSelected(table, startColIndex, rowCount); + // We're done as soon as we fail for any column + if (!allCellsInRowAreSelected) { + break; + } + } + rv = GetNextSelectedCell(nullptr, getter_AddRefs(selectedCell)); + } + if (allCellsInColAreSelected) { + *aSelectionType = nsISelectionPrivate::TABLESELECTION_COLUMN; + } + + return NS_OK; +} + +bool +HTMLEditor::AllCellsInRowSelected(nsIDOMElement* aTable, + int32_t aRowIndex, + int32_t aNumberOfColumns) +{ + NS_ENSURE_TRUE(aTable, false); + + int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + + for (int32_t col = 0; col < aNumberOfColumns; + col += std::max(actualColSpan, 1)) { + nsCOMPtr<nsIDOMElement> cell; + nsresult rv = GetCellDataAt(aTable, aRowIndex, col, getter_AddRefs(cell), + &curStartRowIndex, &curStartColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + + NS_ENSURE_SUCCESS(rv, false); + // If no cell, we may have a "ragged" right edge, + // so return TRUE only if we already found a cell in the row + NS_ENSURE_TRUE(cell, (col > 0) ? true : false); + + // Return as soon as a non-selected cell is found + NS_ENSURE_TRUE(isSelected, false); + + NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in AllCellsInRowSelected"); + } + return true; +} + +bool +HTMLEditor::AllCellsInColumnSelected(nsIDOMElement* aTable, + int32_t aColIndex, + int32_t aNumberOfRows) +{ + NS_ENSURE_TRUE(aTable, false); + + int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan; + bool isSelected; + + for (int32_t row = 0; row < aNumberOfRows; + row += std::max(actualRowSpan, 1)) { + nsCOMPtr<nsIDOMElement> cell; + nsresult rv = GetCellDataAt(aTable, row, aColIndex, getter_AddRefs(cell), + &curStartRowIndex, &curStartColIndex, + &rowSpan, &colSpan, + &actualRowSpan, &actualColSpan, &isSelected); + + NS_ENSURE_SUCCESS(rv, false); + // If no cell, we must have a "ragged" right edge on the last column + // so return TRUE only if we already found a cell in the row + NS_ENSURE_TRUE(cell, (row > 0) ? true : false); + + // Return as soon as a non-selected cell is found + NS_ENSURE_TRUE(isSelected, false); + } + return true; +} + +bool +HTMLEditor::IsEmptyCell(dom::Element* aCell) +{ + MOZ_ASSERT(aCell); + + // Check if target only contains empty text node or <br> + nsCOMPtr<nsINode> cellChild = aCell->GetFirstChild(); + if (!cellChild) { + return false; + } + + nsCOMPtr<nsINode> nextChild = cellChild->GetNextSibling(); + if (nextChild) { + return false; + } + + // We insert a single break into a cell by default + // to have some place to locate a cursor -- it is dispensable + if (cellChild->IsHTMLElement(nsGkAtoms::br)) { + return true; + } + + bool isEmpty; + // Or check if no real content + nsresult rv = IsEmptyNode(cellChild, &isEmpty, false, false); + NS_ENSURE_SUCCESS(rv, false); + return isEmpty; +} + +} // namespace mozilla diff --git a/editor/libeditor/HTMLURIRefObject.cpp b/editor/libeditor/HTMLURIRefObject.cpp new file mode 100644 index 000000000..612451d85 --- /dev/null +++ b/editor/libeditor/HTMLURIRefObject.cpp @@ -0,0 +1,262 @@ +/* -*- 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/. */ + +/* Here is the list, from beppe and glazman: + href >> A, AREA, BASE, LINK + src >> FRAME, IFRAME, IMG, INPUT, SCRIPT + <META http-equiv="refresh" content="3,http://www.acme.com/intro.html"> + longdesc >> FRAME, IFRAME, IMG + usemap >> IMG, INPUT, OBJECT + action >> FORM + background >> BODY + codebase >> OBJECT, APPLET + classid >> OBJECT + data >> OBJECT + cite >> BLOCKQUOTE, DEL, INS, Q + profile >> HEAD + ARCHIVE attribute on APPLET ; warning, it contains a list of URIs. + + Easier way of organizing the list: + a: href + area: href + base: href + body: background + blockquote: cite (not normally rewritable) + link: href + frame: src, longdesc + iframe: src, longdesc + input: src, usemap + form: action + img: src, longdesc, usemap + script: src + applet: codebase, archive <list> + object: codebase, data, classid, usemap + head: profile + del: cite + ins: cite + q: cite + */ + +#include "HTMLURIRefObject.h" + +#include "mozilla/mozalloc.h" +#include "nsAString.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsID.h" +#include "nsIDOMAttr.h" +#include "nsIDOMElement.h" +#include "nsIDOMMozNamedAttrMap.h" +#include "nsIDOMNode.h" +#include "nsISupportsUtils.h" +#include "nsString.h" + +namespace mozilla { + +// String classes change too often and I can't keep up. +// Set this macro to this week's approved case-insensitive compare routine. +#define MATCHES(tagName, str) tagName.EqualsIgnoreCase(str) + +HTMLURIRefObject::HTMLURIRefObject() + : mCurAttrIndex(0) + , mAttributeCnt(0) +{ +} + +HTMLURIRefObject::~HTMLURIRefObject() +{ +} + +//Interfaces for addref and release and queryinterface +NS_IMPL_ISUPPORTS(HTMLURIRefObject, nsIURIRefObject) + +NS_IMETHODIMP +HTMLURIRefObject::Reset() +{ + mCurAttrIndex = 0; + return NS_OK; +} + +NS_IMETHODIMP +HTMLURIRefObject::GetNextURI(nsAString& aURI) +{ + NS_ENSURE_TRUE(mNode, NS_ERROR_NOT_INITIALIZED); + + // XXX Why don't you use nsIAtom for comparing the tag name a lot? + nsAutoString tagName; + nsresult rv = mNode->GetNodeName(tagName); + NS_ENSURE_SUCCESS(rv, rv); + + // Loop over attribute list: + if (!mAttributes) { + nsCOMPtr<nsIDOMElement> element (do_QueryInterface(mNode)); + NS_ENSURE_TRUE(element, NS_ERROR_INVALID_ARG); + + mCurAttrIndex = 0; + element->GetAttributes(getter_AddRefs(mAttributes)); + NS_ENSURE_TRUE(mAttributes, NS_ERROR_NOT_INITIALIZED); + + rv = mAttributes->GetLength(&mAttributeCnt); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(mAttributeCnt, NS_ERROR_FAILURE); + mCurAttrIndex = 0; + } + + while (mCurAttrIndex < mAttributeCnt) { + nsCOMPtr<nsIDOMAttr> attrNode; + rv = mAttributes->Item(mCurAttrIndex++, getter_AddRefs(attrNode)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_ARG_POINTER(attrNode); + nsString curAttr; + rv = attrNode->GetName(curAttr); + NS_ENSURE_SUCCESS(rv, rv); + + // href >> A, AREA, BASE, LINK + if (MATCHES(curAttr, "href")) { + if (!MATCHES(tagName, "a") && !MATCHES(tagName, "area") && + !MATCHES(tagName, "base") && !MATCHES(tagName, "link")) { + continue; + } + rv = attrNode->GetValue(aURI); + NS_ENSURE_SUCCESS(rv, rv); + nsString uri (aURI); + // href pointing to a named anchor doesn't count + if (aURI.First() != char16_t('#')) { + return NS_OK; + } + aURI.Truncate(); + return NS_ERROR_INVALID_ARG; + } + // src >> FRAME, IFRAME, IMG, INPUT, SCRIPT + else if (MATCHES(curAttr, "src")) { + if (!MATCHES(tagName, "img") && + !MATCHES(tagName, "frame") && !MATCHES(tagName, "iframe") && + !MATCHES(tagName, "input") && !MATCHES(tagName, "script")) { + continue; + } + return attrNode->GetValue(aURI); + } + //<META http-equiv="refresh" content="3,http://www.acme.com/intro.html"> + else if (MATCHES(curAttr, "content")) { + if (!MATCHES(tagName, "meta")) { + continue; + } + } + // longdesc >> FRAME, IFRAME, IMG + else if (MATCHES(curAttr, "longdesc")) { + if (!MATCHES(tagName, "img") && + !MATCHES(tagName, "frame") && !MATCHES(tagName, "iframe")) { + continue; + } + } + // usemap >> IMG, INPUT, OBJECT + else if (MATCHES(curAttr, "usemap")) { + if (!MATCHES(tagName, "img") && + !MATCHES(tagName, "input") && !MATCHES(tagName, "object")) { + continue; + } + } + // action >> FORM + else if (MATCHES(curAttr, "action")) { + if (!MATCHES(tagName, "form")) { + continue; + } + } + // background >> BODY + else if (MATCHES(curAttr, "background")) { + if (!MATCHES(tagName, "body")) { + continue; + } + } + // codebase >> OBJECT, APPLET + else if (MATCHES(curAttr, "codebase")) { + if (!MATCHES(tagName, "meta")) { + continue; + } + } + // classid >> OBJECT + else if (MATCHES(curAttr, "classid")) { + if (!MATCHES(tagName, "object")) { + continue; + } + } + // data >> OBJECT + else if (MATCHES(curAttr, "data")) { + if (!MATCHES(tagName, "object")) { + continue; + } + } + // cite >> BLOCKQUOTE, DEL, INS, Q + else if (MATCHES(curAttr, "cite")) { + if (!MATCHES(tagName, "blockquote") && !MATCHES(tagName, "q") && + !MATCHES(tagName, "del") && !MATCHES(tagName, "ins")) { + continue; + } + } + // profile >> HEAD + else if (MATCHES(curAttr, "profile")) { + if (!MATCHES(tagName, "head")) { + continue; + } + } + // archive attribute on APPLET; warning, it contains a list of URIs. + else if (MATCHES(curAttr, "archive")) { + if (!MATCHES(tagName, "applet")) { + continue; + } + } + } + // Return a code to indicate that there are no more, + // to distinguish that case from real errors. + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +HTMLURIRefObject::RewriteAllURIs(const nsAString& aOldPat, + const nsAString& aNewPat, + bool aMakeRel) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HTMLURIRefObject::GetNode(nsIDOMNode** aNode) +{ + NS_ENSURE_TRUE(mNode, NS_ERROR_NOT_INITIALIZED); + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + *aNode = mNode.get(); + NS_ADDREF(*aNode); + return NS_OK; +} + +NS_IMETHODIMP +HTMLURIRefObject::SetNode(nsIDOMNode* aNode) +{ + mNode = aNode; + nsAutoString dummyURI; + if (NS_SUCCEEDED(GetNextURI(dummyURI))) { + mCurAttrIndex = 0; // Reset so we'll get the first node next time + return NS_OK; + } + + // If there weren't any URIs in the attributes, + // then don't accept this node. + mNode = nullptr; + return NS_ERROR_INVALID_ARG; +} + +} // namespace mozilla + +nsresult NS_NewHTMLURIRefObject(nsIURIRefObject** aResult, nsIDOMNode* aNode) +{ + RefPtr<mozilla::HTMLURIRefObject> refObject = new mozilla::HTMLURIRefObject(); + nsresult rv = refObject->SetNode(aNode); + if (NS_FAILED(rv)) { + *aResult = 0; + return rv; + } + refObject.forget(aResult); + return NS_OK; +} diff --git a/editor/libeditor/HTMLURIRefObject.h b/editor/libeditor/HTMLURIRefObject.h new file mode 100644 index 000000000..47f7f1868 --- /dev/null +++ b/editor/libeditor/HTMLURIRefObject.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#ifndef HTMLURIRefObject_h +#define HTMLURIRefObject_h + +#include "nsCOMPtr.h" +#include "nsISupportsImpl.h" +#include "nsIURIRefObject.h" +#include "nscore.h" + +#define NS_URI_REF_OBJECT_CID \ +{ /* {bdd79df6-1dd1-11b2-b29c-c3d63a58f1d2} */ \ + 0xbdd79df6, 0x1dd1, 0x11b2, \ + { 0xb2, 0x9c, 0xc3, 0xd6, 0x3a, 0x58, 0xf1, 0xd2 } \ +} + +class nsIDOMMozNamedAttrMap; +class nsIDOMNode; + +namespace mozilla { + +class HTMLURIRefObject final : public nsIURIRefObject +{ +public: + HTMLURIRefObject(); + + // Interfaces for addref and release and queryinterface + NS_DECL_ISUPPORTS + + NS_DECL_NSIURIREFOBJECT + +protected: + virtual ~HTMLURIRefObject(); + + nsCOMPtr<nsIDOMNode> mNode; + nsCOMPtr<nsIDOMMozNamedAttrMap> mAttributes; + uint32_t mCurAttrIndex; + uint32_t mAttributeCnt; +}; + +} // namespace mozilla + +nsresult NS_NewHTMLURIRefObject(nsIURIRefObject** aResult, nsIDOMNode* aNode); + +#endif // #ifndef HTMLURIRefObject_h diff --git a/editor/libeditor/InsertNodeTransaction.cpp b/editor/libeditor/InsertNodeTransaction.cpp new file mode 100644 index 000000000..6af33b74f --- /dev/null +++ b/editor/libeditor/InsertNodeTransaction.cpp @@ -0,0 +1,97 @@ +/* -*- 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 "InsertNodeTransaction.h" + +#include "mozilla/EditorBase.h" // for EditorBase + +#include "mozilla/dom/Selection.h" // for Selection + +#include "nsAString.h" +#include "nsDebug.h" // for NS_ENSURE_TRUE, etc. +#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc. +#include "nsIContent.h" // for nsIContent +#include "nsMemory.h" // for nsMemory +#include "nsReadableUtils.h" // for ToNewCString +#include "nsString.h" // for nsString + +namespace mozilla { + +using namespace dom; + +InsertNodeTransaction::InsertNodeTransaction(nsIContent& aNode, + nsINode& aParent, + int32_t aOffset, + EditorBase& aEditorBase) + : mNode(&aNode) + , mParent(&aParent) + , mOffset(aOffset) + , mEditorBase(aEditorBase) +{ +} + +InsertNodeTransaction::~InsertNodeTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(InsertNodeTransaction, EditTransactionBase, + mNode, + mParent) + +NS_IMPL_ADDREF_INHERITED(InsertNodeTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(InsertNodeTransaction, EditTransactionBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertNodeTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +NS_IMETHODIMP +InsertNodeTransaction::DoTransaction() +{ + MOZ_ASSERT(mNode && mParent); + + uint32_t count = mParent->GetChildCount(); + if (mOffset > static_cast<int32_t>(count) || mOffset == -1) { + // -1 is sentinel value meaning "append at end" + mOffset = count; + } + + // Note, it's ok for ref to be null. That means append. + nsCOMPtr<nsIContent> ref = mParent->GetChildAt(mOffset); + + mEditorBase.MarkNodeDirty(GetAsDOMNode(mNode)); + + ErrorResult rv; + mParent->InsertBefore(*mNode, ref, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + + // Only set selection to insertion point if editor gives permission + if (mEditorBase.GetShouldTxnSetSelection()) { + RefPtr<Selection> selection = mEditorBase.GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + // Place the selection just after the inserted element + selection->Collapse(mParent, mOffset + 1); + } else { + // Do nothing - DOM Range gravity will adjust selection + } + return NS_OK; +} + +NS_IMETHODIMP +InsertNodeTransaction::UndoTransaction() +{ + MOZ_ASSERT(mNode && mParent); + + ErrorResult rv; + mParent->RemoveChild(*mNode, rv); + return rv.StealNSResult(); +} + +NS_IMETHODIMP +InsertNodeTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("InsertNodeTransaction"); + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/InsertNodeTransaction.h b/editor/libeditor/InsertNodeTransaction.h new file mode 100644 index 000000000..5af7b8aff --- /dev/null +++ b/editor/libeditor/InsertNodeTransaction.h @@ -0,0 +1,58 @@ +/* -*- 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/. */ + +#ifndef InsertNodeTransaction_h +#define InsertNodeTransaction_h + +#include "mozilla/EditTransactionBase.h" // for EditTransactionBase, etc. +#include "nsCOMPtr.h" // for nsCOMPtr +#include "nsCycleCollectionParticipant.h" +#include "nsIContent.h" // for nsIContent +#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED + +namespace mozilla { + +class EditorBase; + +/** + * A transaction that inserts a single element + */ +class InsertNodeTransaction final : public EditTransactionBase +{ +public: + /** + * Initialize the transaction. + * @param aNode The node to insert. + * @param aParent The node to insert into. + * @param aOffset The offset in aParent to insert aNode. + */ + InsertNodeTransaction(nsIContent& aNode, nsINode& aParent, int32_t aOffset, + EditorBase& aEditorBase); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InsertNodeTransaction, + EditTransactionBase) + + NS_DECL_EDITTRANSACTIONBASE + +protected: + virtual ~InsertNodeTransaction(); + + // The element to insert. + nsCOMPtr<nsIContent> mNode; + + // The node into which the new node will be inserted. + nsCOMPtr<nsINode> mParent; + + // The index in mParent for the new node. + int32_t mOffset; + + // The editor for this transaction. + EditorBase& mEditorBase; +}; + +} // namespace mozilla + +#endif // #ifndef InsertNodeTransaction_h diff --git a/editor/libeditor/InsertTextTransaction.cpp b/editor/libeditor/InsertTextTransaction.cpp new file mode 100644 index 000000000..009b2d023 --- /dev/null +++ b/editor/libeditor/InsertTextTransaction.cpp @@ -0,0 +1,125 @@ +/* -*- 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 "InsertTextTransaction.h" + +#include "mozilla/EditorBase.h" // mEditorBase +#include "mozilla/SelectionState.h" // RangeUpdater +#include "mozilla/dom/Selection.h" // Selection local var +#include "mozilla/dom/Text.h" // mTextNode +#include "nsAString.h" // nsAString parameter +#include "nsDebug.h" // for NS_ASSERTION, etc. +#include "nsError.h" // for NS_OK, etc. +#include "nsQueryObject.h" // for do_QueryObject + +namespace mozilla { + +using namespace dom; + +InsertTextTransaction::InsertTextTransaction(Text& aTextNode, + uint32_t aOffset, + const nsAString& aStringToInsert, + EditorBase& aEditorBase, + RangeUpdater* aRangeUpdater) + : mTextNode(&aTextNode) + , mOffset(aOffset) + , mStringToInsert(aStringToInsert) + , mEditorBase(aEditorBase) + , mRangeUpdater(aRangeUpdater) +{ +} + +InsertTextTransaction::~InsertTextTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(InsertTextTransaction, EditTransactionBase, + mTextNode) + +NS_IMPL_ADDREF_INHERITED(InsertTextTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(InsertTextTransaction, EditTransactionBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertTextTransaction) + if (aIID.Equals(NS_GET_IID(InsertTextTransaction))) { + foundInterface = static_cast<nsITransaction*>(this); + } else +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + + +NS_IMETHODIMP +InsertTextTransaction::DoTransaction() +{ + nsresult rv = mTextNode->InsertData(mOffset, mStringToInsert); + NS_ENSURE_SUCCESS(rv, rv); + + // Only set selection to insertion point if editor gives permission + if (mEditorBase.GetShouldTxnSetSelection()) { + RefPtr<Selection> selection = mEditorBase.GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + DebugOnly<nsresult> rv = + selection->Collapse(mTextNode, mOffset + mStringToInsert.Length()); + NS_ASSERTION(NS_SUCCEEDED(rv), + "Selection could not be collapsed after insert"); + } else { + // Do nothing - DOM Range gravity will adjust selection + } + mRangeUpdater->SelAdjInsertText(*mTextNode, mOffset, mStringToInsert); + + return NS_OK; +} + +NS_IMETHODIMP +InsertTextTransaction::UndoTransaction() +{ + return mTextNode->DeleteData(mOffset, mStringToInsert.Length()); +} + +NS_IMETHODIMP +InsertTextTransaction::Merge(nsITransaction* aTransaction, + bool* aDidMerge) +{ + if (!aTransaction || !aDidMerge) { + return NS_OK; + } + // Set out param default value + *aDidMerge = false; + + // If aTransaction is a InsertTextTransaction, and if the selection hasn't + // changed, then absorb it. + RefPtr<InsertTextTransaction> otherTransaction = do_QueryObject(aTransaction); + if (otherTransaction && IsSequentialInsert(*otherTransaction)) { + nsAutoString otherData; + otherTransaction->GetData(otherData); + mStringToInsert += otherData; + *aDidMerge = true; + } + + return NS_OK; +} + +NS_IMETHODIMP +InsertTextTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("InsertTextTransaction: "); + aString += mStringToInsert; + return NS_OK; +} + +/* ============ private methods ================== */ + +void +InsertTextTransaction::GetData(nsString& aResult) +{ + aResult = mStringToInsert; +} + +bool +InsertTextTransaction::IsSequentialInsert( + InsertTextTransaction& aOtherTransaction) +{ + return aOtherTransaction.mTextNode == mTextNode && + aOtherTransaction.mOffset == mOffset + mStringToInsert.Length(); +} + +} // namespace mozilla diff --git a/editor/libeditor/InsertTextTransaction.h b/editor/libeditor/InsertTextTransaction.h new file mode 100644 index 000000000..f97f20e37 --- /dev/null +++ b/editor/libeditor/InsertTextTransaction.h @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +#ifndef InsertTextTransaction_h +#define InsertTextTransaction_h + +#include "mozilla/EditTransactionBase.h" // base class +#include "nsCycleCollectionParticipant.h" // various macros +#include "nsID.h" // NS_DECLARE_STATIC_IID_ACCESSOR +#include "nsISupportsImpl.h" // NS_DECL_ISUPPORTS_INHERITED +#include "nsString.h" // nsString members +#include "nscore.h" // NS_IMETHOD, nsAString + +class nsITransaction; + +#define NS_INSERTTEXTTXN_IID \ +{ 0x8c9ad77f, 0x22a7, 0x4d01, \ + { 0xb1, 0x59, 0x8a, 0x0f, 0xdb, 0x1d, 0x08, 0xe9 } } + +namespace mozilla { + +class EditorBase; +class RangeUpdater; + +namespace dom { +class Text; +} // namespace dom + +/** + * A transaction that inserts text into a content node. + */ +class InsertTextTransaction final : public EditTransactionBase +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_INSERTTEXTTXN_IID) + + /** + * @param aElement The text content node. + * @param aOffset The location in aElement to do the insertion. + * @param aString The new text to insert. + * @param aPresShell Used to get and set the selection. + * @param aRangeUpdater The range updater + */ + InsertTextTransaction(dom::Text& aTextNode, uint32_t aOffset, + const nsAString& aString, EditorBase& aEditorBase, + RangeUpdater* aRangeUpdater); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InsertTextTransaction, + EditTransactionBase) + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override; + + /** + * Return the string data associated with this transaction. + */ + void GetData(nsString& aResult); + +private: + virtual ~InsertTextTransaction(); + + // Return true if aOtherTransaction immediately follows this transaction. + bool IsSequentialInsert(InsertTextTransaction& aOtherTrasaction); + + // The Text node to operate upon. + RefPtr<dom::Text> mTextNode; + + // The offset into mTextNode where the insertion is to take place. + uint32_t mOffset; + + // The text to insert into mTextNode at mOffset. + nsString mStringToInsert; + + // The editor, which we'll need to get the selection. + EditorBase& mEditorBase; + + RangeUpdater* mRangeUpdater; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(InsertTextTransaction, NS_INSERTTEXTTXN_IID) + +} // namespace mozilla + +#endif // #ifndef InsertTextTransaction_h diff --git a/editor/libeditor/InternetCiter.cpp b/editor/libeditor/InternetCiter.cpp new file mode 100644 index 000000000..ed9fa5549 --- /dev/null +++ b/editor/libeditor/InternetCiter.cpp @@ -0,0 +1,366 @@ +/* -*- 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 "InternetCiter.h" + +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsDebug.h" +#include "nsDependentSubstring.h" +#include "nsError.h" +#include "nsILineBreaker.h" +#include "nsLWBrkCIID.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringIterator.h" + +namespace mozilla { + +const char16_t gt ('>'); +const char16_t space (' '); +const char16_t nl ('\n'); +const char16_t cr('\r'); + +/** + * Mail citations using the Internet style: > This is a citation. + */ + +nsresult +InternetCiter::GetCiteString(const nsAString& aInString, + nsAString& aOutString) +{ + aOutString.Truncate(); + char16_t uch = nl; + + // Strip trailing new lines which will otherwise turn up + // as ugly quoted empty lines. + nsReadingIterator <char16_t> beginIter,endIter; + aInString.BeginReading(beginIter); + aInString.EndReading(endIter); + while(beginIter!= endIter && + (*endIter == cr || *endIter == nl)) { + --endIter; + } + + // Loop over the string: + while (beginIter != endIter) { + if (uch == nl) { + aOutString.Append(gt); + // No space between >: this is ">>> " style quoting, for + // compatibility with RFC 2646 and format=flowed. + if (*beginIter != gt) { + aOutString.Append(space); + } + } + + uch = *beginIter; + ++beginIter; + + aOutString += uch; + } + + if (uch != nl) { + aOutString += nl; + } + return NS_OK; +} + +nsresult +InternetCiter::StripCitesAndLinebreaks(const nsAString& aInString, + nsAString& aOutString, + bool aLinebreaksToo, + int32_t* aCiteLevel) +{ + if (aCiteLevel) { + *aCiteLevel = 0; + } + + aOutString.Truncate(); + nsReadingIterator <char16_t> beginIter,endIter; + aInString.BeginReading(beginIter); + aInString.EndReading(endIter); + while (beginIter!= endIter) { // loop over lines + // Clear out cites first, at the beginning of the line: + int32_t thisLineCiteLevel = 0; + while (beginIter!= endIter && + (*beginIter == gt || nsCRT::IsAsciiSpace(*beginIter))) { + if (*beginIter == gt) { + ++thisLineCiteLevel; + } + ++beginIter; + } + // Now copy characters until line end: + while (beginIter != endIter && (*beginIter != '\r' && *beginIter != '\n')) { + aOutString.Append(*beginIter); + ++beginIter; + } + if (aLinebreaksToo) { + aOutString.Append(char16_t(' ')); + } else { + aOutString.Append(char16_t('\n')); // DOM linebreaks, not NS_LINEBREAK + } + // Skip over any more consecutive linebreak-like characters: + while (beginIter != endIter && (*beginIter == '\r' || *beginIter == '\n')) { + ++beginIter; + } + // Done with this line -- update cite level + if (aCiteLevel && (thisLineCiteLevel > *aCiteLevel)) { + *aCiteLevel = thisLineCiteLevel; + } + } + return NS_OK; +} + +nsresult +InternetCiter::StripCites(const nsAString& aInString, + nsAString& aOutString) +{ + return StripCitesAndLinebreaks(aInString, aOutString, false, 0); +} + +static void AddCite(nsAString& aOutString, int32_t citeLevel) +{ + for (int32_t i = 0; i < citeLevel; ++i) { + aOutString.Append(gt); + } + if (citeLevel > 0) { + aOutString.Append(space); + } +} + +static inline void +BreakLine(nsAString& aOutString, uint32_t& outStringCol, + uint32_t citeLevel) +{ + aOutString.Append(nl); + if (citeLevel > 0) { + AddCite(aOutString, citeLevel); + outStringCol = citeLevel + 1; + } else { + outStringCol = 0; + } +} + +static inline bool IsSpace(char16_t c) +{ + const char16_t nbsp (0xa0); + return (nsCRT::IsAsciiSpace(c) || (c == nl) || (c == cr) || (c == nbsp)); +} + +nsresult +InternetCiter::Rewrap(const nsAString& aInString, + uint32_t aWrapCol, + uint32_t aFirstLineOffset, + bool aRespectNewlines, + nsAString& aOutString) +{ + // There shouldn't be returns in this string, only dom newlines. + // Check to make sure: +#ifdef DEBUG + int32_t cr = aInString.FindChar(char16_t('\r')); + NS_ASSERTION((cr < 0), "Rewrap: CR in string gotten from DOM!\n"); +#endif /* DEBUG */ + + aOutString.Truncate(); + + nsresult rv; + nsCOMPtr<nsILineBreaker> lineBreaker = do_GetService(NS_LBRK_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Loop over lines in the input string, rewrapping each one. + uint32_t length; + uint32_t posInString = 0; + uint32_t outStringCol = 0; + uint32_t citeLevel = 0; + const nsPromiseFlatString &tString = PromiseFlatString(aInString); + length = tString.Length(); + while (posInString < length) { + // Get the new cite level here since we're at the beginning of a line + uint32_t newCiteLevel = 0; + while (posInString < length && tString[posInString] == gt) { + ++newCiteLevel; + ++posInString; + while (posInString < length && tString[posInString] == space) { + ++posInString; + } + } + if (posInString >= length) { + break; + } + + // Special case: if this is a blank line, maintain a blank line + // (retain the original paragraph breaks) + if (tString[posInString] == nl && !aOutString.IsEmpty()) { + if (aOutString.Last() != nl) { + aOutString.Append(nl); + } + AddCite(aOutString, newCiteLevel); + aOutString.Append(nl); + + ++posInString; + outStringCol = 0; + continue; + } + + // If the cite level has changed, then start a new line with the + // new cite level (but if we're at the beginning of the string, + // don't bother). + if (newCiteLevel != citeLevel && posInString > newCiteLevel+1 && + outStringCol) { + BreakLine(aOutString, outStringCol, 0); + } + citeLevel = newCiteLevel; + + // Prepend the quote level to the out string if appropriate + if (!outStringCol) { + AddCite(aOutString, citeLevel); + outStringCol = citeLevel + (citeLevel ? 1 : 0); + } + // If it's not a cite, and we're not at the beginning of a line in + // the output string, add a space to separate new text from the + // previous text. + else if (outStringCol > citeLevel) { + aOutString.Append(space); + ++outStringCol; + } + + // find the next newline -- don't want to go farther than that + int32_t nextNewline = tString.FindChar(nl, posInString); + if (nextNewline < 0) { + nextNewline = length; + } + + // For now, don't wrap unquoted lines at all. + // This is because the plaintext edit window has already wrapped them + // by the time we get them for rewrap, yet when we call the line + // breaker, it will refuse to break backwards, and we'll end up + // with a line that's too long and gets displayed as a lone word + // on a line by itself. Need special logic to detect this case + // and break it ourselves without resorting to the line breaker. + if (!citeLevel) { + aOutString.Append(Substring(tString, posInString, + nextNewline-posInString)); + outStringCol += nextNewline - posInString; + if (nextNewline != (int32_t)length) { + aOutString.Append(nl); + outStringCol = 0; + } + posInString = nextNewline+1; + continue; + } + + // Otherwise we have to use the line breaker and loop + // over this line of the input string to get all of it: + while ((int32_t)posInString < nextNewline) { + // Skip over initial spaces: + while ((int32_t)posInString < nextNewline && + nsCRT::IsAsciiSpace(tString[posInString])) { + ++posInString; + } + + // If this is a short line, just append it and continue: + if (outStringCol + nextNewline - posInString <= aWrapCol-citeLevel-1) { + // If this short line is the final one in the in string, + // then we need to include the final newline, if any: + if (nextNewline+1 == (int32_t)length && tString[nextNewline-1] == nl) { + ++nextNewline; + } + // Trim trailing spaces: + int32_t lastRealChar = nextNewline; + while ((uint32_t)lastRealChar > posInString && + nsCRT::IsAsciiSpace(tString[lastRealChar-1])) { + --lastRealChar; + } + + aOutString += Substring(tString, + posInString, lastRealChar - posInString); + outStringCol += lastRealChar - posInString; + posInString = nextNewline + 1; + continue; + } + + int32_t eol = posInString + aWrapCol - citeLevel - outStringCol; + // eol is the prospective end of line. + // We'll first look backwards from there for a place to break. + // If it's already less than our current position, + // then our line is already too long, so break now. + if (eol <= (int32_t)posInString) { + BreakLine(aOutString, outStringCol, citeLevel); + continue; // continue inner loop, with outStringCol now at bol + } + + int32_t breakPt = 0; + // XXX Why this uses NS_ERROR_"BASE"? + rv = NS_ERROR_BASE; + if (lineBreaker) { + breakPt = lineBreaker->Prev(tString.get() + posInString, + length - posInString, eol + 1 - posInString); + if (breakPt == NS_LINEBREAKER_NEED_MORE_TEXT) { + // if we couldn't find a breakpoint looking backwards, + // and we're not starting a new line, then end this line + // and loop around again: + if (outStringCol > citeLevel + 1) { + BreakLine(aOutString, outStringCol, citeLevel); + continue; // continue inner loop, with outStringCol now at bol + } + + // Else try looking forwards: + breakPt = lineBreaker->Next(tString.get() + posInString, + length - posInString, eol - posInString); + + rv = breakPt == NS_LINEBREAKER_NEED_MORE_TEXT ? NS_ERROR_BASE : + NS_OK; + } else { + rv = NS_OK; + } + } + // If rv is okay, then breakPt is the place to break. + // If we get out here and rv is set, something went wrong with line + // breaker. Just break the line, hard. + if (NS_FAILED(rv)) { + breakPt = eol; + } + + // Special case: maybe we should have wrapped last time. + // If the first breakpoint here makes the current line too long, + // then if we already have text on the current line, + // break and loop around again. + // If we're at the beginning of the current line, though, + // don't force a break since the long word might be a url + // and breaking it would make it unclickable on the other end. + const int SLOP = 6; + if (outStringCol + breakPt > aWrapCol + SLOP && + outStringCol > citeLevel+1) { + BreakLine(aOutString, outStringCol, citeLevel); + continue; + } + + nsAutoString sub (Substring(tString, posInString, breakPt)); + // skip newlines or whitespace at the end of the string + int32_t subend = sub.Length(); + while (subend > 0 && IsSpace(sub[subend-1])) { + --subend; + } + sub.Left(sub, subend); + aOutString += sub; + outStringCol += sub.Length(); + // Advance past the whitespace which caused the wrap: + posInString += breakPt; + while (posInString < length && IsSpace(tString[posInString])) { + ++posInString; + } + + // Add a newline and the quote level to the out string + if (posInString < length) { // not for the last line, though + BreakLine(aOutString, outStringCol, citeLevel); + } + } // end inner loop within one line of aInString + } // end outer loop over lines of aInString + + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/InternetCiter.h b/editor/libeditor/InternetCiter.h new file mode 100644 index 000000000..d1a861678 --- /dev/null +++ b/editor/libeditor/InternetCiter.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +#ifndef InternetCiter_h +#define InternetCiter_h + +#include "nscore.h" +#include "nsStringFwd.h" + +namespace mozilla { + +/** + * Mail citations using standard Internet style. + */ +class InternetCiter final +{ +public: + static nsresult GetCiteString(const nsAString& aInString, + nsAString& aOutString); + + static nsresult StripCites(const nsAString& aInString, + nsAString& aOutString); + + static nsresult Rewrap(const nsAString& aInString, + uint32_t aWrapCol, uint32_t aFirstLineOffset, + bool aRespectNewlines, + nsAString& aOutString); + +protected: + static nsresult StripCitesAndLinebreaks(const nsAString& aInString, + nsAString& aOutString, + bool aLinebreaksToo, + int32_t* aCiteLevel); +}; + +} // namespace mozilla + +#endif // #ifndef InternetCiter_h diff --git a/editor/libeditor/JoinNodeTransaction.cpp b/editor/libeditor/JoinNodeTransaction.cpp new file mode 100644 index 000000000..228d1f4d6 --- /dev/null +++ b/editor/libeditor/JoinNodeTransaction.cpp @@ -0,0 +1,109 @@ +/* -*- 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 "JoinNodeTransaction.h" + +#include "mozilla/EditorBase.h" // for EditorBase +#include "nsAString.h" +#include "nsDebug.h" // for NS_ASSERTION, etc. +#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc. +#include "nsIContent.h" // for nsIContent +#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData +#include "nsIEditor.h" // for EditorBase::IsModifiableNode +#include "nsISupportsImpl.h" // for QueryInterface, etc. + +namespace mozilla { + +using namespace dom; + +JoinNodeTransaction::JoinNodeTransaction(EditorBase& aEditorBase, + nsINode& aLeftNode, + nsINode& aRightNode) + : mEditorBase(aEditorBase) + , mLeftNode(&aLeftNode) + , mRightNode(&aRightNode) + , mOffset(0) +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(JoinNodeTransaction, EditTransactionBase, + mLeftNode, + mRightNode, + mParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JoinNodeTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +nsresult +JoinNodeTransaction::CheckValidity() +{ + if (!mEditorBase.IsModifiableNode(mLeftNode->GetParentNode())) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +// After DoTransaction() and RedoTransaction(), the left node is removed from +// the content tree and right node remains. +NS_IMETHODIMP +JoinNodeTransaction::DoTransaction() +{ + // Get the parent node + nsCOMPtr<nsINode> leftParent = mLeftNode->GetParentNode(); + NS_ENSURE_TRUE(leftParent, NS_ERROR_NULL_POINTER); + + // Verify that mLeftNode and mRightNode have the same parent + if (leftParent != mRightNode->GetParentNode()) { + NS_ASSERTION(false, "Nodes do not have same parent"); + return NS_ERROR_INVALID_ARG; + } + + // Set this instance's mParent. Other methods will see a non-null mParent + // and know all is well + mParent = leftParent; + mOffset = mLeftNode->Length(); + + return mEditorBase.JoinNodesImpl(mRightNode, mLeftNode, mParent); +} + +//XXX: What if instead of split, we just deleted the unneeded children of +// mRight and re-inserted mLeft? +NS_IMETHODIMP +JoinNodeTransaction::UndoTransaction() +{ + MOZ_ASSERT(mParent); + + // First, massage the existing node so it is in its post-split state + ErrorResult rv; + if (mRightNode->GetAsText()) { + rv = mRightNode->GetAsText()->DeleteData(0, mOffset); + } else { + nsCOMPtr<nsIContent> child = mRightNode->GetFirstChild(); + for (uint32_t i = 0; i < mOffset; i++) { + if (rv.Failed()) { + return rv.StealNSResult(); + } + if (!child) { + return NS_ERROR_NULL_POINTER; + } + nsCOMPtr<nsIContent> nextSibling = child->GetNextSibling(); + mLeftNode->AppendChild(*child, rv); + child = nextSibling; + } + } + // Second, re-insert the left node into the tree + nsCOMPtr<nsINode> refNode = mRightNode; + mParent->InsertBefore(*mLeftNode, refNode, rv); + return rv.StealNSResult(); +} + +NS_IMETHODIMP +JoinNodeTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("JoinNodeTransaction"); + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/JoinNodeTransaction.h b/editor/libeditor/JoinNodeTransaction.h new file mode 100644 index 000000000..84208cb45 --- /dev/null +++ b/editor/libeditor/JoinNodeTransaction.h @@ -0,0 +1,68 @@ +/* -*- 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/. */ + +#ifndef JoinNodeTransaction_h +#define JoinNodeTransaction_h + +#include "mozilla/EditTransactionBase.h" // for EditTransactionBase, etc. +#include "nsCOMPtr.h" // for nsCOMPtr +#include "nsCycleCollectionParticipant.h" +#include "nsID.h" // for REFNSIID +#include "nscore.h" // for NS_IMETHOD + +class nsINode; + +namespace mozilla { + +class EditorBase; + +/** + * A transaction that joins two nodes E1 (left node) and E2 (right node) into a + * single node E. The children of E are the children of E1 followed by the + * children of E2. After DoTransaction() and RedoTransaction(), E1 is removed + * from the content tree and E2 remains. + */ +class JoinNodeTransaction final : public EditTransactionBase +{ +public: + /** + * @param aEditorBase The provider of core editing operations. + * @param aLeftNode The first of two nodes to join. + * @param aRightNode The second of two nodes to join. + */ + JoinNodeTransaction(EditorBase& aEditorBase, + nsINode& aLeftNode, nsINode& aRightNode); + + /** + * Call this after constructing to ensure the inputs are correct. + */ + nsresult CheckValidity(); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(JoinNodeTransaction, + EditTransactionBase) + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; + + NS_DECL_EDITTRANSACTIONBASE + +protected: + EditorBase& mEditorBase; + + // The nodes to operate upon. After the merge, mRightNode remains and + // mLeftNode is removed from the content tree. + nsCOMPtr<nsINode> mLeftNode; + nsCOMPtr<nsINode> mRightNode; + + // The offset into mNode where the children of mElement are split (for + // undo). mOffset is the index of the first child in the right node. -1 + // means the left node had no children. + uint32_t mOffset; + + // The parent node containing mLeftNode and mRightNode. + nsCOMPtr<nsINode> mParent; +}; + +} // namespace mozilla + +#endif // #ifndef JoinNodeTransaction_h diff --git a/editor/libeditor/PlaceholderTransaction.cpp b/editor/libeditor/PlaceholderTransaction.cpp new file mode 100644 index 000000000..1031b45ab --- /dev/null +++ b/editor/libeditor/PlaceholderTransaction.cpp @@ -0,0 +1,270 @@ +/* -*- 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 "PlaceholderTransaction.h" + +#include "CompositionTransaction.h" +#include "mozilla/EditorBase.h" +#include "mozilla/dom/Selection.h" +#include "nsGkAtoms.h" +#include "nsQueryObject.h" + +namespace mozilla { + +using namespace dom; + +PlaceholderTransaction::PlaceholderTransaction() + : mAbsorb(true) + , mForwarding(nullptr) + , mCompositionTransaction(nullptr) + , mCommitted(false) + , mEditorBase(nullptr) +{ +} + +PlaceholderTransaction::~PlaceholderTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(PlaceholderTransaction) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PlaceholderTransaction, + EditAggregateTransaction) + if (tmp->mStartSel) { + ImplCycleCollectionUnlink(*tmp->mStartSel); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndSel); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PlaceholderTransaction, + EditAggregateTransaction) + if (tmp->mStartSel) { + ImplCycleCollectionTraverse(cb, *tmp->mStartSel, "mStartSel", 0); + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndSel); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PlaceholderTransaction) + NS_INTERFACE_MAP_ENTRY(nsIAbsorbingTransaction) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(EditAggregateTransaction) + +NS_IMPL_ADDREF_INHERITED(PlaceholderTransaction, EditAggregateTransaction) +NS_IMPL_RELEASE_INHERITED(PlaceholderTransaction, EditAggregateTransaction) + +NS_IMETHODIMP +PlaceholderTransaction::Init(nsIAtom* aName, + SelectionState* aSelState, + EditorBase* aEditorBase) +{ + NS_ENSURE_TRUE(aEditorBase && aSelState, NS_ERROR_NULL_POINTER); + + mName = aName; + mStartSel = aSelState; + mEditorBase = aEditorBase; + return NS_OK; +} + +NS_IMETHODIMP +PlaceholderTransaction::DoTransaction() +{ + return NS_OK; +} + +NS_IMETHODIMP +PlaceholderTransaction::UndoTransaction() +{ + // Undo transactions. + nsresult rv = EditAggregateTransaction::UndoTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(mStartSel, NS_ERROR_NULL_POINTER); + + // now restore selection + RefPtr<Selection> selection = mEditorBase->GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + return mStartSel->RestoreSelection(selection); +} + +NS_IMETHODIMP +PlaceholderTransaction::RedoTransaction() +{ + // Redo transactions. + nsresult rv = EditAggregateTransaction::RedoTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + + // now restore selection + RefPtr<Selection> selection = mEditorBase->GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + return mEndSel.RestoreSelection(selection); +} + + +NS_IMETHODIMP +PlaceholderTransaction::Merge(nsITransaction* aTransaction, + bool* aDidMerge) +{ + NS_ENSURE_TRUE(aDidMerge && aTransaction, NS_ERROR_NULL_POINTER); + + // set out param default value + *aDidMerge=false; + + if (mForwarding) { + NS_NOTREACHED("tried to merge into a placeholder that was in forwarding mode!"); + return NS_ERROR_FAILURE; + } + + // check to see if aTransaction is one of the editor's + // private transactions. If not, we want to avoid merging + // the foreign transaction into our placeholder since we + // don't know what it does. + + nsCOMPtr<nsPIEditorTransaction> pTxn = do_QueryInterface(aTransaction); + NS_ENSURE_TRUE(pTxn, NS_OK); // it's foreign so just bail! + + // XXX: hack, not safe! need nsIEditTransaction! + EditTransactionBase* editTransactionBase = (EditTransactionBase*)aTransaction; + // determine if this incoming txn is a placeholder txn + nsCOMPtr<nsIAbsorbingTransaction> absorbingTransaction = + do_QueryObject(editTransactionBase); + + // We are absorbing all transactions if mAbsorb is lit. + if (mAbsorb) { + RefPtr<CompositionTransaction> otherTransaction = + do_QueryObject(aTransaction); + if (otherTransaction) { + // special handling for CompositionTransaction's: they need to merge with + // any previous CompositionTransaction in this placeholder, if possible. + if (!mCompositionTransaction) { + // this is the first IME txn in the placeholder + mCompositionTransaction = otherTransaction; + AppendChild(editTransactionBase); + } else { + bool didMerge; + mCompositionTransaction->Merge(otherTransaction, &didMerge); + if (!didMerge) { + // it wouldn't merge. Earlier IME txn is already committed and will + // not absorb further IME txns. So just stack this one after it + // and remember it as a candidate for further merges. + mCompositionTransaction = otherTransaction; + AppendChild(editTransactionBase); + } + } + } else if (!absorbingTransaction) { + // See bug 171243: just drop incoming placeholders on the floor. + // Their children will be swallowed by this preexisting one. + AppendChild(editTransactionBase); + } + *aDidMerge = true; +// RememberEndingSelection(); +// efficiency hack: no need to remember selection here, as we haven't yet +// finished the initial batch and we know we will be told when the batch ends. +// we can remeber the selection then. + } else { + // merge typing or IME or deletion transactions if the selection matches + if ((mName.get() == nsGkAtoms::TypingTxnName || + mName.get() == nsGkAtoms::IMETxnName || + mName.get() == nsGkAtoms::DeleteTxnName) && !mCommitted) { + if (absorbingTransaction) { + nsCOMPtr<nsIAtom> atom; + absorbingTransaction->GetTxnName(getter_AddRefs(atom)); + if (atom && atom == mName) { + // check if start selection of next placeholder matches + // end selection of this placeholder + bool isSame; + absorbingTransaction->StartSelectionEquals(&mEndSel, &isSame); + if (isSame) { + mAbsorb = true; // we need to start absorbing again + absorbingTransaction->ForwardEndBatchTo(this); + // AppendChild(editTransactionBase); + // see bug 171243: we don't need to merge placeholders + // into placeholders. We just reactivate merging in the pre-existing + // placeholder and drop the new one on the floor. The EndPlaceHolderBatch() + // call on the new placeholder will be forwarded to this older one. + RememberEndingSelection(); + *aDidMerge = true; + } + } + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +PlaceholderTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("PlaceholderTransaction: "); + + if (mName) { + nsAutoString name; + mName->ToString(name); + aString += name; + } + + return NS_OK; +} + +NS_IMETHODIMP +PlaceholderTransaction::GetTxnName(nsIAtom** aName) +{ + return GetName(aName); +} + +NS_IMETHODIMP +PlaceholderTransaction::StartSelectionEquals(SelectionState* aSelState, + bool* aResult) +{ + // determine if starting selection matches the given selection state. + // note that we only care about collapsed selections. + NS_ENSURE_TRUE(aResult && aSelState, NS_ERROR_NULL_POINTER); + if (!mStartSel->IsCollapsed() || !aSelState->IsCollapsed()) { + *aResult = false; + return NS_OK; + } + *aResult = mStartSel->IsEqual(aSelState); + return NS_OK; +} + +NS_IMETHODIMP +PlaceholderTransaction::EndPlaceHolderBatch() +{ + mAbsorb = false; + + if (mForwarding) { + nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryReferent(mForwarding); + if (plcTxn) { + plcTxn->EndPlaceHolderBatch(); + } + } + // remember our selection state. + return RememberEndingSelection(); +} + +NS_IMETHODIMP +PlaceholderTransaction::ForwardEndBatchTo( + nsIAbsorbingTransaction* aForwardingAddress) +{ + mForwarding = do_GetWeakReference(aForwardingAddress); + return NS_OK; +} + +NS_IMETHODIMP +PlaceholderTransaction::Commit() +{ + mCommitted = true; + return NS_OK; +} + +nsresult +PlaceholderTransaction::RememberEndingSelection() +{ + RefPtr<Selection> selection = mEditorBase->GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + mEndSel.SaveSelection(selection); + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/PlaceholderTransaction.h b/editor/libeditor/PlaceholderTransaction.h new file mode 100644 index 000000000..867a82ce3 --- /dev/null +++ b/editor/libeditor/PlaceholderTransaction.h @@ -0,0 +1,92 @@ +/* -*- 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/. */ + +#ifndef PlaceholderTransaction_h +#define PlaceholderTransaction_h + +#include "EditAggregateTransaction.h" +#include "mozilla/EditorUtils.h" +#include "nsIAbsorbingTransaction.h" +#include "nsIDOMNode.h" +#include "nsCOMPtr.h" +#include "nsWeakPtr.h" +#include "nsWeakReference.h" +#include "nsAutoPtr.h" + +namespace mozilla { + +class CompositionTransaction; + +/** + * An aggregate transaction that knows how to absorb all subsequent + * transactions with the same name. This transaction does not "Do" anything. + * But it absorbs other transactions via merge, and can undo/redo the + * transactions it has absorbed. + */ + +class PlaceholderTransaction final : public EditAggregateTransaction, + public nsIAbsorbingTransaction, + public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS_INHERITED + + PlaceholderTransaction(); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PlaceholderTransaction, + EditAggregateTransaction) +// ------------ EditAggregateTransaction ----------------------- + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override; + +// ------------ nsIAbsorbingTransaction ----------------------- + + NS_IMETHOD Init(nsIAtom* aName, SelectionState* aSelState, + EditorBase* aEditorBase) override; + + NS_IMETHOD GetTxnName(nsIAtom** aName) override; + + NS_IMETHOD StartSelectionEquals(SelectionState* aSelState, + bool* aResult) override; + + NS_IMETHOD EndPlaceHolderBatch() override; + + NS_IMETHOD ForwardEndBatchTo( + nsIAbsorbingTransaction* aForwardingAddress) override; + + NS_IMETHOD Commit() override; + + nsresult RememberEndingSelection(); + +protected: + virtual ~PlaceholderTransaction(); + + // Do we auto absorb any and all transaction? + bool mAbsorb; + nsWeakPtr mForwarding; + // First IME txn in this placeholder - used for IME merging. + mozilla::CompositionTransaction* mCompositionTransaction; + // Do we stop auto absorbing any matching placeholder transactions? + bool mCommitted; + + // These next two members store the state of the selection in a safe way. + // Selection at the start of the transaction is stored, as is the selection + // at the end. This is so that UndoTransaction() and RedoTransaction() can + // restore the selection properly. + + // Use a pointer because this is constructed before we exist. + nsAutoPtr<SelectionState> mStartSel; + SelectionState mEndSel; + + // The editor for this transaction. + EditorBase* mEditorBase; +}; + +} // namespace mozilla + +#endif // #ifndef PlaceholderTransaction_h diff --git a/editor/libeditor/SelectionState.cpp b/editor/libeditor/SelectionState.cpp new file mode 100644 index 000000000..f9ad5947a --- /dev/null +++ b/editor/libeditor/SelectionState.cpp @@ -0,0 +1,695 @@ +/* -*- 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/SelectionState.h" + +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc. +#include "mozilla/EditorUtils.h" // for EditorUtils +#include "mozilla/dom/Selection.h" // for Selection +#include "nsAString.h" // for nsAString_internal::Length +#include "nsCycleCollectionParticipant.h" +#include "nsDebug.h" // for NS_ENSURE_TRUE, etc. +#include "nsError.h" // for NS_OK, etc. +#include "nsIContent.h" // for nsIContent +#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData +#include "nsIDOMNode.h" // for nsIDOMNode +#include "nsISupportsImpl.h" // for nsRange::Release +#include "nsRange.h" // for nsRange + +namespace mozilla { + +using namespace dom; + +/****************************************************************************** + * mozilla::SelectionState + * + * Class for recording selection info. Stores selection as collection of + * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store + * ranges since dom gravity will possibly change the ranges. + ******************************************************************************/ +SelectionState::SelectionState() +{ +} + +SelectionState::~SelectionState() +{ + MakeEmpty(); +} + +void +SelectionState::SaveSelection(Selection* aSel) +{ + MOZ_ASSERT(aSel); + int32_t arrayCount = mArray.Length(); + int32_t rangeCount = aSel->RangeCount(); + + // if we need more items in the array, new them + if (arrayCount < rangeCount) { + for (int32_t i = arrayCount; i < rangeCount; i++) { + mArray.AppendElement(); + mArray[i] = new RangeItem(); + } + } else if (arrayCount > rangeCount) { + // else if we have too many, delete them + for (int32_t i = arrayCount - 1; i >= rangeCount; i--) { + mArray.RemoveElementAt(i); + } + } + + // now store the selection ranges + for (int32_t i = 0; i < rangeCount; i++) { + mArray[i]->StoreRange(aSel->GetRangeAt(i)); + } +} + +nsresult +SelectionState::RestoreSelection(Selection* aSel) +{ + NS_ENSURE_TRUE(aSel, NS_ERROR_NULL_POINTER); + + // clear out selection + aSel->RemoveAllRanges(); + + // set the selection ranges anew + size_t arrayCount = mArray.Length(); + for (size_t i = 0; i < arrayCount; i++) { + RefPtr<nsRange> range = mArray[i]->GetRange(); + NS_ENSURE_TRUE(range, NS_ERROR_UNEXPECTED); + + nsresult rv = aSel->AddRange(range); + if (NS_FAILED(rv)) { + return rv; + } + } + return NS_OK; +} + +bool +SelectionState::IsCollapsed() +{ + if (mArray.Length() != 1) { + return false; + } + RefPtr<nsRange> range = mArray[0]->GetRange(); + NS_ENSURE_TRUE(range, false); + bool bIsCollapsed = false; + range->GetCollapsed(&bIsCollapsed); + return bIsCollapsed; +} + +bool +SelectionState::IsEqual(SelectionState* aSelState) +{ + NS_ENSURE_TRUE(aSelState, false); + size_t myCount = mArray.Length(), itsCount = aSelState->mArray.Length(); + if (myCount != itsCount) { + return false; + } + if (!myCount) { + return false; + } + + for (size_t i = 0; i < myCount; i++) { + RefPtr<nsRange> myRange = mArray[i]->GetRange(); + RefPtr<nsRange> itsRange = aSelState->mArray[i]->GetRange(); + NS_ENSURE_TRUE(myRange && itsRange, false); + + int16_t compResult; + nsresult rv; + rv = myRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START, itsRange, &compResult); + if (NS_FAILED(rv) || compResult) { + return false; + } + rv = myRange->CompareBoundaryPoints(nsIDOMRange::END_TO_END, itsRange, &compResult); + if (NS_FAILED(rv) || compResult) { + return false; + } + } + // if we got here, they are equal + return true; +} + +void +SelectionState::MakeEmpty() +{ + // free any items in the array + mArray.Clear(); +} + +bool +SelectionState::IsEmpty() +{ + return mArray.IsEmpty(); +} + +/****************************************************************************** + * mozilla::RangeUpdater + * + * Class for updating nsRanges in response to editor actions. + ******************************************************************************/ + +RangeUpdater::RangeUpdater() + : mLock(false) +{ +} + +RangeUpdater::~RangeUpdater() +{ + // nothing to do, we don't own the items in our array. +} + +void +RangeUpdater::RegisterRangeItem(RangeItem* aRangeItem) +{ + if (!aRangeItem) { + return; + } + if (mArray.Contains(aRangeItem)) { + NS_ERROR("tried to register an already registered range"); + return; // don't register it again. It would get doubly adjusted. + } + mArray.AppendElement(aRangeItem); +} + +void +RangeUpdater::DropRangeItem(RangeItem* aRangeItem) +{ + if (!aRangeItem) { + return; + } + mArray.RemoveElement(aRangeItem); +} + +nsresult +RangeUpdater::RegisterSelectionState(SelectionState& aSelState) +{ + size_t theCount = aSelState.mArray.Length(); + if (theCount < 1) { + return NS_ERROR_FAILURE; + } + + for (size_t i = 0; i < theCount; i++) { + RegisterRangeItem(aSelState.mArray[i]); + } + + return NS_OK; +} + +nsresult +RangeUpdater::DropSelectionState(SelectionState& aSelState) +{ + size_t theCount = aSelState.mArray.Length(); + if (theCount < 1) { + return NS_ERROR_FAILURE; + } + + for (size_t i = 0; i < theCount; i++) { + DropRangeItem(aSelState.mArray[i]); + } + + return NS_OK; +} + +// gravity methods: + +nsresult +RangeUpdater::SelAdjCreateNode(nsINode* aParent, + int32_t aPosition) +{ + if (mLock) { + // lock set by Will/DidReplaceParent, etc... + return NS_OK; + } + NS_ENSURE_TRUE(aParent, NS_ERROR_NULL_POINTER); + size_t count = mArray.Length(); + if (!count) { + return NS_OK; + } + + for (size_t i = 0; i < count; i++) { + RangeItem* item = mArray[i]; + NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER); + + if (item->startNode == aParent && item->startOffset > aPosition) { + item->startOffset++; + } + if (item->endNode == aParent && item->endOffset > aPosition) { + item->endOffset++; + } + } + return NS_OK; +} + +nsresult +RangeUpdater::SelAdjCreateNode(nsIDOMNode* aParent, + int32_t aPosition) +{ + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + return SelAdjCreateNode(parent, aPosition); +} + +nsresult +RangeUpdater::SelAdjInsertNode(nsINode* aParent, + int32_t aPosition) +{ + return SelAdjCreateNode(aParent, aPosition); +} + +nsresult +RangeUpdater::SelAdjInsertNode(nsIDOMNode* aParent, + int32_t aPosition) +{ + return SelAdjCreateNode(aParent, aPosition); +} + +void +RangeUpdater::SelAdjDeleteNode(nsINode* aNode) +{ + if (mLock) { + // lock set by Will/DidReplaceParent, etc... + return; + } + MOZ_ASSERT(aNode); + size_t count = mArray.Length(); + if (!count) { + return; + } + + nsCOMPtr<nsINode> parent = aNode->GetParentNode(); + int32_t offset = parent ? parent->IndexOf(aNode) : -1; + + // check for range endpoints that are after aNode and in the same parent + for (size_t i = 0; i < count; i++) { + RangeItem* item = mArray[i]; + MOZ_ASSERT(item); + + if (item->startNode == parent && item->startOffset > offset) { + item->startOffset--; + } + if (item->endNode == parent && item->endOffset > offset) { + item->endOffset--; + } + + // check for range endpoints that are in aNode + if (item->startNode == aNode) { + item->startNode = parent; + item->startOffset = offset; + } + if (item->endNode == aNode) { + item->endNode = parent; + item->endOffset = offset; + } + + // check for range endpoints that are in descendants of aNode + nsCOMPtr<nsINode> oldStart; + if (EditorUtils::IsDescendantOf(item->startNode, aNode)) { + oldStart = item->startNode; // save for efficiency hack below. + item->startNode = parent; + item->startOffset = offset; + } + + // avoid having to call IsDescendantOf() for common case of range startnode == range endnode. + if (item->endNode == oldStart || + EditorUtils::IsDescendantOf(item->endNode, aNode)) { + item->endNode = parent; + item->endOffset = offset; + } + } +} + +void +RangeUpdater::SelAdjDeleteNode(nsIDOMNode* aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE_VOID(node); + return SelAdjDeleteNode(node); +} + +nsresult +RangeUpdater::SelAdjSplitNode(nsIContent& aOldRightNode, + int32_t aOffset, + nsIContent* aNewLeftNode) +{ + if (mLock) { + // lock set by Will/DidReplaceParent, etc... + return NS_OK; + } + NS_ENSURE_TRUE(aNewLeftNode, NS_ERROR_NULL_POINTER); + size_t count = mArray.Length(); + if (!count) { + return NS_OK; + } + + nsCOMPtr<nsINode> parent = aOldRightNode.GetParentNode(); + int32_t offset = parent ? parent->IndexOf(&aOldRightNode) : -1; + + // first part is same as inserting aNewLeftnode + nsresult rv = SelAdjInsertNode(parent, offset - 1); + NS_ENSURE_SUCCESS(rv, rv); + + // next step is to check for range enpoints inside aOldRightNode + for (size_t i = 0; i < count; i++) { + RangeItem* item = mArray[i]; + NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER); + + if (item->startNode == &aOldRightNode) { + if (item->startOffset > aOffset) { + item->startOffset -= aOffset; + } else { + item->startNode = aNewLeftNode; + } + } + if (item->endNode == &aOldRightNode) { + if (item->endOffset > aOffset) { + item->endOffset -= aOffset; + } else { + item->endNode = aNewLeftNode; + } + } + } + return NS_OK; +} + +nsresult +RangeUpdater::SelAdjJoinNodes(nsINode& aLeftNode, + nsINode& aRightNode, + nsINode& aParent, + int32_t aOffset, + int32_t aOldLeftNodeLength) +{ + if (mLock) { + // lock set by Will/DidReplaceParent, etc... + return NS_OK; + } + size_t count = mArray.Length(); + if (!count) { + return NS_OK; + } + + for (size_t i = 0; i < count; i++) { + RangeItem* item = mArray[i]; + NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER); + + if (item->startNode == &aParent) { + // adjust start point in aParent + if (item->startOffset > aOffset) { + item->startOffset--; + } else if (item->startOffset == aOffset) { + // join keeps right hand node + item->startNode = &aRightNode; + item->startOffset = aOldLeftNodeLength; + } + } else if (item->startNode == &aRightNode) { + // adjust start point in aRightNode + item->startOffset += aOldLeftNodeLength; + } else if (item->startNode == &aLeftNode) { + // adjust start point in aLeftNode + item->startNode = &aRightNode; + } + + if (item->endNode == &aParent) { + // adjust end point in aParent + if (item->endOffset > aOffset) { + item->endOffset--; + } else if (item->endOffset == aOffset) { + // join keeps right hand node + item->endNode = &aRightNode; + item->endOffset = aOldLeftNodeLength; + } + } else if (item->endNode == &aRightNode) { + // adjust end point in aRightNode + item->endOffset += aOldLeftNodeLength; + } else if (item->endNode == &aLeftNode) { + // adjust end point in aLeftNode + item->endNode = &aRightNode; + } + } + + return NS_OK; +} + +void +RangeUpdater::SelAdjInsertText(Text& aTextNode, + int32_t aOffset, + const nsAString& aString) +{ + if (mLock) { + // lock set by Will/DidReplaceParent, etc... + return; + } + + size_t count = mArray.Length(); + if (!count) { + return; + } + + size_t len = aString.Length(); + for (size_t i = 0; i < count; i++) { + RangeItem* item = mArray[i]; + MOZ_ASSERT(item); + + if (item->startNode == &aTextNode && item->startOffset > aOffset) { + item->startOffset += len; + } + if (item->endNode == &aTextNode && item->endOffset > aOffset) { + item->endOffset += len; + } + } + return; +} + +nsresult +RangeUpdater::SelAdjDeleteText(nsIContent* aTextNode, + int32_t aOffset, + int32_t aLength) +{ + if (mLock) { + // lock set by Will/DidReplaceParent, etc... + return NS_OK; + } + + size_t count = mArray.Length(); + if (!count) { + return NS_OK; + } + NS_ENSURE_TRUE(aTextNode, NS_ERROR_NULL_POINTER); + + for (size_t i = 0; i < count; i++) { + RangeItem* item = mArray[i]; + NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER); + + if (item->startNode == aTextNode && item->startOffset > aOffset) { + item->startOffset -= aLength; + if (item->startOffset < 0) { + item->startOffset = 0; + } + } + if (item->endNode == aTextNode && item->endOffset > aOffset) { + item->endOffset -= aLength; + if (item->endOffset < 0) { + item->endOffset = 0; + } + } + } + return NS_OK; +} + +nsresult +RangeUpdater::SelAdjDeleteText(nsIDOMCharacterData* aTextNode, + int32_t aOffset, + int32_t aLength) +{ + nsCOMPtr<nsIContent> textNode = do_QueryInterface(aTextNode); + return SelAdjDeleteText(textNode, aOffset, aLength); +} + +nsresult +RangeUpdater::WillReplaceContainer() +{ + if (mLock) { + return NS_ERROR_UNEXPECTED; + } + mLock = true; + return NS_OK; +} + +nsresult +RangeUpdater::DidReplaceContainer(Element* aOriginalNode, + Element* aNewNode) +{ + NS_ENSURE_TRUE(mLock, NS_ERROR_UNEXPECTED); + mLock = false; + + NS_ENSURE_TRUE(aOriginalNode && aNewNode, NS_ERROR_NULL_POINTER); + size_t count = mArray.Length(); + if (!count) { + return NS_OK; + } + + for (size_t i = 0; i < count; i++) { + RangeItem* item = mArray[i]; + NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER); + + if (item->startNode == aOriginalNode) { + item->startNode = aNewNode; + } + if (item->endNode == aOriginalNode) { + item->endNode = aNewNode; + } + } + return NS_OK; +} + +nsresult +RangeUpdater::WillRemoveContainer() +{ + if (mLock) { + return NS_ERROR_UNEXPECTED; + } + mLock = true; + return NS_OK; +} + +nsresult +RangeUpdater::DidRemoveContainer(nsINode* aNode, + nsINode* aParent, + int32_t aOffset, + uint32_t aNodeOrigLen) +{ + NS_ENSURE_TRUE(mLock, NS_ERROR_UNEXPECTED); + mLock = false; + + NS_ENSURE_TRUE(aNode && aParent, NS_ERROR_NULL_POINTER); + size_t count = mArray.Length(); + if (!count) { + return NS_OK; + } + + for (size_t i = 0; i < count; i++) { + RangeItem* item = mArray[i]; + NS_ENSURE_TRUE(item, NS_ERROR_NULL_POINTER); + + if (item->startNode == aNode) { + item->startNode = aParent; + item->startOffset += aOffset; + } else if (item->startNode == aParent && item->startOffset > aOffset) { + item->startOffset += (int32_t)aNodeOrigLen - 1; + } + + if (item->endNode == aNode) { + item->endNode = aParent; + item->endOffset += aOffset; + } else if (item->endNode == aParent && item->endOffset > aOffset) { + item->endOffset += (int32_t)aNodeOrigLen - 1; + } + } + return NS_OK; +} + +nsresult +RangeUpdater::DidRemoveContainer(nsIDOMNode* aNode, + nsIDOMNode* aParent, + int32_t aOffset, + uint32_t aNodeOrigLen) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + return DidRemoveContainer(node, parent, aOffset, aNodeOrigLen); +} + +nsresult +RangeUpdater::WillInsertContainer() +{ + if (mLock) { + return NS_ERROR_UNEXPECTED; + } + mLock = true; + return NS_OK; +} + +nsresult +RangeUpdater::DidInsertContainer() +{ + NS_ENSURE_TRUE(mLock, NS_ERROR_UNEXPECTED); + mLock = false; + return NS_OK; +} + +void +RangeUpdater::WillMoveNode() +{ + mLock = true; +} + +void +RangeUpdater::DidMoveNode(nsINode* aOldParent, int32_t aOldOffset, + nsINode* aNewParent, int32_t aNewOffset) +{ + MOZ_ASSERT(aOldParent); + MOZ_ASSERT(aNewParent); + NS_ENSURE_TRUE_VOID(mLock); + mLock = false; + + for (size_t i = 0, count = mArray.Length(); i < count; ++i) { + RangeItem* item = mArray[i]; + NS_ENSURE_TRUE_VOID(item); + + // like a delete in aOldParent + if (item->startNode == aOldParent && item->startOffset > aOldOffset) { + item->startOffset--; + } + if (item->endNode == aOldParent && item->endOffset > aOldOffset) { + item->endOffset--; + } + + // and like an insert in aNewParent + if (item->startNode == aNewParent && item->startOffset > aNewOffset) { + item->startOffset++; + } + if (item->endNode == aNewParent && item->endOffset > aNewOffset) { + item->endOffset++; + } + } +} + +/****************************************************************************** + * mozilla::RangeItem + * + * Helper struct for SelectionState. This stores range endpoints. + ******************************************************************************/ + +RangeItem::RangeItem() +{ +} + +RangeItem::~RangeItem() +{ +} + +NS_IMPL_CYCLE_COLLECTION(RangeItem, startNode, endNode) +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(RangeItem, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(RangeItem, Release) + +void +RangeItem::StoreRange(nsRange* aRange) +{ + MOZ_ASSERT(aRange); + startNode = aRange->GetStartParent(); + startOffset = aRange->StartOffset(); + endNode = aRange->GetEndParent(); + endOffset = aRange->EndOffset(); +} + +already_AddRefed<nsRange> +RangeItem::GetRange() +{ + RefPtr<nsRange> range = new nsRange(startNode); + if (NS_FAILED(range->Set(startNode, startOffset, endNode, endOffset))) { + return nullptr; + } + return range.forget(); +} + +} // namespace mozilla diff --git a/editor/libeditor/SelectionState.h b/editor/libeditor/SelectionState.h new file mode 100644 index 000000000..36e7b7769 --- /dev/null +++ b/editor/libeditor/SelectionState.h @@ -0,0 +1,357 @@ +/* -*- 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/. */ + +#ifndef mozilla_SelectionState_h +#define mozilla_SelectionState_h + +#include "nsCOMPtr.h" +#include "nsIDOMNode.h" +#include "nsINode.h" +#include "nsTArray.h" +#include "nscore.h" + +class nsCycleCollectionTraversalCallback; +class nsIDOMCharacterData; +class nsRange; +namespace mozilla { +class RangeUpdater; +namespace dom { +class Selection; +class Text; +} // namespace dom + +/** + * A helper struct for saving/setting ranges. + */ +struct RangeItem final +{ + RangeItem(); + +private: + // Private destructor, to discourage deletion outside of Release(): + ~RangeItem(); + +public: + void StoreRange(nsRange* aRange); + already_AddRefed<nsRange> GetRange(); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem) + + nsCOMPtr<nsINode> startNode; + int32_t startOffset; + nsCOMPtr<nsINode> endNode; + int32_t endOffset; +}; + +/** + * mozilla::SelectionState + * + * Class for recording selection info. Stores selection as collection of + * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store + * ranges since dom gravity will possibly change the ranges. + */ + +class SelectionState final +{ +public: + SelectionState(); + ~SelectionState(); + + void SaveSelection(dom::Selection *aSel); + nsresult RestoreSelection(dom::Selection* aSel); + bool IsCollapsed(); + bool IsEqual(SelectionState *aSelState); + void MakeEmpty(); + bool IsEmpty(); +private: + nsTArray<RefPtr<RangeItem>> mArray; + + friend class RangeUpdater; + friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&, + SelectionState&, + const char*, + uint32_t); + friend void ImplCycleCollectionUnlink(SelectionState&); +}; + +inline void +ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + SelectionState& aField, + const char* aName, + uint32_t aFlags = 0) +{ + ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags); +} + +inline void +ImplCycleCollectionUnlink(SelectionState& aField) +{ + ImplCycleCollectionUnlink(aField.mArray); +} + +class RangeUpdater final +{ +public: + RangeUpdater(); + ~RangeUpdater(); + + void RegisterRangeItem(RangeItem* aRangeItem); + void DropRangeItem(RangeItem* aRangeItem); + nsresult RegisterSelectionState(SelectionState& aSelState); + nsresult DropSelectionState(SelectionState& aSelState); + + // editor selection gravity routines. Note that we can't always depend on + // DOM Range gravity to do what we want to the "real" selection. For instance, + // if you move a node, that corresponds to deleting it and reinserting it. + // DOM Range gravity will promote the selection out of the node on deletion, + // which is not what you want if you know you are reinserting it. + nsresult SelAdjCreateNode(nsINode* aParent, int32_t aPosition); + nsresult SelAdjCreateNode(nsIDOMNode* aParent, int32_t aPosition); + nsresult SelAdjInsertNode(nsINode* aParent, int32_t aPosition); + nsresult SelAdjInsertNode(nsIDOMNode* aParent, int32_t aPosition); + void SelAdjDeleteNode(nsINode* aNode); + void SelAdjDeleteNode(nsIDOMNode* aNode); + nsresult SelAdjSplitNode(nsIContent& aOldRightNode, int32_t aOffset, + nsIContent* aNewLeftNode); + nsresult SelAdjJoinNodes(nsINode& aLeftNode, + nsINode& aRightNode, + nsINode& aParent, + int32_t aOffset, + int32_t aOldLeftNodeLength); + void SelAdjInsertText(dom::Text& aTextNode, int32_t aOffset, + const nsAString &aString); + nsresult SelAdjDeleteText(nsIContent* aTextNode, int32_t aOffset, + int32_t aLength); + nsresult SelAdjDeleteText(nsIDOMCharacterData* aTextNode, + int32_t aOffset, int32_t aLength); + // the following gravity routines need will/did sandwiches, because the other + // gravity routines will be called inside of these sandwiches, but should be + // ignored. + nsresult WillReplaceContainer(); + nsresult DidReplaceContainer(dom::Element* aOriginalNode, + dom::Element* aNewNode); + nsresult WillRemoveContainer(); + nsresult DidRemoveContainer(nsINode* aNode, nsINode* aParent, + int32_t aOffset, uint32_t aNodeOrigLen); + nsresult DidRemoveContainer(nsIDOMNode* aNode, nsIDOMNode* aParent, + int32_t aOffset, uint32_t aNodeOrigLen); + nsresult WillInsertContainer(); + nsresult DidInsertContainer(); + void WillMoveNode(); + void DidMoveNode(nsINode* aOldParent, int32_t aOldOffset, + nsINode* aNewParent, int32_t aNewOffset); + +private: + friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&, + RangeUpdater&, + const char*, + uint32_t); + friend void ImplCycleCollectionUnlink(RangeUpdater& aField); + + nsTArray<RefPtr<RangeItem>> mArray; + bool mLock; +}; + +inline void +ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + RangeUpdater& aField, + const char* aName, + uint32_t aFlags = 0) +{ + ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags); +} + +inline void +ImplCycleCollectionUnlink(RangeUpdater& aField) +{ + ImplCycleCollectionUnlink(aField.mArray); +} + +/** + * Helper class for using SelectionState. Stack based class for doing + * preservation of dom points across editor actions. + */ + +class MOZ_STACK_CLASS AutoTrackDOMPoint final +{ +private: + RangeUpdater& mRangeUpdater; + // Allow tracking either nsIDOMNode or nsINode until nsIDOMNode is gone + nsCOMPtr<nsINode>* mNode; + nsCOMPtr<nsIDOMNode>* mDOMNode; + int32_t* mOffset; + RefPtr<RangeItem> mRangeItem; + +public: + AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, + nsCOMPtr<nsINode>* aNode, int32_t* aOffset) + : mRangeUpdater(aRangeUpdater) + , mNode(aNode) + , mDOMNode(nullptr) + , mOffset(aOffset) + { + mRangeItem = new RangeItem(); + mRangeItem->startNode = *mNode; + mRangeItem->endNode = *mNode; + mRangeItem->startOffset = *mOffset; + mRangeItem->endOffset = *mOffset; + mRangeUpdater.RegisterRangeItem(mRangeItem); + } + + AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, + nsCOMPtr<nsIDOMNode>* aNode, int32_t* aOffset) + : mRangeUpdater(aRangeUpdater) + , mNode(nullptr) + , mDOMNode(aNode) + , mOffset(aOffset) + { + mRangeItem = new RangeItem(); + mRangeItem->startNode = do_QueryInterface(*mDOMNode); + mRangeItem->endNode = do_QueryInterface(*mDOMNode); + mRangeItem->startOffset = *mOffset; + mRangeItem->endOffset = *mOffset; + mRangeUpdater.RegisterRangeItem(mRangeItem); + } + + ~AutoTrackDOMPoint() + { + mRangeUpdater.DropRangeItem(mRangeItem); + if (mNode) { + *mNode = mRangeItem->startNode; + } else { + *mDOMNode = GetAsDOMNode(mRangeItem->startNode); + } + *mOffset = mRangeItem->startOffset; + } +}; + +/** + * Another helper class for SelectionState. Stack based class for doing + * Will/DidReplaceContainer() + */ + +class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final +{ +private: + RangeUpdater& mRangeUpdater; + dom::Element* mOriginalElement; + dom::Element* mNewElement; + +public: + AutoReplaceContainerSelNotify(RangeUpdater& aRangeUpdater, + dom::Element* aOriginalElement, + dom::Element* aNewElement) + : mRangeUpdater(aRangeUpdater) + , mOriginalElement(aOriginalElement) + , mNewElement(aNewElement) + { + mRangeUpdater.WillReplaceContainer(); + } + + ~AutoReplaceContainerSelNotify() + { + mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement); + } +}; + +/** + * Another helper class for SelectionState. Stack based class for doing + * Will/DidRemoveContainer() + */ + +class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final +{ +private: + RangeUpdater& mRangeUpdater; + nsIDOMNode* mNode; + nsIDOMNode* mParent; + int32_t mOffset; + uint32_t mNodeOrigLen; + +public: + AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater, + nsINode* aNode, + nsINode* aParent, + int32_t aOffset, + uint32_t aNodeOrigLen) + : mRangeUpdater(aRangeUpdater) + , mNode(aNode->AsDOMNode()) + , mParent(aParent->AsDOMNode()) + , mOffset(aOffset) + , mNodeOrigLen(aNodeOrigLen) + { + mRangeUpdater.WillRemoveContainer(); + } + + ~AutoRemoveContainerSelNotify() + { + mRangeUpdater.DidRemoveContainer(mNode, mParent, mOffset, mNodeOrigLen); + } +}; + +/** + * Another helper class for SelectionState. Stack based class for doing + * Will/DidInsertContainer() + */ + +class MOZ_STACK_CLASS AutoInsertContainerSelNotify final +{ +private: + RangeUpdater& mRangeUpdater; + +public: + explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater) + : mRangeUpdater(aRangeUpdater) + { + mRangeUpdater.WillInsertContainer(); + } + + ~AutoInsertContainerSelNotify() + { + mRangeUpdater.DidInsertContainer(); + } +}; + +/** + * Another helper class for SelectionState. Stack based class for doing + * Will/DidMoveNode() + */ + +class MOZ_STACK_CLASS AutoMoveNodeSelNotify final +{ +private: + RangeUpdater& mRangeUpdater; + nsINode* mOldParent; + nsINode* mNewParent; + int32_t mOldOffset; + int32_t mNewOffset; + +public: + AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater, + nsINode* aOldParent, + int32_t aOldOffset, + nsINode* aNewParent, + int32_t aNewOffset) + : mRangeUpdater(aRangeUpdater) + , mOldParent(aOldParent) + , mNewParent(aNewParent) + , mOldOffset(aOldOffset) + , mNewOffset(aNewOffset) + { + MOZ_ASSERT(aOldParent); + MOZ_ASSERT(aNewParent); + mRangeUpdater.WillMoveNode(); + } + + ~AutoMoveNodeSelNotify() + { + mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset); + } +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_SelectionState_h diff --git a/editor/libeditor/SetDocumentTitleTransaction.cpp b/editor/libeditor/SetDocumentTitleTransaction.cpp new file mode 100644 index 000000000..88403c0ad --- /dev/null +++ b/editor/libeditor/SetDocumentTitleTransaction.cpp @@ -0,0 +1,229 @@ +/* -*- 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 "SetDocumentTitleTransaction.h" +#include "mozilla/dom/Element.h" // for Element +#include "nsAString.h" +#include "nsCOMPtr.h" // for nsCOMPtr, getter_AddRefs, etc. +#include "nsDebug.h" // for NS_ENSURE_SUCCESS, etc. +#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc. +#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData +#include "nsIDOMDocument.h" // for nsIDOMDocument +#include "nsIDOMElement.h" // for nsIDOMElement +#include "nsIDOMNode.h" // for nsIDOMNode +#include "nsIDOMNodeList.h" // for nsIDOMNodeList +#include "nsIDOMText.h" // for nsIDOMText +#include "nsIDocument.h" // for nsIDocument +#include "nsIEditor.h" // for nsIEditor +#include "nsIHTMLEditor.h" // for nsIHTMLEditor +#include "nsLiteralString.h" // for NS_LITERAL_STRING + +namespace mozilla { + +// Note that aEditor is not refcounted. +SetDocumentTitleTransaction::SetDocumentTitleTransaction() + : mEditor(nullptr) + , mIsTransient(false) +{ +} + +NS_IMETHODIMP +SetDocumentTitleTransaction::Init(nsIHTMLEditor* aEditor, + const nsAString* aValue) + +{ + NS_ASSERTION(aEditor && aValue, "null args"); + if (!aEditor || !aValue) { + return NS_ERROR_NULL_POINTER; + } + + mEditor = aEditor; + mValue = *aValue; + + return NS_OK; +} + +NS_IMETHODIMP +SetDocumentTitleTransaction::DoTransaction() +{ + return SetDomTitle(mValue); +} + +NS_IMETHODIMP +SetDocumentTitleTransaction::UndoTransaction() +{ + // No extra work required; the DOM changes alone are enough + return NS_OK; +} + +NS_IMETHODIMP +SetDocumentTitleTransaction::RedoTransaction() +{ + // No extra work required; the DOM changes alone are enough + return NS_OK; +} + +nsresult +SetDocumentTitleTransaction::SetDomTitle(const nsAString& aTitle) +{ + nsCOMPtr<nsIEditor> editor = do_QueryInterface(mEditor); + if (NS_WARN_IF(!editor)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMDocument> domDoc; + nsresult rv = editor->GetDocument(getter_AddRefs(domDoc)); + if (NS_WARN_IF(!domDoc)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDOMNodeList> titleList; + rv = domDoc->GetElementsByTagName(NS_LITERAL_STRING("title"), + getter_AddRefs(titleList)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // First assume we will NOT really do anything + // (transaction will not be pushed on stack) + mIsTransient = true; + + nsCOMPtr<nsIDOMNode> titleNode; + if(titleList) { + rv = titleList->Item(0, getter_AddRefs(titleNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (titleNode) { + // Delete existing child textnode of title node + // (Note: all contents under a TITLE node are always in a single text node) + nsCOMPtr<nsIDOMNode> child; + rv = titleNode->GetFirstChild(getter_AddRefs(child)); + if (NS_FAILED(rv)) { + return rv; + } + + if(child) { + // Save current text as the undo value + nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(child); + if(textNode) { + textNode->GetData(mUndoValue); + + // If title text is identical to what already exists, + // quit now (mIsTransient is now TRUE) + if (mUndoValue == aTitle) { + return NS_OK; + } + } + rv = editor->DeleteNode(child); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + } + + // We didn't return above, thus we really will be changing the title + mIsTransient = false; + + // Get the <HEAD> node, create a <TITLE> and insert it under the HEAD + nsCOMPtr<nsIDocument> document = do_QueryInterface(domDoc); + if (NS_WARN_IF(!document)) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<dom::Element> headElement = document->GetHeadElement(); + if (NS_WARN_IF(!headElement)) { + return NS_ERROR_UNEXPECTED; + } + + bool newTitleNode = false; + uint32_t newTitleIndex = 0; + + if (!titleNode) { + // Didn't find one above: Create a new one + nsCOMPtr<nsIDOMElement>titleElement; + rv = domDoc->CreateElement(NS_LITERAL_STRING("title"), + getter_AddRefs(titleElement)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (NS_WARN_IF(!titleElement)) { + return NS_ERROR_FAILURE; + } + + titleNode = do_QueryInterface(titleElement); + newTitleNode = true; + + // Get index so we append new title node after all existing HEAD children. + newTitleIndex = headElement->GetChildCount(); + } + + // Append a text node under the TITLE only if the title text isn't empty. + if (titleNode && !aTitle.IsEmpty()) { + nsCOMPtr<nsIDOMText> textNode; + rv = domDoc->CreateTextNode(aTitle, getter_AddRefs(textNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIDOMNode> newNode = do_QueryInterface(textNode); + if (NS_WARN_IF(!newNode)) { + return NS_ERROR_FAILURE; + } + + if (newTitleNode) { + // Not undoable: We will insert newTitleNode below + nsCOMPtr<nsIDOMNode> resultNode; + rv = titleNode->AppendChild(newNode, getter_AddRefs(resultNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // This is an undoable transaction + rv = editor->InsertNode(newNode, titleNode, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + // Calling AppendChild() or InsertNode() could cause removing the head + // element. So, let's mark it dirty. + headElement = nullptr; + } + + if (newTitleNode) { + if (!headElement) { + headElement = document->GetHeadElement(); + if (NS_WARN_IF(!headElement)) { + // XXX Can we return NS_OK when there is no head element? + return NS_ERROR_UNEXPECTED; + } + } + // Undoable transaction to insert title+text together + rv = editor->InsertNode(titleNode, headElement->AsDOMNode(), newTitleIndex); + } + return rv; +} + +NS_IMETHODIMP +SetDocumentTitleTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("SetDocumentTitleTransaction: "); + aString += mValue; + return NS_OK; +} + +NS_IMETHODIMP +SetDocumentTitleTransaction::GetIsTransient(bool* aIsTransient) +{ + if (NS_WARN_IF(!aIsTransient)) { + return NS_ERROR_NULL_POINTER; + } + *aIsTransient = mIsTransient; + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/SetDocumentTitleTransaction.h b/editor/libeditor/SetDocumentTitleTransaction.h new file mode 100644 index 000000000..0f17fe8d2 --- /dev/null +++ b/editor/libeditor/SetDocumentTitleTransaction.h @@ -0,0 +1,60 @@ +/* -*- 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/. */ + +#ifndef SetDocumentTitleTransaction_h +#define SetDocumentTitleTransaction_h + +#include "mozilla/EditTransactionBase.h" // for EditTransactionBase, etc. +#include "nsString.h" // for nsString +#include "nscore.h" // for NS_IMETHOD, nsAString, etc. + +class nsIHTMLEditor; + +namespace mozilla { + +/** + * A transaction that changes the document's title, + * which is a text node under the <title> tag in a page's <head> section + * provides default concrete behavior for all nsITransaction methods. + */ +class SetDocumentTitleTransaction final : public EditTransactionBase +{ +public: + /** + * Initialize the transaction. + * @param aEditor The object providing core editing operations. + * @param aValue The new value for document title. + */ + NS_IMETHOD Init(nsIHTMLEditor* aEditor, + const nsAString* aValue); + SetDocumentTitleTransaction(); + +private: + nsresult SetDomTitle(const nsAString& aTitle); + +public: + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + NS_IMETHOD GetIsTransient(bool *aIsTransient) override; + +protected: + + // The editor that created this transaction. + nsIHTMLEditor* mEditor; + + // The new title string. + nsString mValue; + + // The previous title string to use for undo. + nsString mUndoValue; + + // Set true if we dont' really change the title during Do(). + bool mIsTransient; +}; + +} // namespace mozilla + +#endif // #ifndef SetDocumentTitleTransaction_h diff --git a/editor/libeditor/SplitNodeTransaction.cpp b/editor/libeditor/SplitNodeTransaction.cpp new file mode 100644 index 000000000..113ff7a61 --- /dev/null +++ b/editor/libeditor/SplitNodeTransaction.cpp @@ -0,0 +1,128 @@ +/* -*- 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 "SplitNodeTransaction.h" + +#include "mozilla/EditorBase.h" // for EditorBase +#include "mozilla/dom/Selection.h" +#include "nsAString.h" +#include "nsDebug.h" // for NS_ASSERTION, etc. +#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc. +#include "nsIContent.h" // for nsIContent + +namespace mozilla { + +using namespace dom; + +SplitNodeTransaction::SplitNodeTransaction(EditorBase& aEditorBase, + nsIContent& aNode, + int32_t aOffset) + : mEditorBase(aEditorBase) + , mExistingRightNode(&aNode) + , mOffset(aOffset) +{ +} + +SplitNodeTransaction::~SplitNodeTransaction() +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(SplitNodeTransaction, EditTransactionBase, + mParent, + mNewLeftNode) + +NS_IMPL_ADDREF_INHERITED(SplitNodeTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(SplitNodeTransaction, EditTransactionBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SplitNodeTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +NS_IMETHODIMP +SplitNodeTransaction::DoTransaction() +{ + // Create a new node + ErrorResult rv; + // Don't use .downcast directly because AsContent has an assertion we want + nsCOMPtr<nsINode> clone = mExistingRightNode->CloneNode(false, rv); + NS_ASSERTION(!rv.Failed() && clone, "Could not create clone"); + NS_ENSURE_TRUE(!rv.Failed() && clone, rv.StealNSResult()); + mNewLeftNode = dont_AddRef(clone.forget().take()->AsContent()); + mEditorBase.MarkNodeDirty(mExistingRightNode->AsDOMNode()); + + // Get the parent node + mParent = mExistingRightNode->GetParentNode(); + NS_ENSURE_TRUE(mParent, NS_ERROR_NULL_POINTER); + + // Insert the new node + rv = mEditorBase.SplitNodeImpl(*mExistingRightNode, mOffset, *mNewLeftNode); + if (mEditorBase.GetShouldTxnSetSelection()) { + RefPtr<Selection> selection = mEditorBase.GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + rv = selection->Collapse(mNewLeftNode, mOffset); + } + return rv.StealNSResult(); +} + +NS_IMETHODIMP +SplitNodeTransaction::UndoTransaction() +{ + MOZ_ASSERT(mNewLeftNode && mParent); + + // This assumes Do inserted the new node in front of the prior existing node + return mEditorBase.JoinNodesImpl(mExistingRightNode, mNewLeftNode, mParent); +} + +/* Redo cannot simply resplit the right node, because subsequent transactions + * on the redo stack may depend on the left node existing in its previous + * state. + */ +NS_IMETHODIMP +SplitNodeTransaction::RedoTransaction() +{ + MOZ_ASSERT(mNewLeftNode && mParent); + + ErrorResult rv; + // First, massage the existing node so it is in its post-split state + if (mExistingRightNode->GetAsText()) { + rv = mExistingRightNode->GetAsText()->DeleteData(0, mOffset); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + } else { + nsCOMPtr<nsIContent> child = mExistingRightNode->GetFirstChild(); + nsCOMPtr<nsIContent> nextSibling; + for (int32_t i=0; i < mOffset; i++) { + if (rv.Failed()) { + return rv.StealNSResult(); + } + if (!child) { + return NS_ERROR_NULL_POINTER; + } + nextSibling = child->GetNextSibling(); + mExistingRightNode->RemoveChild(*child, rv); + if (!rv.Failed()) { + mNewLeftNode->AppendChild(*child, rv); + } + child = nextSibling; + } + } + // Second, re-insert the left node into the tree + nsCOMPtr<nsIContent> refNode = mExistingRightNode; + mParent->InsertBefore(*mNewLeftNode, refNode, rv); + return rv.StealNSResult(); +} + + +NS_IMETHODIMP +SplitNodeTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("SplitNodeTransaction"); + return NS_OK; +} + +nsIContent* +SplitNodeTransaction::GetNewNode() +{ + return mNewLeftNode; +} + +} // namespace mozilla diff --git a/editor/libeditor/SplitNodeTransaction.h b/editor/libeditor/SplitNodeTransaction.h new file mode 100644 index 000000000..36119518b --- /dev/null +++ b/editor/libeditor/SplitNodeTransaction.h @@ -0,0 +1,71 @@ +/* -*- 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/. */ + +#ifndef SplitNodeTransaction_h +#define SplitNodeTransaction_h + +#include "mozilla/EditTransactionBase.h" // for EditTxn, etc. +#include "nsCOMPtr.h" // for nsCOMPtr +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED +#include "nscore.h" // for NS_IMETHOD + +class nsIContent; +class nsINode; + +namespace mozilla { + +class EditorBase; + +/** + * A transaction that splits a node into two identical nodes, with the children + * divided between the new nodes. + */ +class SplitNodeTransaction final : public EditTransactionBase +{ +public: + /** + * @param aEditorBase The provider of core editing operations + * @param aNode The node to split + * @param aOffset The location within aNode to do the split. aOffset may + * refer to children of aNode, or content of aNode. The + * left node will have child|content 0..aOffset-1. + */ + SplitNodeTransaction(EditorBase& aEditorBase, nsIContent& aNode, + int32_t aOffset); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SplitNodeTransaction, + EditTransactionBase) + + NS_DECL_EDITTRANSACTIONBASE + + NS_IMETHOD RedoTransaction() override; + + nsIContent* GetNewNode(); + +protected: + virtual ~SplitNodeTransaction(); + + EditorBase& mEditorBase; + + // The node to operate upon. + nsCOMPtr<nsIContent> mExistingRightNode; + + // The offset into mExistingRightNode where its children are split. mOffset + // is the index of the first child in the right node. -1 means the new node + // gets no children. + int32_t mOffset; + + // The node we create when splitting mExistingRightNode. + nsCOMPtr<nsIContent> mNewLeftNode; + + // The parent shared by mExistingRightNode and mNewLeftNode. + nsCOMPtr<nsINode> mParent; +}; + +} // namespace mozilla + +#endif // #ifndef SplitNodeTransaction_h diff --git a/editor/libeditor/StyleSheetTransactions.cpp b/editor/libeditor/StyleSheetTransactions.cpp new file mode 100644 index 000000000..6a31a16e2 --- /dev/null +++ b/editor/libeditor/StyleSheetTransactions.cpp @@ -0,0 +1,157 @@ +/* -*- 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 "StyleSheetTransactions.h" + +#include <stddef.h> // for nullptr + +#include "nsAString.h" +#include "nsCOMPtr.h" // for nsCOMPtr, do_QueryInterface, etc. +#include "mozilla/StyleSheet.h" // for mozilla::StyleSheet +#include "mozilla/StyleSheetInlines.h" +#include "nsDebug.h" // for NS_ENSURE_TRUE +#include "nsError.h" // for NS_OK, etc. +#include "nsIDOMDocument.h" // for nsIDOMDocument +#include "nsIDocument.h" // for nsIDocument +#include "nsIDocumentObserver.h" // for UPDATE_STYLE +#include "nsIEditor.h" // for nsIEditor + +namespace mozilla { + +static void +AddStyleSheet(nsIEditor* aEditor, StyleSheet* aSheet) +{ + nsCOMPtr<nsIDOMDocument> domDoc; + aEditor->GetDocument(getter_AddRefs(domDoc)); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc); + if (doc) { + doc->BeginUpdate(UPDATE_STYLE); + doc->AddStyleSheet(aSheet); + doc->EndUpdate(UPDATE_STYLE); + } +} + +static void +RemoveStyleSheet(nsIEditor* aEditor, StyleSheet* aSheet) +{ + nsCOMPtr<nsIDOMDocument> domDoc; + aEditor->GetDocument(getter_AddRefs(domDoc)); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc); + if (doc) { + doc->BeginUpdate(UPDATE_STYLE); + doc->RemoveStyleSheet(aSheet); + doc->EndUpdate(UPDATE_STYLE); + } +} + +/****************************************************************************** + * AddStyleSheetTransaction + ******************************************************************************/ + +AddStyleSheetTransaction::AddStyleSheetTransaction() + : mEditor(nullptr) +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(AddStyleSheetTransaction, + EditTransactionBase, + mSheet) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AddStyleSheetTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +NS_IMETHODIMP +AddStyleSheetTransaction::Init(nsIEditor* aEditor, + StyleSheet* aSheet) +{ + NS_ENSURE_TRUE(aEditor && aSheet, NS_ERROR_INVALID_ARG); + + mEditor = aEditor; + mSheet = aSheet; + + return NS_OK; +} + + +NS_IMETHODIMP +AddStyleSheetTransaction::DoTransaction() +{ + NS_ENSURE_TRUE(mEditor && mSheet, NS_ERROR_NOT_INITIALIZED); + + AddStyleSheet(mEditor, mSheet); + return NS_OK; +} + +NS_IMETHODIMP +AddStyleSheetTransaction::UndoTransaction() +{ + NS_ENSURE_TRUE(mEditor && mSheet, NS_ERROR_NOT_INITIALIZED); + + RemoveStyleSheet(mEditor, mSheet); + return NS_OK; +} + +NS_IMETHODIMP +AddStyleSheetTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("AddStyleSheetTransaction"); + return NS_OK; +} + +/****************************************************************************** + * RemoveStyleSheetTransaction + ******************************************************************************/ + +RemoveStyleSheetTransaction::RemoveStyleSheetTransaction() + : mEditor(nullptr) +{ +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(RemoveStyleSheetTransaction, + EditTransactionBase, + mSheet) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RemoveStyleSheetTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) + +NS_IMETHODIMP +RemoveStyleSheetTransaction::Init(nsIEditor* aEditor, + StyleSheet* aSheet) +{ + NS_ENSURE_TRUE(aEditor && aSheet, NS_ERROR_INVALID_ARG); + + mEditor = aEditor; + mSheet = aSheet; + + return NS_OK; +} + + +NS_IMETHODIMP +RemoveStyleSheetTransaction::DoTransaction() +{ + NS_ENSURE_TRUE(mEditor && mSheet, NS_ERROR_NOT_INITIALIZED); + + RemoveStyleSheet(mEditor, mSheet); + return NS_OK; +} + +NS_IMETHODIMP +RemoveStyleSheetTransaction::UndoTransaction() +{ + NS_ENSURE_TRUE(mEditor && mSheet, NS_ERROR_NOT_INITIALIZED); + + AddStyleSheet(mEditor, mSheet); + return NS_OK; +} + +NS_IMETHODIMP +RemoveStyleSheetTransaction::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("RemoveStyleSheetTransaction"); + return NS_OK; +} + +} // namespace mozilla diff --git a/editor/libeditor/StyleSheetTransactions.h b/editor/libeditor/StyleSheetTransactions.h new file mode 100644 index 000000000..bf615b263 --- /dev/null +++ b/editor/libeditor/StyleSheetTransactions.h @@ -0,0 +1,73 @@ +/* -*- 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/. */ + +#ifndef StylesheetTransactions_h +#define StylesheetTransactions_h + +#include "mozilla/EditTransactionBase.h" // for EditTransactionBase, etc. +#include "mozilla/StyleSheet.h" // for mozilla::StyleSheet +#include "nsCycleCollectionParticipant.h" +#include "nsID.h" // for REFNSIID +#include "nscore.h" // for NS_IMETHOD + +class nsIEditor; + +namespace mozilla { + +class AddStyleSheetTransaction final : public EditTransactionBase +{ +public: + /** + * Initialize the transaction. + * @param aEditor The object providing core editing operations + * @param aSheet The stylesheet to add + */ + NS_IMETHOD Init(nsIEditor* aEditor, StyleSheet* aSheet); + + AddStyleSheetTransaction(); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AddStyleSheetTransaction, + EditTransactionBase) + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; + + NS_DECL_EDITTRANSACTIONBASE + +protected: + // The editor that created this transaction. + nsIEditor* mEditor; + // The style sheet to add. + RefPtr<mozilla::StyleSheet> mSheet; +}; + + +class RemoveStyleSheetTransaction final : public EditTransactionBase +{ +public: + /** + * Initialize the transaction. + * @param aEditor The object providing core editing operations. + * @param aSheet The stylesheet to remove. + */ + NS_IMETHOD Init(nsIEditor* aEditor, StyleSheet* aSheet); + + RemoveStyleSheetTransaction(); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(RemoveStyleSheetTransaction, + EditTransactionBase) + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; + + NS_DECL_EDITTRANSACTIONBASE + +protected: + // The editor that created this transaction. + nsIEditor* mEditor; + // The style sheet to remove. + RefPtr<StyleSheet> mSheet; + +}; + +} // namespace mozilla + +#endif // #ifndef StylesheetTransactions_h 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 diff --git a/editor/libeditor/TextEditRules.h b/editor/libeditor/TextEditRules.h new file mode 100644 index 000000000..6d4915f15 --- /dev/null +++ b/editor/libeditor/TextEditRules.h @@ -0,0 +1,357 @@ +/* -*- 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/. */ + +#ifndef mozilla_TextEditRules_h +#define mozilla_TextEditRules_h + +#include "mozilla/EditorBase.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIEditRules.h" +#include "nsIEditor.h" +#include "nsISupportsImpl.h" +#include "nsITimer.h" +#include "nsString.h" +#include "nscore.h" + +class nsIDOMElement; +class nsIDOMNode; + +namespace mozilla { + +class AutoLockRulesSniffing; +class TextEditor; +namespace dom { +class Selection; +} // namespace dom + +/** + * Object that encapsulates HTML text-specific editing rules. + * + * To be a good citizen, edit rules must live by these restrictions: + * 1. All data manipulation is through the editor. + * Content nodes in the document tree must <B>not</B> be manipulated + * directly. Content nodes in document fragments that are not part of the + * document itself may be manipulated at will. Operations on document + * fragments must <B>not</B> go through the editor. + * 2. Selection must not be explicitly set by the rule method. + * Any manipulation of Selection must be done by the editor. + */ +class TextEditRules : public nsIEditRules + , public nsITimerCallback +{ +public: + typedef dom::Element Element; + typedef dom::Selection Selection; + typedef dom::Text Text; + template<typename T> using OwningNonNull = OwningNonNull<T>; + + NS_DECL_NSITIMERCALLBACK + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TextEditRules, nsIEditRules) + + TextEditRules(); + + // nsIEditRules methods + NS_IMETHOD Init(TextEditor* aTextEditor) override; + NS_IMETHOD SetInitialValue(const nsAString& aValue) override; + NS_IMETHOD DetachEditor() override; + NS_IMETHOD BeforeEdit(EditAction action, + nsIEditor::EDirection aDirection) override; + NS_IMETHOD AfterEdit(EditAction action, + nsIEditor::EDirection aDirection) override; + NS_IMETHOD WillDoAction(Selection* aSelection, RulesInfo* aInfo, + bool* aCancel, bool* aHandled) override; + NS_IMETHOD DidDoAction(Selection* aSelection, RulesInfo* aInfo, + nsresult aResult) override; + NS_IMETHOD DocumentIsEmpty(bool* aDocumentIsEmpty) override; + NS_IMETHOD DocumentModified() override; + +protected: + virtual ~TextEditRules(); + +public: + void ResetIMETextPWBuf(); + + /** + * Handles the newline characters either according to aNewLineHandling + * or to the default system prefs if aNewLineHandling is negative. + * + * @param aString the string to be modified in place. + * @param aNewLineHandling determine the desired type of newline handling: + * * negative values: + * handle newlines according to platform defaults. + * * nsIPlaintextEditor::eNewlinesReplaceWithSpaces: + * replace newlines with spaces. + * * nsIPlaintextEditor::eNewlinesStrip: + * remove newlines from the string. + * * nsIPlaintextEditor::eNewlinesReplaceWithCommas: + * replace newlines with commas. + * * nsIPlaintextEditor::eNewlinesStripSurroundingWhitespace: + * collapse newlines and surrounding whitespace characters and + * remove them from the string. + * * nsIPlaintextEditor::eNewlinesPasteIntact: + * only remove the leading and trailing newlines. + * * nsIPlaintextEditor::eNewlinesPasteToFirst or any other value: + * remove the first newline and all characters following it. + */ + static void HandleNewLines(nsString& aString, int32_t aNewLineHandling); + + /** + * Prepare a string buffer for being displayed as the contents of a password + * field. This function uses the platform-specific character for representing + * characters entered into password fields. + * + * @param aOutString the output string. When this function returns, + * aOutString will contain aLength password characters. + * @param aLength the number of password characters that aOutString should + * contain. + */ + static void FillBufWithPWChars(nsAString* aOutString, int32_t aLength); + +protected: + + void InitFields(); + + // TextEditRules implementation methods + nsresult WillInsertText(EditAction aAction, + Selection* aSelection, + bool* aCancel, + bool* aHandled, + const nsAString* inString, + nsAString* outString, + int32_t aMaxLength); + nsresult DidInsertText(Selection* aSelection, nsresult aResult); + nsresult GetTopEnclosingPre(nsIDOMNode* aNode, nsIDOMNode** aOutPreNode); + + nsresult WillInsertBreak(Selection* aSelection, bool* aCancel, + bool* aHandled, int32_t aMaxLength); + nsresult DidInsertBreak(Selection* aSelection, nsresult aResult); + + void WillInsert(Selection& aSelection, bool* aCancel); + nsresult DidInsert(Selection* aSelection, nsresult aResult); + + nsresult WillDeleteSelection(Selection* aSelection, + nsIEditor::EDirection aCollapsedAction, + bool* aCancel, + bool* aHandled); + nsresult DidDeleteSelection(Selection* aSelection, + nsIEditor::EDirection aCollapsedAction, + nsresult aResult); + + nsresult WillSetTextProperty(Selection* aSelection, bool* aCancel, + bool* aHandled); + nsresult DidSetTextProperty(Selection* aSelection, nsresult aResult); + + nsresult WillRemoveTextProperty(Selection* aSelection, bool* aCancel, + bool* aHandled); + nsresult DidRemoveTextProperty(Selection* aSelection, nsresult aResult); + + nsresult WillUndo(Selection* aSelection, bool* aCancel, bool* aHandled); + nsresult DidUndo(Selection* aSelection, nsresult aResult); + + nsresult WillRedo(Selection* aSelection, bool* aCancel, bool* aHandled); + nsresult DidRedo(Selection* aSelection, nsresult aResult); + + /** + * Called prior to nsIEditor::OutputToString. + * @param aSelection + * @param aInFormat The format requested for the output, a MIME type. + * @param aOutText The string to use for output, if aCancel is set to true. + * @param aOutCancel If set to true, the caller should cancel the operation + * and use aOutText as the result. + */ + nsresult WillOutputText(Selection* aSelection, + const nsAString* aInFormat, + nsAString* aOutText, + bool* aOutCancel, + bool* aHandled); + + nsresult DidOutputText(Selection* aSelection, nsresult aResult); + + /** + * Check for and replace a redundant trailing break. + */ + nsresult RemoveRedundantTrailingBR(); + + /** + * Creates a trailing break in the text doc if there is not one already. + */ + nsresult CreateTrailingBRIfNeeded(); + + /** + * Creates a bogus text node if the document has no editable content. + */ + nsresult CreateBogusNodeIfNeeded(Selection* aSelection); + + /** + * Returns a truncated insertion string if insertion would place us over + * aMaxLength + */ + nsresult TruncateInsertionIfNeeded(Selection* aSelection, + const nsAString* aInString, + nsAString* aOutString, + int32_t aMaxLength, + bool* aTruncated); + + /** + * Remove IME composition text from password buffer. + */ + void RemoveIMETextFromPWBuf(int32_t& aStart, nsAString* aIMEString); + + nsresult CreateMozBR(nsIDOMNode* inParent, int32_t inOffset, + nsIDOMNode** outBRNode = nullptr); + + void UndefineCaretBidiLevel(Selection* aSelection); + + nsresult CheckBidiLevelForDeletion(Selection* aSelection, + nsIDOMNode* aSelNode, + int32_t aSelOffset, + nsIEditor::EDirection aAction, + bool* aCancel); + + nsresult HideLastPWInput(); + + nsresult CollapseSelectionToTrailingBRIfNeeded(Selection* aSelection); + + bool IsPasswordEditor() const; + bool IsSingleLineEditor() const; + bool IsPlaintextEditor() const; + bool IsReadonly() const; + bool IsDisabled() const; + bool IsMailEditor() const; + bool DontEchoPassword() const; + + // Note that we do not refcount the editor. + TextEditor* mTextEditor; + // A buffer we use to store the real value of password editors. + nsString mPasswordText; + // A buffer we use to track the IME composition string. + nsString mPasswordIMEText; + uint32_t mPasswordIMEIndex; + // Magic node acts as placeholder in empty doc. + nsCOMPtr<nsIDOMNode> mBogusNode; + // Cached selected node. + nsCOMPtr<nsIDOMNode> mCachedSelectionNode; + // Cached selected offset. + int32_t mCachedSelectionOffset; + uint32_t mActionNesting; + bool mLockRulesSniffing; + bool mDidExplicitlySetInterline; + // In bidirectional text, delete characters not visually adjacent to the + // caret without moving the caret first. + bool mDeleteBidiImmediately; + // The top level editor action. + EditAction mTheAction; + nsCOMPtr<nsITimer> mTimer; + uint32_t mLastStart; + uint32_t mLastLength; + + // friends + friend class AutoLockRulesSniffing; +}; + +class TextRulesInfo final : public RulesInfo +{ +public: + explicit TextRulesInfo(EditAction aAction) + : RulesInfo(aAction) + , inString(nullptr) + , outString(nullptr) + , outputFormat(nullptr) + , maxLength(-1) + , collapsedAction(nsIEditor::eNext) + , stripWrappers(nsIEditor::eStrip) + , bOrdered(false) + , entireList(false) + , bulletType(nullptr) + , alignType(nullptr) + , blockType(nullptr) + , insertElement(nullptr) + {} + + // kInsertText + const nsAString* inString; + nsAString* outString; + const nsAString* outputFormat; + int32_t maxLength; + + // kDeleteSelection + nsIEditor::EDirection collapsedAction; + nsIEditor::EStripWrappers stripWrappers; + + // kMakeList + bool bOrdered; + bool entireList; + const nsAString* bulletType; + + // kAlign + const nsAString* alignType; + + // kMakeBasicBlock + const nsAString* blockType; + + // kInsertElement + const nsIDOMElement* insertElement; +}; + +/** + * Stack based helper class for StartOperation()/EndOperation() sandwich. + * This class sets a bool letting us know to ignore any rules sniffing + * that tries to occur reentrantly. + */ +class MOZ_STACK_CLASS AutoLockRulesSniffing final +{ +public: + explicit AutoLockRulesSniffing(TextEditRules* aRules) + : mRules(aRules) + { + if (mRules) { + mRules->mLockRulesSniffing = true; + } + } + + ~AutoLockRulesSniffing() + { + if (mRules) { + mRules->mLockRulesSniffing = false; + } + } + +protected: + TextEditRules* mRules; +}; + +/** + * Stack based helper class for turning on/off the edit listener. + */ +class MOZ_STACK_CLASS AutoLockListener final +{ +public: + explicit AutoLockListener(bool* aEnabled) + : mEnabled(aEnabled) + , mOldState(false) + { + if (mEnabled) { + mOldState = *mEnabled; + *mEnabled = false; + } + } + + ~AutoLockListener() + { + if (mEnabled) { + *mEnabled = mOldState; + } + } + +protected: + bool* mEnabled; + bool mOldState; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_TextEditRules_h diff --git a/editor/libeditor/TextEditRulesBidi.cpp b/editor/libeditor/TextEditRulesBidi.cpp new file mode 100644 index 000000000..f6b8b7120 --- /dev/null +++ b/editor/libeditor/TextEditRulesBidi.cpp @@ -0,0 +1,100 @@ +/* -*- 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 "mozilla/TextEditor.h" +#include "mozilla/dom/Selection.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsFrameSelection.h" +#include "nsIContent.h" +#include "nsIDOMNode.h" +#include "nsIEditor.h" +#include "nsIPresShell.h" +#include "nsISupportsImpl.h" +#include "nsPresContext.h" +#include "nscore.h" + +namespace mozilla { + +using namespace dom; + +// Test for distance between caret and text that will be deleted +nsresult +TextEditRules::CheckBidiLevelForDeletion(Selection* aSelection, + nsIDOMNode* aSelNode, + int32_t aSelOffset, + nsIEditor::EDirection aAction, + bool* aCancel) +{ + NS_ENSURE_ARG_POINTER(aCancel); + *aCancel = false; + + nsCOMPtr<nsIPresShell> shell = mTextEditor->GetPresShell(); + NS_ENSURE_TRUE(shell, NS_ERROR_NOT_INITIALIZED); + + nsPresContext *context = shell->GetPresContext(); + NS_ENSURE_TRUE(context, NS_ERROR_NULL_POINTER); + + if (!context->BidiEnabled()) { + return NS_OK; + } + + nsCOMPtr<nsIContent> content = do_QueryInterface(aSelNode); + NS_ENSURE_TRUE(content, NS_ERROR_NULL_POINTER); + + nsBidiLevel levelBefore; + nsBidiLevel levelAfter; + RefPtr<nsFrameSelection> frameSelection = + aSelection->AsSelection()->GetFrameSelection(); + NS_ENSURE_TRUE(frameSelection, NS_ERROR_NULL_POINTER); + + nsPrevNextBidiLevels levels = frameSelection-> + GetPrevNextBidiLevels(content, aSelOffset, true); + + levelBefore = levels.mLevelBefore; + levelAfter = levels.mLevelAfter; + + nsBidiLevel currentCaretLevel = frameSelection->GetCaretBidiLevel(); + + nsBidiLevel levelOfDeletion; + levelOfDeletion = + (nsIEditor::eNext==aAction || nsIEditor::eNextWord==aAction) ? + levelAfter : levelBefore; + + if (currentCaretLevel == levelOfDeletion) { + return NS_OK; // perform the deletion + } + + if (!mDeleteBidiImmediately && levelBefore != levelAfter) { + *aCancel = true; + } + + // Set the bidi level of the caret to that of the + // character that will be (or would have been) deleted + frameSelection->SetCaretBidiLevel(levelOfDeletion); + return NS_OK; +} + +void +TextEditRules::UndefineCaretBidiLevel(Selection* aSelection) +{ + /** + * After inserting text the caret Bidi level must be set to the level of the + * inserted text.This is difficult, because we cannot know what the level is + * until after the Bidi algorithm is applied to the whole paragraph. + * + * So we set the caret Bidi level to UNDEFINED here, and the caret code will + * set it correctly later + */ + RefPtr<nsFrameSelection> frameSelection = aSelection->GetFrameSelection(); + if (frameSelection) { + frameSelection->UndefineCaretBidiLevel(); + } +} + +} // namespace mozilla diff --git a/editor/libeditor/TextEditUtils.cpp b/editor/libeditor/TextEditUtils.cpp new file mode 100644 index 000000000..7c4f2ec12 --- /dev/null +++ b/editor/libeditor/TextEditUtils.cpp @@ -0,0 +1,113 @@ +/* -*- 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 "TextEditUtils.h" + +#include "mozilla/Assertions.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/Element.h" +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsCaseTreatment.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIDOMElement.h" +#include "nsIDOMNode.h" +#include "nsNameSpaceManager.h" +#include "nsLiteralString.h" +#include "nsString.h" + +namespace mozilla { + +/****************************************************************************** + * TextEditUtils + ******************************************************************************/ + +/** + * IsBody() returns true if aNode is an html body node. + */ +bool +TextEditUtils::IsBody(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::body); +} + +/** + * IsBreak() returns true if aNode is an html break node. + */ +bool +TextEditUtils::IsBreak(nsIDOMNode* aNode) +{ + return EditorBase::NodeIsType(aNode, nsGkAtoms::br); +} + +bool +TextEditUtils::IsBreak(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsHTMLElement(nsGkAtoms::br); +} + + +/** + * IsMozBR() returns true if aNode is an html br node with |type = _moz|. + */ +bool +TextEditUtils::IsMozBR(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + return IsBreak(aNode) && HasMozAttr(aNode); +} + +bool +TextEditUtils::IsMozBR(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + return aNode->IsHTMLElement(nsGkAtoms::br) && + aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("_moz"), + eIgnoreCase); +} + +/** + * HasMozAttr() returns true if aNode has type attribute and its value is + * |_moz|. (Used to indicate div's and br's we use in mail compose rules) + */ +bool +TextEditUtils::HasMozAttr(nsIDOMNode* aNode) +{ + MOZ_ASSERT(aNode); + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(aNode); + if (!element) { + return false; + } + nsAutoString typeAttrVal; + nsresult rv = element->GetAttribute(NS_LITERAL_STRING("type"), typeAttrVal); + return NS_SUCCEEDED(rv) && typeAttrVal.LowerCaseEqualsLiteral("_moz"); +} + +/****************************************************************************** + * AutoEditInitRulesTrigger + ******************************************************************************/ + +AutoEditInitRulesTrigger::AutoEditInitRulesTrigger(TextEditor* aTextEditor, + nsresult& aResult) + : mTextEditor(aTextEditor) + , mResult(aResult) +{ + if (mTextEditor) { + mTextEditor->BeginEditorInit(); + } +} + +AutoEditInitRulesTrigger::~AutoEditInitRulesTrigger() +{ + if (mTextEditor) { + mResult = mTextEditor->EndEditorInit(); + } +} + +} // namespace mozilla diff --git a/editor/libeditor/TextEditUtils.h b/editor/libeditor/TextEditUtils.h new file mode 100644 index 000000000..cfdbc928f --- /dev/null +++ b/editor/libeditor/TextEditUtils.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef TextEditUtils_h +#define TextEditUtils_h + +#include "nscore.h" + +class nsIDOMNode; +class nsINode; + +namespace mozilla { + +class TextEditor; + +class TextEditUtils final +{ +public: + // from TextEditRules: + static bool IsBody(nsIDOMNode* aNode); + static bool IsBreak(nsIDOMNode* aNode); + static bool IsBreak(nsINode* aNode); + static bool IsMozBR(nsIDOMNode* aNode); + static bool IsMozBR(nsINode* aNode); + static bool HasMozAttr(nsIDOMNode* aNode); +}; + +/*************************************************************************** + * stack based helper class for detecting end of editor initialization, in + * order to trigger "end of init" initialization of the edit rules. + */ +class AutoEditInitRulesTrigger final +{ +private: + TextEditor* mTextEditor; + nsresult& mResult; + +public: + AutoEditInitRulesTrigger(TextEditor* aTextEditor, nsresult& aResult); + ~AutoEditInitRulesTrigger(); +}; + +} // naemspace mozilla + +#endif // #ifndef TextEditUtils_h diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp new file mode 100644 index 000000000..8fe824e11 --- /dev/null +++ b/editor/libeditor/TextEditor.cpp @@ -0,0 +1,1638 @@ +/* -*- 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/TextEditor.h" + +#include "InternetCiter.h" +#include "TextEditUtils.h" +#include "gfxFontUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/EditorUtils.h" // AutoEditBatch, AutoRules +#include "mozilla/mozalloc.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEditRules.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/Element.h" +#include "nsAString.h" +#include "nsCRT.h" +#include "nsCaret.h" +#include "nsCharTraits.h" +#include "nsComponentManagerUtils.h" +#include "nsContentCID.h" +#include "nsCopySupport.h" +#include "nsDebug.h" +#include "nsDependentSubstring.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIClipboard.h" +#include "nsIContent.h" +#include "nsIContentIterator.h" +#include "nsIDOMCharacterData.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMNode.h" +#include "nsIDOMNodeList.h" +#include "nsIDocumentEncoder.h" +#include "nsIEditorIMESupport.h" +#include "nsIEditRules.h" +#include "nsINode.h" +#include "nsIPresShell.h" +#include "nsISelectionController.h" +#include "nsISupportsPrimitives.h" +#include "nsITransferable.h" +#include "nsIWeakReferenceUtils.h" +#include "nsNameSpaceManager.h" +#include "nsLiteralString.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsSubstringTuple.h" +#include "nsUnicharUtils.h" +#include "nsXPCOM.h" + +class nsIOutputStream; +class nsISupports; + +namespace mozilla { + +using namespace dom; + +TextEditor::TextEditor() + : mWrapColumn(0) + , mMaxTextLength(-1) + , mInitTriggerCounter(0) + , mNewlineHandling(nsIPlaintextEditor::eNewlinesPasteToFirst) +#ifdef XP_WIN + , mCaretStyle(1) +#else + , mCaretStyle(0) +#endif +{ + // check the "single line editor newline handling" + // and "caret behaviour in selection" prefs + GetDefaultEditorPrefs(mNewlineHandling, mCaretStyle); +} + +TextEditor::~TextEditor() +{ + // Remove event listeners. Note that if we had an HTML editor, + // it installed its own instead of these + RemoveEventListeners(); + + if (mRules) + mRules->DetachEditor(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(TextEditor) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TextEditor, EditorBase) + if (tmp->mRules) + tmp->mRules->DetachEditor(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRules) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRules) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(TextEditor, EditorBase) +NS_IMPL_RELEASE_INHERITED(TextEditor, EditorBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TextEditor) + NS_INTERFACE_MAP_ENTRY(nsIPlaintextEditor) + NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport) +NS_INTERFACE_MAP_END_INHERITING(EditorBase) + + +NS_IMETHODIMP +TextEditor::Init(nsIDOMDocument* aDoc, + nsIContent* aRoot, + nsISelectionController* aSelCon, + uint32_t aFlags, + const nsAString& aInitialValue) +{ + NS_PRECONDITION(aDoc, "bad arg"); + NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER); + + if (mRules) { + mRules->DetachEditor(); + } + + nsresult rulesRv = NS_OK; + { + // block to scope AutoEditInitRulesTrigger + AutoEditInitRulesTrigger rulesTrigger(this, rulesRv); + + // Init the base editor + nsresult rv = EditorBase::Init(aDoc, aRoot, aSelCon, aFlags, aInitialValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + NS_ENSURE_SUCCESS(rulesRv, rulesRv); + + // mRules may not have been initialized yet, when this is called via + // HTMLEditor::Init. + if (mRules) { + mRules->SetInitialValue(aInitialValue); + } + + return NS_OK; +} + +static int32_t sNewlineHandlingPref = -1, + sCaretStylePref = -1; + +static void +EditorPrefsChangedCallback(const char* aPrefName, void *) +{ + if (!nsCRT::strcmp(aPrefName, "editor.singleLine.pasteNewlines")) { + sNewlineHandlingPref = + Preferences::GetInt("editor.singleLine.pasteNewlines", + nsIPlaintextEditor::eNewlinesPasteToFirst); + } else if (!nsCRT::strcmp(aPrefName, "layout.selection.caret_style")) { + sCaretStylePref = Preferences::GetInt("layout.selection.caret_style", +#ifdef XP_WIN + 1); + if (!sCaretStylePref) { + sCaretStylePref = 1; + } +#else + 0); +#endif + } +} + +// static +void +TextEditor::GetDefaultEditorPrefs(int32_t& aNewlineHandling, + int32_t& aCaretStyle) +{ + if (sNewlineHandlingPref == -1) { + Preferences::RegisterCallback(EditorPrefsChangedCallback, + "editor.singleLine.pasteNewlines"); + EditorPrefsChangedCallback("editor.singleLine.pasteNewlines", nullptr); + Preferences::RegisterCallback(EditorPrefsChangedCallback, + "layout.selection.caret_style"); + EditorPrefsChangedCallback("layout.selection.caret_style", nullptr); + } + + aNewlineHandling = sNewlineHandlingPref; + aCaretStyle = sCaretStylePref; +} + +void +TextEditor::BeginEditorInit() +{ + mInitTriggerCounter++; +} + +nsresult +TextEditor::EndEditorInit() +{ + NS_PRECONDITION(mInitTriggerCounter > 0, "ended editor init before we began?"); + mInitTriggerCounter--; + if (mInitTriggerCounter) { + return NS_OK; + } + + nsresult rv = InitRules(); + if (NS_FAILED(rv)) { + return rv; + } + // Throw away the old transaction manager if this is not the first time that + // we're initializing the editor. + EnableUndo(false); + EnableUndo(true); + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::SetDocumentCharacterSet(const nsACString& characterSet) +{ + nsresult rv = EditorBase::SetDocumentCharacterSet(characterSet); + NS_ENSURE_SUCCESS(rv, rv); + + // Update META charset element. + nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument(); + NS_ENSURE_TRUE(domdoc, NS_ERROR_NOT_INITIALIZED); + + if (UpdateMetaCharset(domdoc, characterSet)) { + return NS_OK; + } + + nsCOMPtr<nsIDOMNodeList> headList; + rv = domdoc->GetElementsByTagName(NS_LITERAL_STRING("head"), getter_AddRefs(headList)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(headList, NS_OK); + + nsCOMPtr<nsIDOMNode> headNode; + headList->Item(0, getter_AddRefs(headNode)); + NS_ENSURE_TRUE(headNode, NS_OK); + + // Create a new meta charset tag + nsCOMPtr<nsIDOMNode> resultNode; + rv = CreateNode(NS_LITERAL_STRING("meta"), headNode, 0, getter_AddRefs(resultNode)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(resultNode, NS_OK); + + // Set attributes to the created element + if (characterSet.IsEmpty()) { + return NS_OK; + } + + nsCOMPtr<dom::Element> metaElement = do_QueryInterface(resultNode); + if (!metaElement) { + return NS_OK; + } + + // not undoable, undo should undo CreateNode + metaElement->SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, + NS_LITERAL_STRING("Content-Type"), true); + metaElement->SetAttr(kNameSpaceID_None, nsGkAtoms::content, + NS_LITERAL_STRING("text/html;charset=") + + NS_ConvertASCIItoUTF16(characterSet), + true); + return NS_OK; +} + +bool +TextEditor::UpdateMetaCharset(nsIDOMDocument* aDocument, + const nsACString& aCharacterSet) +{ + MOZ_ASSERT(aDocument); + // get a list of META tags + nsCOMPtr<nsIDOMNodeList> list; + nsresult rv = aDocument->GetElementsByTagName(NS_LITERAL_STRING("meta"), + getter_AddRefs(list)); + NS_ENSURE_SUCCESS(rv, false); + NS_ENSURE_TRUE(list, false); + + nsCOMPtr<nsINodeList> metaList = do_QueryInterface(list); + + uint32_t listLength = 0; + metaList->GetLength(&listLength); + + for (uint32_t i = 0; i < listLength; ++i) { + nsCOMPtr<nsIContent> metaNode = metaList->Item(i); + MOZ_ASSERT(metaNode); + + if (!metaNode->IsElement()) { + continue; + } + + nsAutoString currentValue; + metaNode->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, currentValue); + + if (!FindInReadable(NS_LITERAL_STRING("content-type"), + currentValue, + nsCaseInsensitiveStringComparator())) { + continue; + } + + metaNode->GetAttr(kNameSpaceID_None, nsGkAtoms::content, currentValue); + + NS_NAMED_LITERAL_STRING(charsetEquals, "charset="); + 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 + nsCOMPtr<nsIDOMElement> metaElement = do_QueryInterface(metaNode); + MOZ_ASSERT(metaElement); + rv = EditorBase::SetAttribute(metaElement, NS_LITERAL_STRING("content"), + Substring(originalStart, start) + + charsetEquals + + NS_ConvertASCIItoUTF16(aCharacterSet)); + return NS_SUCCEEDED(rv); + } + return false; +} + +NS_IMETHODIMP +TextEditor::InitRules() +{ + if (!mRules) { + // instantiate the rules for this text editor + mRules = new TextEditRules(); + } + return mRules->Init(this); +} + + +NS_IMETHODIMP +TextEditor::GetIsDocumentEditable(bool* aIsDocumentEditable) +{ + NS_ENSURE_ARG_POINTER(aIsDocumentEditable); + + nsCOMPtr<nsIDOMDocument> doc = GetDOMDocument(); + *aIsDocumentEditable = doc && IsModifiable(); + + return NS_OK; +} + +bool +TextEditor::IsModifiable() +{ + return !IsReadonly(); +} + +nsresult +TextEditor::HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) +{ + // NOTE: When you change this method, you should also change: + // * editor/libeditor/tests/test_texteditor_keyevent_handling.html + // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html + // + // And also when you add new key handling, you need to change the subclass's + // HandleKeyPressEvent()'s switch statement. + + if (IsReadonly() || IsDisabled()) { + // When we're not editable, the events handled on EditorBase. + return EditorBase::HandleKeyPressEvent(aKeyEvent); + } + + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent(); + NS_ENSURE_TRUE(nativeKeyEvent, NS_ERROR_UNEXPECTED); + NS_ASSERTION(nativeKeyEvent->mMessage == eKeyPress, + "HandleKeyPressEvent gets non-keypress event"); + + switch (nativeKeyEvent->mKeyCode) { + case NS_VK_META: + case NS_VK_WIN: + case NS_VK_SHIFT: + case NS_VK_CONTROL: + case NS_VK_ALT: + case NS_VK_BACK: + case NS_VK_DELETE: + // These keys are handled on EditorBase + return EditorBase::HandleKeyPressEvent(aKeyEvent); + case NS_VK_TAB: { + if (IsTabbable()) { + return NS_OK; // let it be used for focus switching + } + + if (nativeKeyEvent->IsShift() || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + return NS_OK; + } + + // else we insert the tab straight through + aKeyEvent->AsEvent()->PreventDefault(); + return TypedText(NS_LITERAL_STRING("\t"), eTypedText); + } + case NS_VK_RETURN: + if (IsSingleLineEditor() || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + return NS_OK; + } + aKeyEvent->AsEvent()->PreventDefault(); + return TypedText(EmptyString(), eTypedBreak); + } + + // NOTE: On some keyboard layout, some characters are inputted with Control + // key or Alt key, but at that time, widget sets FALSE to these keys. + if (!nativeKeyEvent->mCharCode || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + // we don't PreventDefault() here or keybindings like control-x won't work + return NS_OK; + } + aKeyEvent->AsEvent()->PreventDefault(); + nsAutoString str(nativeKeyEvent->mCharCode); + return TypedText(str, eTypedText); +} + +/* This routine is needed to provide a bottleneck for typing for logging + purposes. Can't use HandleKeyPress() (above) for that since it takes + a nsIDOMKeyEvent* parameter. So instead we pass enough info through + to TypedText() to determine what action to take, but without passing + an event. + */ +NS_IMETHODIMP +TextEditor::TypedText(const nsAString& aString, ETypingAction aAction) +{ + AutoPlaceHolderBatch batch(this, nsGkAtoms::TypingTxnName); + + switch (aAction) { + case eTypedText: + return InsertText(aString); + case eTypedBreak: + return InsertLineBreak(); + default: + // eTypedBR is only for HTML + return NS_ERROR_FAILURE; + } +} + +already_AddRefed<Element> +TextEditor::CreateBRImpl(nsCOMPtr<nsINode>* aInOutParent, + int32_t* aInOutOffset, + EDirection aSelect) +{ + nsCOMPtr<nsIDOMNode> parent(GetAsDOMNode(*aInOutParent)); + nsCOMPtr<nsIDOMNode> br; + // We ignore the retval, and assume it's fine if the br is non-null + CreateBRImpl(address_of(parent), aInOutOffset, address_of(br), aSelect); + *aInOutParent = do_QueryInterface(parent); + nsCOMPtr<Element> ret(do_QueryInterface(br)); + return ret.forget(); +} + +nsresult +TextEditor::CreateBRImpl(nsCOMPtr<nsIDOMNode>* aInOutParent, + int32_t* aInOutOffset, + nsCOMPtr<nsIDOMNode>* outBRNode, + EDirection aSelect) +{ + NS_ENSURE_TRUE(aInOutParent && *aInOutParent && aInOutOffset && outBRNode, NS_ERROR_NULL_POINTER); + *outBRNode = nullptr; + + // we need to insert a br. unfortunately, we may have to split a text node to do it. + nsCOMPtr<nsIDOMNode> node = *aInOutParent; + int32_t theOffset = *aInOutOffset; + nsCOMPtr<nsIDOMCharacterData> nodeAsText = do_QueryInterface(node); + NS_NAMED_LITERAL_STRING(brType, "br"); + nsCOMPtr<nsIDOMNode> brNode; + if (nodeAsText) { + int32_t offset; + uint32_t len; + nodeAsText->GetLength(&len); + nsCOMPtr<nsIDOMNode> tmp = GetNodeLocation(node, &offset); + NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE); + if (!theOffset) { + // we are already set to go + } else if (theOffset == (int32_t)len) { + // update offset to point AFTER the text node + offset++; + } else { + // split the text node + nsresult rv = SplitNode(node, theOffset, getter_AddRefs(tmp)); + NS_ENSURE_SUCCESS(rv, rv); + tmp = GetNodeLocation(node, &offset); + } + // create br + nsresult rv = CreateNode(brType, tmp, offset, getter_AddRefs(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + *aInOutParent = tmp; + *aInOutOffset = offset+1; + } else { + nsresult rv = CreateNode(brType, node, theOffset, getter_AddRefs(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + (*aInOutOffset)++; + } + + *outBRNode = brNode; + if (*outBRNode && (aSelect != eNone)) { + int32_t offset; + nsCOMPtr<nsIDOMNode> parent = GetNodeLocation(*outBRNode, &offset); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + if (aSelect == eNext) { + // position selection after br + selection->SetInterlinePosition(true); + selection->Collapse(parent, offset + 1); + } else if (aSelect == ePrevious) { + // position selection before br + selection->SetInterlinePosition(true); + selection->Collapse(parent, offset); + } + } + return NS_OK; +} + + +NS_IMETHODIMP +TextEditor::CreateBR(nsIDOMNode* aNode, + int32_t aOffset, + nsCOMPtr<nsIDOMNode>* outBRNode, + EDirection aSelect) +{ + nsCOMPtr<nsIDOMNode> parent = aNode; + int32_t offset = aOffset; + return CreateBRImpl(address_of(parent), &offset, outBRNode, aSelect); +} + +nsresult +TextEditor::InsertBR(nsCOMPtr<nsIDOMNode>* outBRNode) +{ + NS_ENSURE_TRUE(outBRNode, NS_ERROR_NULL_POINTER); + *outBRNode = nullptr; + + // calling it text insertion to trigger moz br treatment by rules + AutoRules beginRulesSniffing(this, EditAction::insertText, nsIEditor::eNext); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + if (!selection->Collapsed()) { + nsresult rv = DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIDOMNode> selNode; + int32_t selOffset; + nsresult rv = + GetStartNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CreateBR(selNode, selOffset, outBRNode); + NS_ENSURE_SUCCESS(rv, rv); + + // position selection after br + selNode = GetNodeLocation(*outBRNode, &selOffset); + selection->SetInterlinePosition(true); + return selection->Collapse(selNode, selOffset+1); +} + +nsresult +TextEditor::ExtendSelectionForDelete(Selection* aSelection, + nsIEditor::EDirection* aAction) +{ + bool bCollapsed = aSelection->Collapsed(); + + if (*aAction == eNextWord || + *aAction == ePreviousWord || + (*aAction == eNext && bCollapsed) || + (*aAction == ePrevious && bCollapsed) || + *aAction == eToBeginningOfLine || + *aAction == eToEndOfLine) { + nsCOMPtr<nsISelectionController> selCont; + GetSelectionController(getter_AddRefs(selCont)); + NS_ENSURE_TRUE(selCont, NS_ERROR_NO_INTERFACE); + + nsresult rv; + switch (*aAction) { + case eNextWord: + rv = selCont->WordExtendForDelete(true); + // DeleteSelectionImpl doesn't handle these actions + // because it's inside batching, so don't confuse it: + *aAction = eNone; + break; + case ePreviousWord: + rv = selCont->WordExtendForDelete(false); + *aAction = eNone; + break; + case eNext: + rv = selCont->CharacterExtendForDelete(); + // Don't set aAction to eNone (see Bug 502259) + break; + case ePrevious: { + // Only extend the selection where the selection is after a UTF-16 + // surrogate pair or a variation selector. + // For other cases we don't want to do that, in order + // to make sure that pressing backspace will only delete the last + // typed character. + nsCOMPtr<nsIDOMNode> node; + int32_t offset; + rv = GetStartNodeAndOffset(aSelection, getter_AddRefs(node), &offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(node, NS_ERROR_FAILURE); + + // node might be anonymous DIV, so we find better text node + FindBetterInsertionPoint(node, offset); + + if (IsTextNode(node)) { + nsCOMPtr<nsIDOMCharacterData> charData = do_QueryInterface(node); + if (charData) { + nsAutoString data; + rv = charData->GetData(data); + NS_ENSURE_SUCCESS(rv, rv); + + if ((offset > 1 && + NS_IS_LOW_SURROGATE(data[offset - 1]) && + NS_IS_HIGH_SURROGATE(data[offset - 2])) || + (offset > 0 && + gfxFontUtils::IsVarSelector(data[offset - 1]))) { + rv = selCont->CharacterExtendForBackspace(); + } + } + } + break; + } + case eToBeginningOfLine: + selCont->IntraLineMove(true, false); // try to move to end + rv = selCont->IntraLineMove(false, true); // select to beginning + *aAction = eNone; + break; + case eToEndOfLine: + rv = selCont->IntraLineMove(true, true); + *aAction = eNext; + break; + default: // avoid several compiler warnings + rv = NS_OK; + break; + } + return rv; + } + return NS_OK; +} + +nsresult +TextEditor::DeleteSelection(EDirection aAction, + EStripWrappers aStripWrappers) +{ + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // delete placeholder txns merge. + AutoPlaceHolderBatch batch(this, nsGkAtoms::DeleteTxnName); + AutoRules beginRulesSniffing(this, EditAction::deleteSelection, aAction); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // If there is an existing selection when an extended delete is requested, + // platforms that use "caret-style" caret positioning collapse the + // selection to the start and then create a new selection. + // Platforms that use "selection-style" caret positioning just delete the + // existing selection without extending it. + if (!selection->Collapsed() && + (aAction == eNextWord || aAction == ePreviousWord || + aAction == eToBeginningOfLine || aAction == eToEndOfLine)) { + if (mCaretStyle == 1) { + nsresult rv = selection->CollapseToStart(); + NS_ENSURE_SUCCESS(rv, rv); + } else { + aAction = eNone; + } + } + + TextRulesInfo ruleInfo(EditAction::deleteSelection); + ruleInfo.collapsedAction = aAction; + ruleInfo.stripWrappers = aStripWrappers; + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (!cancel && !handled) { + rv = DeleteSelectionImpl(aAction, aStripWrappers); + } + if (!cancel) { + // post-process + rv = rules->DidDoAction(selection, &ruleInfo, rv); + } + return rv; +} + +NS_IMETHODIMP +TextEditor::InsertText(const nsAString& aStringToInsert) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + EditAction opID = EditAction::insertText; + if (ShouldHandleIMEComposition()) { + opID = EditAction::insertIMEText; + } + AutoPlaceHolderBatch batch(this, nullptr); + AutoRules beginRulesSniffing(this, opID, nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + nsAutoString resultString; + // XXX can we trust instring to outlive ruleInfo, + // XXX and ruleInfo not to refer to instring in its dtor? + //nsAutoString instring(aStringToInsert); + TextRulesInfo ruleInfo(opID); + ruleInfo.inString = &aStringToInsert; + ruleInfo.outString = &resultString; + ruleInfo.maxLength = mMaxTextLength; + + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (!cancel && !handled) { + // we rely on rules code for now - no default implementation + } + if (cancel) { + return NS_OK; + } + // post-process + return rules->DidDoAction(selection, &ruleInfo, rv); +} + +NS_IMETHODIMP +TextEditor::InsertLineBreak() +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertBreak, nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + TextRulesInfo ruleInfo(EditAction::insertBreak); + ruleInfo.maxLength = mMaxTextLength; + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (!cancel && !handled) { + // get the (collapsed) selection location + NS_ENSURE_STATE(selection->GetRangeAt(0)); + nsCOMPtr<nsINode> selNode = selection->GetRangeAt(0)->GetStartParent(); + int32_t selOffset = selection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_STATE(selNode); + + // don't put text in places that can't have it + if (!IsTextNode(selNode) && !CanContainTag(*selNode, + *nsGkAtoms::textTagName)) { + return NS_ERROR_FAILURE; + } + + // we need to get the doc + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + + // don't spaz my selection in subtransactions + AutoTransactionsConserveSelection dontSpazMySelection(this); + + // insert a linefeed character + rv = InsertTextImpl(NS_LITERAL_STRING("\n"), address_of(selNode), + &selOffset, doc); + if (!selNode) { + rv = NS_ERROR_NULL_POINTER; // don't return here, so DidDoAction is called + } + if (NS_SUCCEEDED(rv)) { + // set the selection to the correct location + rv = selection->Collapse(selNode, selOffset); + if (NS_SUCCEEDED(rv)) { + // see if we're at the end of the editor range + nsCOMPtr<nsIDOMNode> endNode; + int32_t endOffset; + rv = GetEndNodeAndOffset(selection, + getter_AddRefs(endNode), &endOffset); + + if (NS_SUCCEEDED(rv) && + endNode == GetAsDOMNode(selNode) && endOffset == selOffset) { + // SetInterlinePosition(true) means we want the caret to stick to the content on the "right". + // We want the caret to stick to whatever is past the break. This is + // because the break is on the same line we were on, but the next content + // will be on the following line. + selection->SetInterlinePosition(true); + } + } + } + } + + if (!cancel) { + // post-process, always called if WillInsertBreak didn't return cancel==true + rv = rules->DidDoAction(selection, &ruleInfo, rv); + } + return rv; +} + +nsresult +TextEditor::BeginIMEComposition(WidgetCompositionEvent* aEvent) +{ + NS_ENSURE_TRUE(!mComposition, NS_OK); + + if (IsPasswordEditor()) { + NS_ENSURE_TRUE(mRules, NS_ERROR_NULL_POINTER); + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + TextEditRules* textEditRules = static_cast<TextEditRules*>(rules.get()); + textEditRules->ResetIMETextPWBuf(); + } + + return EditorBase::BeginIMEComposition(aEvent); +} + +nsresult +TextEditor::UpdateIMEComposition(nsIDOMEvent* aDOMTextEvent) +{ + MOZ_ASSERT(aDOMTextEvent, "aDOMTextEvent must not be nullptr"); + + WidgetCompositionEvent* compositionChangeEvent = + aDOMTextEvent->WidgetEventPtr()->AsCompositionEvent(); + NS_ENSURE_TRUE(compositionChangeEvent, NS_ERROR_INVALID_ARG); + MOZ_ASSERT(compositionChangeEvent->mMessage == eCompositionChange, + "The internal event should be eCompositionChange"); + + if (!EnsureComposition(compositionChangeEvent)) { + return NS_OK; + } + + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + // NOTE: TextComposition should receive selection change notification before + // CompositionChangeEventHandlingMarker notifies TextComposition of the + // end of handling compositionchange event because TextComposition may + // need to ignore selection changes caused by composition. Therefore, + // CompositionChangeEventHandlingMarker must be destroyed after a call + // of NotifiyEditorObservers(eNotifyEditorObserversOfEnd) or + // NotifiyEditorObservers(eNotifyEditorObserversOfCancel) which notifies + // TextComposition of a selection change. + MOZ_ASSERT(!mPlaceHolderBatch, + "UpdateIMEComposition() must be called without place holder batch"); + TextComposition::CompositionChangeEventHandlingMarker + compositionChangeEventHandlingMarker(mComposition, compositionChangeEvent); + + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + + RefPtr<nsCaret> caretP = ps->GetCaret(); + + nsresult rv; + { + AutoPlaceHolderBatch batch(this, nsGkAtoms::IMETxnName); + + rv = InsertText(compositionChangeEvent->mData); + + if (caretP) { + caretP->SetSelection(selection); + } + } + + // If still composing, we should fire input event via observer. + // Note that if the composition will be committed by the following + // compositionend event, we don't need to notify editor observes of this + // change. + // NOTE: We must notify after the auto batch will be gone. + if (!compositionChangeEvent->IsFollowedByCompositionEnd()) { + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + } + + return rv; +} + +already_AddRefed<nsIContent> +TextEditor::GetInputEventTargetContent() +{ + nsCOMPtr<nsIContent> target = do_QueryInterface(mEventTarget); + return target.forget(); +} + +NS_IMETHODIMP +TextEditor::GetDocumentIsEmpty(bool* aDocumentIsEmpty) +{ + NS_ENSURE_TRUE(aDocumentIsEmpty, NS_ERROR_NULL_POINTER); + + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + return rules->DocumentIsEmpty(aDocumentIsEmpty); +} + +NS_IMETHODIMP +TextEditor::GetTextLength(int32_t* aCount) +{ + NS_ASSERTION(aCount, "null pointer"); + + // initialize out params + *aCount = 0; + + // special-case for empty document, to account for the bogus node + bool docEmpty; + nsresult rv = GetDocumentIsEmpty(&docEmpty); + NS_ENSURE_SUCCESS(rv, rv); + if (docEmpty) { + return NS_OK; + } + + dom::Element *rootElement = GetRoot(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIContentIterator> iter = + do_CreateInstance("@mozilla.org/content/post-content-iterator;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t totalLength = 0; + iter->Init(rootElement); + for (; !iter->IsDone(); iter->Next()) { + nsCOMPtr<nsIDOMNode> currentNode = do_QueryInterface(iter->GetCurrentNode()); + nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(currentNode); + if (textNode && IsEditable(currentNode)) { + uint32_t length; + textNode->GetLength(&length); + totalLength += length; + } + } + + *aCount = totalLength; + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::SetMaxTextLength(int32_t aMaxTextLength) +{ + mMaxTextLength = aMaxTextLength; + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::GetMaxTextLength(int32_t* aMaxTextLength) +{ + NS_ENSURE_TRUE(aMaxTextLength, NS_ERROR_INVALID_POINTER); + *aMaxTextLength = mMaxTextLength; + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::GetWrapWidth(int32_t* aWrapColumn) +{ + NS_ENSURE_TRUE( aWrapColumn, NS_ERROR_NULL_POINTER); + + *aWrapColumn = mWrapColumn; + return NS_OK; +} + +// +// See if the style value includes this attribute, and if it does, +// cut out everything from the attribute to the next semicolon. +// +static void CutStyle(const char* stylename, nsString& styleValue) +{ + // Find the current wrapping type: + int32_t styleStart = styleValue.Find(stylename, true); + if (styleStart >= 0) { + int32_t styleEnd = styleValue.Find(";", false, styleStart); + if (styleEnd > styleStart) { + styleValue.Cut(styleStart, styleEnd - styleStart + 1); + } else { + styleValue.Cut(styleStart, styleValue.Length() - styleStart); + } + } +} + +NS_IMETHODIMP +TextEditor::SetWrapWidth(int32_t aWrapColumn) +{ + SetWrapColumn(aWrapColumn); + + // Make sure we're a plaintext editor, otherwise we shouldn't + // do the rest of this. + if (!IsPlaintextEditor()) { + return NS_OK; + } + + // Ought to set a style sheet here ... + // Probably should keep around an mPlaintextStyleSheet for this purpose. + dom::Element *rootElement = GetRoot(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER); + + // Get the current style for this root element: + nsAutoString styleValue; + rootElement->GetAttr(kNameSpaceID_None, nsGkAtoms::style, styleValue); + + // We'll replace styles for these values: + CutStyle("white-space", styleValue); + CutStyle("width", styleValue); + CutStyle("font-family", styleValue); + + // If we have other style left, trim off any existing semicolons + // or whitespace, then add a known semicolon-space: + if (!styleValue.IsEmpty()) { + styleValue.Trim("; \t", false, true); + styleValue.AppendLiteral("; "); + } + + // Make sure we have fixed-width font. This should be done for us, + // but it isn't, see bug 22502, so we have to add "font: -moz-fixed;". + // Only do this if we're wrapping. + if (IsWrapHackEnabled() && aWrapColumn >= 0) { + styleValue.AppendLiteral("font-family: -moz-fixed; "); + } + + // and now we're ready to set the new whitespace/wrapping style. + if (aWrapColumn > 0) { + // Wrap to a fixed column. + styleValue.AppendLiteral("white-space: pre-wrap; width: "); + styleValue.AppendInt(aWrapColumn); + styleValue.AppendLiteral("ch;"); + } else if (!aWrapColumn) { + styleValue.AppendLiteral("white-space: pre-wrap;"); + } else { + styleValue.AppendLiteral("white-space: pre;"); + } + + return rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleValue, true); +} + +NS_IMETHODIMP +TextEditor::SetWrapColumn(int32_t aWrapColumn) +{ + mWrapColumn = aWrapColumn; + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::GetNewlineHandling(int32_t* aNewlineHandling) +{ + NS_ENSURE_ARG_POINTER(aNewlineHandling); + + *aNewlineHandling = mNewlineHandling; + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::SetNewlineHandling(int32_t aNewlineHandling) +{ + mNewlineHandling = aNewlineHandling; + + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::Undo(uint32_t aCount) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + AutoUpdateViewBatch beginViewBatching(this); + + ForceCompositionEnd(); + + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + + AutoRules beginRulesSniffing(this, EditAction::undo, nsIEditor::eNone); + + TextRulesInfo ruleInfo(EditAction::undo); + RefPtr<Selection> selection = GetSelection(); + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + + if (!cancel && NS_SUCCEEDED(rv)) { + rv = EditorBase::Undo(aCount); + rv = rules->DidDoAction(selection, &ruleInfo, rv); + } + + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + return rv; +} + +NS_IMETHODIMP +TextEditor::Redo(uint32_t aCount) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + AutoUpdateViewBatch beginViewBatching(this); + + ForceCompositionEnd(); + + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + + AutoRules beginRulesSniffing(this, EditAction::redo, nsIEditor::eNone); + + TextRulesInfo ruleInfo(EditAction::redo); + RefPtr<Selection> selection = GetSelection(); + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + + if (!cancel && NS_SUCCEEDED(rv)) { + rv = EditorBase::Redo(aCount); + rv = rules->DidDoAction(selection, &ruleInfo, rv); + } + + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + return rv; +} + +bool +TextEditor::CanCutOrCopy(PasswordFieldAllowed aPasswordFieldAllowed) +{ + RefPtr<Selection> selection = GetSelection(); + if (!selection) { + return false; + } + + if (aPasswordFieldAllowed == ePasswordFieldNotAllowed && + IsPasswordEditor()) { + return false; + } + + return !selection->Collapsed(); +} + +bool +TextEditor::FireClipboardEvent(EventMessage aEventMessage, + int32_t aSelectionType, + bool* aActionTaken) +{ + if (aEventMessage == ePaste) { + ForceCompositionEnd(); + } + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, false); + + RefPtr<Selection> selection = GetSelection(); + if (!selection) { + return false; + } + + if (!nsCopySupport::FireClipboardEvent(aEventMessage, aSelectionType, + presShell, selection, aActionTaken)) { + return false; + } + + // If the event handler caused the editor to be destroyed, return false. + // Otherwise return true to indicate that the event was not cancelled. + return !mDidPreDestroy; +} + +NS_IMETHODIMP +TextEditor::Cut() +{ + bool actionTaken = false; + if (FireClipboardEvent(eCut, nsIClipboard::kGlobalClipboard, &actionTaken)) { + DeleteSelection(eNone, eStrip); + } + return actionTaken ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TextEditor::CanCut(bool* aCanCut) +{ + NS_ENSURE_ARG_POINTER(aCanCut); + // Cut is always enabled in HTML documents + nsCOMPtr<nsIDocument> doc = GetDocument(); + *aCanCut = (doc && doc->IsHTMLOrXHTML()) || + (IsModifiable() && CanCutOrCopy(ePasswordFieldNotAllowed)); + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::Copy() +{ + bool actionTaken = false; + FireClipboardEvent(eCopy, nsIClipboard::kGlobalClipboard, &actionTaken); + + return actionTaken ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TextEditor::CanCopy(bool* aCanCopy) +{ + NS_ENSURE_ARG_POINTER(aCanCopy); + // Copy is always enabled in HTML documents + nsCOMPtr<nsIDocument> doc = GetDocument(); + *aCanCopy = (doc && doc->IsHTMLOrXHTML()) || + CanCutOrCopy(ePasswordFieldNotAllowed); + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::CanDelete(bool* aCanDelete) +{ + NS_ENSURE_ARG_POINTER(aCanDelete); + *aCanDelete = IsModifiable() && CanCutOrCopy(ePasswordFieldAllowed); + return NS_OK; +} + +// Shared between OutputToString and OutputToStream +NS_IMETHODIMP +TextEditor::GetAndInitDocEncoder(const nsAString& aFormatType, + uint32_t aFlags, + const nsACString& aCharset, + nsIDocumentEncoder** encoder) +{ + nsresult rv = NS_OK; + + nsAutoCString formatType(NS_DOC_ENCODER_CONTRACTID_BASE); + LossyAppendUTF16toASCII(aFormatType, formatType); + nsCOMPtr<nsIDocumentEncoder> docEncoder (do_CreateInstance(formatType.get(), &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMDocument> domDoc = do_QueryReferent(mDocWeak); + NS_ASSERTION(domDoc, "Need a document"); + + rv = docEncoder->Init(domDoc, aFormatType, aFlags); + NS_ENSURE_SUCCESS(rv, rv); + + if (!aCharset.IsEmpty() && !aCharset.EqualsLiteral("null")) { + docEncoder->SetCharset(aCharset); + } + + int32_t wc; + (void) GetWrapWidth(&wc); + if (wc >= 0) { + (void) docEncoder->SetWrapColumn(wc); + } + + // Set the selection, if appropriate. + // We do this either if the OutputSelectionOnly flag is set, + // in which case we use our existing selection ... + if (aFlags & nsIDocumentEncoder::OutputSelectionOnly) { + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + rv = docEncoder->SetSelection(selection); + NS_ENSURE_SUCCESS(rv, rv); + } + // ... or if the root element is not a body, + // in which case we set the selection to encompass the root. + else { + dom::Element* rootElement = GetRoot(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE); + if (!rootElement->IsHTMLElement(nsGkAtoms::body)) { + rv = docEncoder->SetNativeContainerNode(rootElement); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + docEncoder.forget(encoder); + return NS_OK; +} + + +NS_IMETHODIMP +TextEditor::OutputToString(const nsAString& aFormatType, + uint32_t aFlags, + nsAString& aOutputString) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + nsString resultString; + TextRulesInfo ruleInfo(EditAction::outputText); + ruleInfo.outString = &resultString; + // XXX Struct should store a nsAReadable* + nsAutoString str(aFormatType); + ruleInfo.outputFormat = &str; + bool cancel, handled; + nsresult rv = rules->WillDoAction(nullptr, &ruleInfo, &cancel, &handled); + if (cancel || NS_FAILED(rv)) { + return rv; + } + if (handled) { + // This case will get triggered by password fields. + aOutputString.Assign(*(ruleInfo.outString)); + return rv; + } + + nsAutoCString charsetStr; + rv = GetDocumentCharacterSet(charsetStr); + if (NS_FAILED(rv) || charsetStr.IsEmpty()) { + charsetStr.AssignLiteral("ISO-8859-1"); + } + + nsCOMPtr<nsIDocumentEncoder> encoder; + rv = GetAndInitDocEncoder(aFormatType, aFlags, charsetStr, getter_AddRefs(encoder)); + NS_ENSURE_SUCCESS(rv, rv); + return encoder->EncodeToString(aOutputString); +} + +NS_IMETHODIMP +TextEditor::OutputToStream(nsIOutputStream* aOutputStream, + const nsAString& aFormatType, + const nsACString& aCharset, + uint32_t aFlags) +{ + nsresult rv; + + // special-case for empty document when requesting plain text, + // to account for the bogus text node. + // XXX Should there be a similar test in OutputToString? + if (aFormatType.EqualsLiteral("text/plain")) { + bool docEmpty; + rv = GetDocumentIsEmpty(&docEmpty); + NS_ENSURE_SUCCESS(rv, rv); + + if (docEmpty) { + return NS_OK; // Output nothing. + } + } + + nsCOMPtr<nsIDocumentEncoder> encoder; + rv = GetAndInitDocEncoder(aFormatType, aFlags, aCharset, + getter_AddRefs(encoder)); + + NS_ENSURE_SUCCESS(rv, rv); + + return encoder->EncodeToStream(aOutputStream); +} + +NS_IMETHODIMP +TextEditor::InsertTextWithQuotations(const nsAString& aStringToInsert) +{ + return InsertText(aStringToInsert); +} + +NS_IMETHODIMP +TextEditor::PasteAsQuotation(int32_t aSelectionType) +{ + // Get Clipboard Service + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the nsITransferable interface for getting the data from the clipboard + nsCOMPtr<nsITransferable> trans; + rv = PrepareTransferable(getter_AddRefs(trans)); + if (NS_SUCCEEDED(rv) && trans) { + // Get the Data from the clipboard + clipboard->GetData(trans, aSelectionType); + + // Now we ask the transferable for the data + // it still owns the data, we just have a pointer to it. + // If it can't support a "text" output of the data the call will fail + nsCOMPtr<nsISupports> genericDataObj; + uint32_t len; + nsAutoCString flav; + rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj), + &len); + if (NS_FAILED(rv)) { + return rv; + } + + if (flav.EqualsLiteral(kUnicodeMime) || + flav.EqualsLiteral(kMozTextInternal)) { + nsCOMPtr<nsISupportsString> textDataObj ( do_QueryInterface(genericDataObj) ); + if (textDataObj && len > 0) { + nsAutoString stuffToPaste; + textDataObj->GetData ( stuffToPaste ); + AutoEditBatch beginBatching(this); + rv = InsertAsQuotation(stuffToPaste, 0); + } + } + } + + return rv; +} + +NS_IMETHODIMP +TextEditor::InsertAsQuotation(const nsAString& aQuotedText, + nsIDOMNode** aNodeInserted) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // Let the citer quote it for us: + nsString quotedStuff; + nsresult rv = InternetCiter::GetCiteString(aQuotedText, quotedStuff); + NS_ENSURE_SUCCESS(rv, rv); + + // It's best to put a blank line after the quoted text so that mails + // written without thinking won't be so ugly. + if (!aQuotedText.IsEmpty() && (aQuotedText.Last() != char16_t('\n'))) { + quotedStuff.Append(char16_t('\n')); + } + + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertText, nsIEditor::eNext); + + // give rules a chance to handle or cancel + TextRulesInfo ruleInfo(EditAction::insertElement); + bool cancel, handled; + rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (cancel) { + return NS_OK; // Rules canceled the operation. + } + if (!handled) { + rv = InsertText(quotedStuff); + + // XXX Should set *aNodeInserted to the first node inserted + if (aNodeInserted && NS_SUCCEEDED(rv)) { + *aNodeInserted = nullptr; + } + } + return rv; +} + +NS_IMETHODIMP +TextEditor::PasteAsCitedQuotation(const nsAString& aCitation, + int32_t aSelectionType) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TextEditor::InsertAsCitedQuotation(const nsAString& aQuotedText, + const nsAString& aCitation, + bool aInsertHTML, + nsIDOMNode** aNodeInserted) +{ + return InsertAsQuotation(aQuotedText, aNodeInserted); +} + +nsresult +TextEditor::SharedOutputString(uint32_t aFlags, + bool* aIsCollapsed, + nsAString& aResult) +{ + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + + *aIsCollapsed = selection->Collapsed(); + + if (!*aIsCollapsed) { + aFlags |= nsIDocumentEncoder::OutputSelectionOnly; + } + // If the selection isn't collapsed, we'll use the whole document. + + return OutputToString(NS_LITERAL_STRING("text/plain"), aFlags, aResult); +} + +NS_IMETHODIMP +TextEditor::Rewrap(bool aRespectNewlines) +{ + int32_t wrapCol; + nsresult rv = GetWrapWidth(&wrapCol); + NS_ENSURE_SUCCESS(rv, NS_OK); + + // Rewrap makes no sense if there's no wrap column; default to 72. + if (wrapCol <= 0) { + wrapCol = 72; + } + + nsAutoString current; + bool isCollapsed; + rv = SharedOutputString(nsIDocumentEncoder::OutputFormatted + | nsIDocumentEncoder::OutputLFLineBreak, + &isCollapsed, current); + NS_ENSURE_SUCCESS(rv, rv); + + nsString wrapped; + uint32_t firstLineOffset = 0; // XXX need to reset this if there is a selection + rv = InternetCiter::Rewrap(current, wrapCol, firstLineOffset, + aRespectNewlines, wrapped); + NS_ENSURE_SUCCESS(rv, rv); + + if (isCollapsed) { + SelectAll(); + } + + return InsertTextWithQuotations(wrapped); +} + +NS_IMETHODIMP +TextEditor::StripCites() +{ + nsAutoString current; + bool isCollapsed; + nsresult rv = SharedOutputString(nsIDocumentEncoder::OutputFormatted, + &isCollapsed, current); + NS_ENSURE_SUCCESS(rv, rv); + + nsString stripped; + rv = InternetCiter::StripCites(current, stripped); + NS_ENSURE_SUCCESS(rv, rv); + + if (isCollapsed) { + rv = SelectAll(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return InsertText(stripped); +} + +NS_IMETHODIMP +TextEditor::GetEmbeddedObjects(nsIArray** aNodeList) +{ + if (NS_WARN_IF(!aNodeList)) { + return NS_ERROR_INVALID_ARG; + } + + *aNodeList = nullptr; + return NS_OK; +} + +/** + * All editor operations which alter the doc should be prefaced + * with a call to StartOperation, naming the action and direction. + */ +NS_IMETHODIMP +TextEditor::StartOperation(EditAction opID, + nsIEditor::EDirection aDirection) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + EditorBase::StartOperation(opID, aDirection); // will set mAction, mDirection + if (rules) { + return rules->BeforeEdit(mAction, mDirection); + } + return NS_OK; +} + +/** + * All editor operations which alter the doc should be followed + * with a call to EndOperation. + */ +NS_IMETHODIMP +TextEditor::EndOperation() +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // post processing + nsresult rv = rules ? rules->AfterEdit(mAction, mDirection) : NS_OK; + EditorBase::EndOperation(); // will clear mAction, mDirection + return rv; +} + +nsresult +TextEditor::SelectEntireDocument(Selection* aSelection) +{ + if (!aSelection || !mRules) { + return NS_ERROR_NULL_POINTER; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // is doc empty? + bool bDocIsEmpty; + if (NS_SUCCEEDED(rules->DocumentIsEmpty(&bDocIsEmpty)) && bDocIsEmpty) { + // get root node + nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(GetRoot()); + NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE); + + // if it's empty don't select entire doc - that would select the bogus node + return aSelection->Collapse(rootElement, 0); + } + + SelectionBatcher selectionBatcher(aSelection); + nsresult rv = EditorBase::SelectEntireDocument(aSelection); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't select the trailing BR node if we have one + int32_t selOffset; + nsCOMPtr<nsIDOMNode> selNode; + rv = GetEndNodeAndOffset(aSelection, getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> childNode = GetChildAt(selNode, selOffset - 1); + + if (childNode && TextEditUtils::IsMozBR(childNode)) { + int32_t parentOffset; + nsCOMPtr<nsIDOMNode> parentNode = GetNodeLocation(childNode, &parentOffset); + + return aSelection->Extend(parentNode, parentOffset); + } + + return NS_OK; +} + +already_AddRefed<EventTarget> +TextEditor::GetDOMEventTarget() +{ + nsCOMPtr<EventTarget> copy = mEventTarget; + return copy.forget(); +} + + +nsresult +TextEditor::SetAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue, + bool aSuppressTransaction) +{ + return EditorBase::SetAttribute(aElement, aAttribute, aValue); +} + +nsresult +TextEditor::RemoveAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + bool aSuppressTransaction) +{ + return EditorBase::RemoveAttribute(aElement, aAttribute); +} + +} // namespace mozilla diff --git a/editor/libeditor/TextEditor.h b/editor/libeditor/TextEditor.h new file mode 100644 index 000000000..872cd91d3 --- /dev/null +++ b/editor/libeditor/TextEditor.h @@ -0,0 +1,246 @@ +/* -*- 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/. */ + +#ifndef mozilla_TextEditor_h +#define mozilla_TextEditor_h + +#include "mozilla/EditorBase.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIEditor.h" +#include "nsIEditorMailSupport.h" +#include "nsIPlaintextEditor.h" +#include "nsISupportsImpl.h" +#include "nscore.h" + +class nsIContent; +class nsIDOMDocument; +class nsIDOMElement; +class nsIDOMEvent; +class nsIDOMKeyEvent; +class nsIDOMNode; +class nsIDocumentEncoder; +class nsIEditRules; +class nsIOutputStream; +class nsISelectionController; +class nsITransferable; + +namespace mozilla { + +class AutoEditInitRulesTrigger; +class HTMLEditRules; +class TextEditRules; +namespace dom { +class Selection; +} // namespace dom + +/** + * The text editor implementation. + * Use to edit text document represented as a DOM tree. + */ +class TextEditor : public EditorBase + , public nsIPlaintextEditor + , public nsIEditorMailSupport +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextEditor, EditorBase) + + enum ETypingAction + { + eTypedText, /* user typed text */ + eTypedBR, /* user typed shift-enter to get a br */ + eTypedBreak /* user typed enter */ + }; + + TextEditor(); + + // nsIPlaintextEditor methods + NS_DECL_NSIPLAINTEXTEDITOR + + // nsIEditorMailSupport overrides + NS_DECL_NSIEDITORMAILSUPPORT + + // Overrides of EditorBase interface methods + NS_IMETHOD SetAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue, + bool aSuppressTransaction) override; + NS_IMETHOD RemoveAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + bool aSuppressTransaction) override; + + NS_IMETHOD Init(nsIDOMDocument* aDoc, nsIContent* aRoot, + nsISelectionController* aSelCon, uint32_t aFlags, + const nsAString& aValue) override; + + NS_IMETHOD GetDocumentIsEmpty(bool* aDocumentIsEmpty) override; + NS_IMETHOD GetIsDocumentEditable(bool* aIsDocumentEditable) override; + + NS_IMETHOD DeleteSelection(EDirection aAction, + EStripWrappers aStripWrappers) override; + + NS_IMETHOD SetDocumentCharacterSet(const nsACString& characterSet) override; + + NS_IMETHOD Undo(uint32_t aCount) override; + NS_IMETHOD Redo(uint32_t aCount) override; + + NS_IMETHOD Cut() override; + NS_IMETHOD CanCut(bool* aCanCut) override; + NS_IMETHOD Copy() override; + NS_IMETHOD CanCopy(bool* aCanCopy) override; + NS_IMETHOD CanDelete(bool* aCanDelete) override; + NS_IMETHOD Paste(int32_t aSelectionType) override; + NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override; + NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override; + NS_IMETHOD CanPasteTransferable(nsITransferable* aTransferable, + bool* aCanPaste) override; + + NS_IMETHOD OutputToString(const nsAString& aFormatType, + uint32_t aFlags, + nsAString& aOutputString) override; + + NS_IMETHOD OutputToStream(nsIOutputStream* aOutputStream, + const nsAString& aFormatType, + const nsACString& aCharsetOverride, + uint32_t aFlags) override; + + /** + * All editor operations which alter the doc should be prefaced + * with a call to StartOperation, naming the action and direction. + */ + NS_IMETHOD StartOperation(EditAction opID, + nsIEditor::EDirection aDirection) override; + + /** + * All editor operations which alter the doc should be followed + * with a call to EndOperation. + */ + NS_IMETHOD EndOperation() override; + + /** + * Make the given selection span the entire document. + */ + virtual nsresult SelectEntireDocument(Selection* aSelection) override; + + virtual nsresult HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) override; + + virtual already_AddRefed<dom::EventTarget> GetDOMEventTarget() override; + + virtual nsresult BeginIMEComposition(WidgetCompositionEvent* aEvent) override; + virtual nsresult UpdateIMEComposition(nsIDOMEvent* aTextEvent) override; + + virtual already_AddRefed<nsIContent> GetInputEventTargetContent() override; + + // Utility Routines, not part of public API + NS_IMETHOD TypedText(const nsAString& aString, ETypingAction aAction); + + nsresult InsertTextAt(const nsAString& aStringToInsert, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection); + + virtual nsresult InsertFromDataTransfer(dom::DataTransfer* aDataTransfer, + int32_t aIndex, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) override; + + virtual nsresult InsertFromDrop(nsIDOMEvent* aDropEvent) override; + + /** + * Extends the selection for given deletion operation + * If done, also update aAction to what's actually left to do after the + * extension. + */ + nsresult ExtendSelectionForDelete(Selection* aSelection, + nsIEditor::EDirection* aAction); + + /** + * Return true if the data is safe to insert as the source and destination + * principals match, or we are in a editor context where this doesn't matter. + * Otherwise, the data must be sanitized first. + */ + bool IsSafeToInsertData(nsIDOMDocument* aSourceDoc); + + static void GetDefaultEditorPrefs(int32_t& aNewLineHandling, + int32_t& aCaretStyle); + +protected: + virtual ~TextEditor(); + + NS_IMETHOD InitRules(); + void BeginEditorInit(); + nsresult EndEditorInit(); + + NS_IMETHOD GetAndInitDocEncoder(const nsAString& aFormatType, + uint32_t aFlags, + const nsACString& aCharset, + nsIDocumentEncoder** encoder); + + NS_IMETHOD CreateBR(nsIDOMNode* aNode, int32_t aOffset, + nsCOMPtr<nsIDOMNode>* outBRNode, + EDirection aSelect = eNone); + already_AddRefed<Element> CreateBRImpl(nsCOMPtr<nsINode>* aInOutParent, + int32_t* aInOutOffset, + EDirection aSelect); + nsresult CreateBRImpl(nsCOMPtr<nsIDOMNode>* aInOutParent, + int32_t* aInOutOffset, + nsCOMPtr<nsIDOMNode>* outBRNode, + EDirection aSelect); + nsresult InsertBR(nsCOMPtr<nsIDOMNode>* outBRNode); + + /** + * Factored methods for handling insertion of data from transferables + * (drag&drop or clipboard). + */ + NS_IMETHOD PrepareTransferable(nsITransferable** transferable); + NS_IMETHOD InsertTextFromTransferable(nsITransferable* transferable, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection); + + /** + * Shared outputstring; returns whether selection is collapsed and resulting + * string. + */ + nsresult SharedOutputString(uint32_t aFlags, bool* aIsCollapsed, + nsAString& aResult); + + /** + * Small utility routine to test the eEditorReadonly bit. + */ + bool IsModifiable(); + + enum PasswordFieldAllowed + { + ePasswordFieldAllowed, + ePasswordFieldNotAllowed + }; + bool CanCutOrCopy(PasswordFieldAllowed aPasswordFieldAllowed); + bool FireClipboardEvent(EventMessage aEventMessage, + int32_t aSelectionType, + bool* aActionTaken = nullptr); + + bool UpdateMetaCharset(nsIDOMDocument* aDocument, + const nsACString& aCharacterSet); + +protected: + nsCOMPtr<nsIEditRules> mRules; + int32_t mWrapColumn; + int32_t mMaxTextLength; + int32_t mInitTriggerCounter; + int32_t mNewlineHandling; + int32_t mCaretStyle; + + friend class AutoEditInitRulesTrigger; + friend class HTMLEditRules; + friend class TextEditRules; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_TextEditor_h diff --git a/editor/libeditor/TextEditorDataTransfer.cpp b/editor/libeditor/TextEditorDataTransfer.cpp new file mode 100644 index 000000000..0388aa4a8 --- /dev/null +++ b/editor/libeditor/TextEditorDataTransfer.cpp @@ -0,0 +1,472 @@ +/* -*- 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/TextEditor.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/SelectionState.h" +#include "mozilla/dom/Selection.h" +#include "nsAString.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIClipboard.h" +#include "nsIContent.h" +#include "nsIDOMDataTransfer.h" +#include "nsIDOMDocument.h" +#include "nsIDOMDragEvent.h" +#include "nsIDOMEvent.h" +#include "nsIDOMNode.h" +#include "nsIDOMUIEvent.h" +#include "nsIDocument.h" +#include "nsIDragService.h" +#include "nsIDragSession.h" +#include "nsIEditor.h" +#include "nsIEditorIMESupport.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIPrincipal.h" +#include "nsIFormControl.h" +#include "nsIPlaintextEditor.h" +#include "nsISupportsPrimitives.h" +#include "nsITransferable.h" +#include "nsIVariant.h" +#include "nsLiteralString.h" +#include "nsRange.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsXPCOM.h" +#include "nscore.h" + +class nsILoadContext; +class nsISupports; + +namespace mozilla { + +using namespace dom; + +NS_IMETHODIMP +TextEditor::PrepareTransferable(nsITransferable** transferable) +{ + // Create generic Transferable for getting the data + nsresult rv = CallCreateInstance("@mozilla.org/widget/transferable;1", transferable); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the nsITransferable interface for getting the data from the clipboard + if (transferable) { + nsCOMPtr<nsIDocument> destdoc = GetDocument(); + nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr; + (*transferable)->Init(loadContext); + + (*transferable)->AddDataFlavor(kUnicodeMime); + (*transferable)->AddDataFlavor(kMozTextInternal); + }; + return NS_OK; +} + +nsresult +TextEditor::InsertTextAt(const nsAString& aStringToInsert, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + if (aDestinationNode) { + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + nsCOMPtr<nsIDOMNode> targetNode = aDestinationNode; + int32_t targetOffset = aDestOffset; + + if (aDoDeleteSelection) { + // Use an auto tracker so that our drop point is correctly + // positioned after the delete. + AutoTrackDOMPoint tracker(mRangeUpdater, &targetNode, &targetOffset); + nsresult rv = DeleteSelection(eNone, eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsresult rv = selection->Collapse(targetNode, targetOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + return InsertText(aStringToInsert); +} + +NS_IMETHODIMP +TextEditor::InsertTextFromTransferable(nsITransferable* aTransferable, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + nsresult rv = NS_OK; + nsAutoCString bestFlavor; + nsCOMPtr<nsISupports> genericDataObj; + uint32_t len = 0; + if (NS_SUCCEEDED( + aTransferable->GetAnyTransferData(bestFlavor, + getter_AddRefs(genericDataObj), + &len)) && + (bestFlavor.EqualsLiteral(kUnicodeMime) || + bestFlavor.EqualsLiteral(kMozTextInternal))) { + AutoTransactionsConserveSelection dontSpazMySelection(this); + nsCOMPtr<nsISupportsString> textDataObj ( do_QueryInterface(genericDataObj) ); + if (textDataObj && len > 0) { + nsAutoString stuffToPaste; + textDataObj->GetData(stuffToPaste); + NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!"); + + // Sanitize possible carriage returns in the string to be inserted + nsContentUtils::PlatformToDOMLineBreaks(stuffToPaste); + + AutoEditBatch beginBatching(this); + rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection); + } + } + + // Try to scroll the selection into view if the paste/drop succeeded + + if (NS_SUCCEEDED(rv)) { + ScrollSelectionIntoView(false); + } + + return rv; +} + +nsresult +TextEditor::InsertFromDataTransfer(DataTransfer* aDataTransfer, + int32_t aIndex, + nsIDOMDocument* aSourceDoc, + nsIDOMNode* aDestinationNode, + int32_t aDestOffset, + bool aDoDeleteSelection) +{ + nsCOMPtr<nsIVariant> data; + DataTransfer::Cast(aDataTransfer)->GetDataAtNoSecurityCheck(NS_LITERAL_STRING("text/plain"), aIndex, + getter_AddRefs(data)); + if (data) { + nsAutoString insertText; + data->GetAsAString(insertText); + nsContentUtils::PlatformToDOMLineBreaks(insertText); + + AutoEditBatch beginBatching(this); + return InsertTextAt(insertText, aDestinationNode, aDestOffset, aDoDeleteSelection); + } + + return NS_OK; +} + +nsresult +TextEditor::InsertFromDrop(nsIDOMEvent* aDropEvent) +{ + ForceCompositionEnd(); + + nsCOMPtr<nsIDOMDragEvent> dragEvent(do_QueryInterface(aDropEvent)); + NS_ENSURE_TRUE(dragEvent, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMDataTransfer> domDataTransfer; + dragEvent->GetDataTransfer(getter_AddRefs(domDataTransfer)); + nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(domDataTransfer); + NS_ENSURE_TRUE(dataTransfer, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + NS_ASSERTION(dragSession, "No drag session"); + + nsCOMPtr<nsIDOMNode> sourceNode; + dataTransfer->GetMozSourceNode(getter_AddRefs(sourceNode)); + + nsCOMPtr<nsIDOMDocument> srcdomdoc; + if (sourceNode) { + sourceNode->GetOwnerDocument(getter_AddRefs(srcdomdoc)); + NS_ENSURE_TRUE(sourceNode, NS_ERROR_FAILURE); + } + + if (nsContentUtils::CheckForSubFrameDrop(dragSession, + aDropEvent->WidgetEventPtr()->AsDragEvent())) { + // Don't allow drags from subframe documents with different origins than + // the drop destination. + if (srcdomdoc && !IsSafeToInsertData(srcdomdoc)) { + return NS_OK; + } + } + + // Current doc is destination + nsCOMPtr<nsIDOMDocument> destdomdoc = GetDOMDocument(); + NS_ENSURE_TRUE(destdomdoc, NS_ERROR_NOT_INITIALIZED); + + uint32_t numItems = 0; + nsresult rv = dataTransfer->GetMozItemCount(&numItems); + NS_ENSURE_SUCCESS(rv, rv); + if (numItems < 1) { + return NS_ERROR_FAILURE; // Nothing to drop? + } + + // Combine any deletion and drop insertion into one transaction + AutoEditBatch beginBatching(this); + + bool deleteSelection = false; + + // We have to figure out whether to delete and relocate caret only once + // Parent and offset are under the mouse cursor + nsCOMPtr<nsIDOMUIEvent> uiEvent = do_QueryInterface(aDropEvent); + NS_ENSURE_TRUE(uiEvent, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMNode> newSelectionParent; + rv = uiEvent->GetRangeParent(getter_AddRefs(newSelectionParent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(newSelectionParent, NS_ERROR_FAILURE); + + int32_t newSelectionOffset; + rv = uiEvent->GetRangeOffset(&newSelectionOffset); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + bool isCollapsed = selection->Collapsed(); + + // Only the HTMLEditor::FindUserSelectAllNode returns a node. + nsCOMPtr<nsIDOMNode> userSelectNode = FindUserSelectAllNode(newSelectionParent); + if (userSelectNode) { + // The drop is happening over a "-moz-user-select: all" + // subtree so make sure the content we insert goes before + // the root of the subtree. + // + // XXX: Note that inserting before the subtree matches the + // current behavior when dropping on top of an image. + // The decision for dropping before or after the + // subtree should really be done based on coordinates. + + newSelectionParent = GetNodeLocation(userSelectNode, &newSelectionOffset); + + NS_ENSURE_TRUE(newSelectionParent, NS_ERROR_FAILURE); + } + + // Check if mouse is in the selection + // if so, jump through some hoops to determine if mouse is over selection (bail) + // and whether user wants to copy selection or delete it + if (!isCollapsed) { + // We never have to delete if selection is already collapsed + bool cursorIsInSelection = false; + + int32_t rangeCount; + rv = selection->GetRangeCount(&rangeCount); + NS_ENSURE_SUCCESS(rv, rv); + + for (int32_t j = 0; j < rangeCount; j++) { + RefPtr<nsRange> range = selection->GetRangeAt(j); + if (!range) { + // don't bail yet, iterate through them all + continue; + } + + rv = range->IsPointInRange(newSelectionParent, newSelectionOffset, &cursorIsInSelection); + if (cursorIsInSelection) { + break; + } + } + + if (cursorIsInSelection) { + // Dragging within same doc can't drop on itself -- leave! + if (srcdomdoc == destdomdoc) { + return NS_OK; + } + + // Dragging from another window onto a selection + // XXX Decision made to NOT do this, + // note that 4.x does replace if dropped on + //deleteSelection = true; + } else { + // We are NOT over the selection + if (srcdomdoc == destdomdoc) { + // Within the same doc: delete if user doesn't want to copy + uint32_t dropEffect; + dataTransfer->GetDropEffectInt(&dropEffect); + deleteSelection = !(dropEffect & nsIDragService::DRAGDROP_ACTION_COPY); + } else { + // Different source doc: Don't delete + deleteSelection = false; + } + } + } + + if (IsPlaintextEditor()) { + nsCOMPtr<nsIContent> content = do_QueryInterface(newSelectionParent); + while (content) { + nsCOMPtr<nsIFormControl> formControl(do_QueryInterface(content)); + if (formControl && !formControl->AllowDrop()) { + // Don't allow dropping into a form control that doesn't allow being + // dropped into. + return NS_OK; + } + content = content->GetParent(); + } + } + + for (uint32_t i = 0; i < numItems; ++i) { + InsertFromDataTransfer(dataTransfer, i, srcdomdoc, newSelectionParent, + newSelectionOffset, deleteSelection); + } + + if (NS_SUCCEEDED(rv)) { + ScrollSelectionIntoView(false); + } + + return rv; +} + +NS_IMETHODIMP +TextEditor::Paste(int32_t aSelectionType) +{ + if (!FireClipboardEvent(ePaste, aSelectionType)) { + return NS_OK; + } + + // Get Clipboard Service + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + if (NS_FAILED(rv)) { + return rv; + } + + // Get the nsITransferable interface for getting the data from the clipboard + nsCOMPtr<nsITransferable> trans; + rv = PrepareTransferable(getter_AddRefs(trans)); + if (NS_SUCCEEDED(rv) && trans) { + // Get the Data from the clipboard + if (NS_SUCCEEDED(clipboard->GetData(trans, aSelectionType)) && + IsModifiable()) { + // handle transferable hooks + nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument(); + if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, trans)) { + return NS_OK; + } + + rv = InsertTextFromTransferable(trans, nullptr, 0, true); + } + } + + return rv; +} + +NS_IMETHODIMP +TextEditor::PasteTransferable(nsITransferable* aTransferable) +{ + // Use an invalid value for the clipboard type as data comes from aTransferable + // and we don't currently implement a way to put that in the data transfer yet. + if (!FireClipboardEvent(ePaste, -1)) { + return NS_OK; + } + + if (!IsModifiable()) { + return NS_OK; + } + + // handle transferable hooks + nsCOMPtr<nsIDOMDocument> domdoc = GetDOMDocument(); + if (!EditorHookUtils::DoInsertionHook(domdoc, nullptr, aTransferable)) { + return NS_OK; + } + + return InsertTextFromTransferable(aTransferable, nullptr, 0, true); +} + +NS_IMETHODIMP +TextEditor::CanPaste(int32_t aSelectionType, + bool* aCanPaste) +{ + NS_ENSURE_ARG_POINTER(aCanPaste); + *aCanPaste = false; + + // can't paste if readonly + if (!IsModifiable()) { + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // the flavors that we can deal with + const char* textEditorFlavors[] = { kUnicodeMime }; + + bool haveFlavors; + rv = clipboard->HasDataMatchingFlavors(textEditorFlavors, + ArrayLength(textEditorFlavors), + aSelectionType, &haveFlavors); + NS_ENSURE_SUCCESS(rv, rv); + + *aCanPaste = haveFlavors; + return NS_OK; +} + + +NS_IMETHODIMP +TextEditor::CanPasteTransferable(nsITransferable* aTransferable, + bool* aCanPaste) +{ + NS_ENSURE_ARG_POINTER(aCanPaste); + + // can't paste if readonly + if (!IsModifiable()) { + *aCanPaste = false; + return NS_OK; + } + + // If |aTransferable| is null, assume that a paste will succeed. + if (!aTransferable) { + *aCanPaste = true; + return NS_OK; + } + + nsCOMPtr<nsISupports> data; + uint32_t dataLen; + nsresult rv = aTransferable->GetTransferData(kUnicodeMime, + getter_AddRefs(data), + &dataLen); + if (NS_SUCCEEDED(rv) && data) { + *aCanPaste = true; + } else { + *aCanPaste = false; + } + + return NS_OK; +} + +bool +TextEditor::IsSafeToInsertData(nsIDOMDocument* aSourceDoc) +{ + // Try to determine whether we should use a sanitizing fragment sink + bool isSafe = false; + + nsCOMPtr<nsIDocument> destdoc = GetDocument(); + NS_ASSERTION(destdoc, "Where is our destination doc?"); + nsCOMPtr<nsIDocShellTreeItem> dsti = destdoc->GetDocShell(); + nsCOMPtr<nsIDocShellTreeItem> root; + if (dsti) { + dsti->GetRootTreeItem(getter_AddRefs(root)); + } + nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(root); + uint32_t appType; + if (docShell && NS_SUCCEEDED(docShell->GetAppType(&appType))) { + isSafe = appType == nsIDocShell::APP_TYPE_EDITOR; + } + if (!isSafe && aSourceDoc) { + nsCOMPtr<nsIDocument> srcdoc = do_QueryInterface(aSourceDoc); + NS_ASSERTION(srcdoc, "Where is our source doc?"); + + nsIPrincipal* srcPrincipal = srcdoc->NodePrincipal(); + nsIPrincipal* destPrincipal = destdoc->NodePrincipal(); + NS_ASSERTION(srcPrincipal && destPrincipal, "How come we don't have a principal?"); + srcPrincipal->Subsumes(destPrincipal, &isSafe); + } + + return isSafe; +} + +} // namespace mozilla diff --git a/editor/libeditor/TextEditorTest.cpp b/editor/libeditor/TextEditorTest.cpp new file mode 100644 index 000000000..5378bc33b --- /dev/null +++ b/editor/libeditor/TextEditorTest.cpp @@ -0,0 +1,259 @@ +/* -*- 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/. */ + +#ifdef DEBUG + +#include "TextEditorTest.h" + +#include <stdio.h> + +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIDOMCharacterData.h" +#include "nsIDOMDocument.h" +#include "nsIDOMNode.h" +#include "nsIDOMNodeList.h" +#include "nsIEditor.h" +#include "nsIHTMLEditor.h" +#include "nsIPlaintextEditor.h" +#include "nsISelection.h" +#include "nsLiteralString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsStringFwd.h" + +#define TEST_RESULT(r) { if (NS_FAILED(r)) {printf("FAILURE result=%X\n", static_cast<uint32_t>(r)); return r; } } +#define TEST_POINTER(p) { if (!p) {printf("FAILURE null pointer\n"); return NS_ERROR_NULL_POINTER; } } + +TextEditorTest::TextEditorTest() +{ + printf("constructed a TextEditorTest\n"); +} + +TextEditorTest::~TextEditorTest() +{ + printf("destroyed a TextEditorTest\n"); +} + +void TextEditorTest::Run(nsIEditor *aEditor, int32_t *outNumTests, int32_t *outNumTestsFailed) +{ + if (!aEditor) return; + mTextEditor = do_QueryInterface(aEditor); + mEditor = do_QueryInterface(aEditor); + RunUnitTest(outNumTests, outNumTestsFailed); +} + +nsresult TextEditorTest::RunUnitTest(int32_t *outNumTests, int32_t *outNumTestsFailed) +{ + NS_ENSURE_TRUE(outNumTests && outNumTestsFailed, NS_ERROR_NULL_POINTER); + + *outNumTests = 0; + *outNumTestsFailed = 0; + + nsresult rv = InitDoc(); + TEST_RESULT(rv); + // shouldn't we just bail on error here? + + // insert some simple text + rv = mTextEditor->InsertText(NS_LITERAL_STRING("1234567890abcdefghij1234567890")); + TEST_RESULT(rv); + (*outNumTests)++; + if (NS_FAILED(rv)) { + ++(*outNumTestsFailed); + } + + // insert some more text + rv = mTextEditor->InsertText(NS_LITERAL_STRING("Moreover, I am cognizant of the interrelatedness of all communities and states. I cannot sit idly by in Atlanta and not be concerned about what happens in Birmingham. Injustice anywhere is a threat to justice everywhere")); + TEST_RESULT(rv); + (*outNumTests)++; + if (NS_FAILED(rv)) { + ++(*outNumTestsFailed); + } + + rv = TestInsertBreak(); + TEST_RESULT(rv); + (*outNumTests)++; + if (NS_FAILED(rv)) { + ++(*outNumTestsFailed); + } + + rv = TestTextProperties(); + TEST_RESULT(rv); + (*outNumTests)++; + if (NS_FAILED(rv)) { + ++(*outNumTestsFailed); + } + + // get us back to the original document + rv = mEditor->Undo(12); + TEST_RESULT(rv); + + return rv; +} + +nsresult TextEditorTest::InitDoc() +{ + nsresult rv = mEditor->SelectAll(); + TEST_RESULT(rv); + rv = mEditor->DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip); + TEST_RESULT(rv); + return rv; +} + +nsresult TextEditorTest::TestInsertBreak() +{ + nsCOMPtr<nsISelection>selection; + nsresult rv = mEditor->GetSelection(getter_AddRefs(selection)); + TEST_RESULT(rv); + TEST_POINTER(selection.get()); + nsCOMPtr<nsIDOMNode>anchor; + rv = selection->GetAnchorNode(getter_AddRefs(anchor)); + TEST_RESULT(rv); + TEST_POINTER(anchor.get()); + selection->Collapse(anchor, 0); + // insert one break + printf("inserting a break\n"); + rv = mTextEditor->InsertLineBreak(); + TEST_RESULT(rv); + mEditor->DebugDumpContent(); + + // insert a second break adjacent to the first + printf("inserting a second break\n"); + rv = mTextEditor->InsertLineBreak(); + TEST_RESULT(rv); + mEditor->DebugDumpContent(); + + return rv; +} + +nsresult TextEditorTest::TestTextProperties() +{ + nsCOMPtr<nsIDOMDocument>doc; + nsresult rv = mEditor->GetDocument(getter_AddRefs(doc)); + TEST_RESULT(rv); + TEST_POINTER(doc.get()); + nsCOMPtr<nsIDOMNodeList>nodeList; + // XXX This is broken, text nodes are not elements. + nsAutoString textTag(NS_LITERAL_STRING("#text")); + rv = doc->GetElementsByTagName(textTag, getter_AddRefs(nodeList)); + TEST_RESULT(rv); + TEST_POINTER(nodeList.get()); + uint32_t count; + nodeList->GetLength(&count); + NS_ASSERTION(0!=count, "there are no text nodes in the document!"); + nsCOMPtr<nsIDOMNode>textNode; + rv = nodeList->Item(count - 1, getter_AddRefs(textNode)); + TEST_RESULT(rv); + TEST_POINTER(textNode.get()); + + // set the whole text node to bold + printf("set the whole first text node to bold\n"); + nsCOMPtr<nsISelection>selection; + rv = mEditor->GetSelection(getter_AddRefs(selection)); + TEST_RESULT(rv); + TEST_POINTER(selection.get()); + nsCOMPtr<nsIDOMCharacterData>textData; + textData = do_QueryInterface(textNode); + uint32_t length; + textData->GetLength(&length); + selection->Collapse(textNode, 0); + selection->Extend(textNode, length); + + nsCOMPtr<nsIHTMLEditor> htmlEditor (do_QueryInterface(mTextEditor)); + NS_ENSURE_TRUE(htmlEditor, NS_ERROR_FAILURE); + + bool any = false; + bool all = false; + bool first=false; + + const nsAFlatString& empty = EmptyString(); + + rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first, + &any, &all); + TEST_RESULT(rv); + NS_ASSERTION(false==first, "first should be false"); + NS_ASSERTION(false==any, "any should be false"); + NS_ASSERTION(false==all, "all should be false"); + rv = htmlEditor->SetInlineProperty(nsGkAtoms::b, empty, empty); + TEST_RESULT(rv); + rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first, + &any, &all); + TEST_RESULT(rv); + NS_ASSERTION(true==first, "first should be true"); + NS_ASSERTION(true==any, "any should be true"); + NS_ASSERTION(true==all, "all should be true"); + mEditor->DebugDumpContent(); + + // remove the bold we just set + printf("set the whole first text node to not bold\n"); + rv = htmlEditor->RemoveInlineProperty(nsGkAtoms::b, empty); + TEST_RESULT(rv); + rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first, + &any, &all); + TEST_RESULT(rv); + NS_ASSERTION(false==first, "first should be false"); + NS_ASSERTION(false==any, "any should be false"); + NS_ASSERTION(false==all, "all should be false"); + mEditor->DebugDumpContent(); + + // set all but the first and last character to bold + printf("set the first text node (1, length-1) to bold and italic, and (2, length-1) to underline.\n"); + selection->Collapse(textNode, 1); + selection->Extend(textNode, length-1); + rv = htmlEditor->SetInlineProperty(nsGkAtoms::b, empty, empty); + TEST_RESULT(rv); + rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first, + &any, &all); + TEST_RESULT(rv); + NS_ASSERTION(true==first, "first should be true"); + NS_ASSERTION(true==any, "any should be true"); + NS_ASSERTION(true==all, "all should be true"); + mEditor->DebugDumpContent(); + // make all that same text italic + rv = htmlEditor->SetInlineProperty(nsGkAtoms::i, empty, empty); + TEST_RESULT(rv); + rv = htmlEditor->GetInlineProperty(nsGkAtoms::i, empty, empty, &first, + &any, &all); + TEST_RESULT(rv); + NS_ASSERTION(true==first, "first should be true"); + NS_ASSERTION(true==any, "any should be true"); + NS_ASSERTION(true==all, "all should be true"); + rv = htmlEditor->GetInlineProperty(nsGkAtoms::b, empty, empty, &first, + &any, &all); + TEST_RESULT(rv); + NS_ASSERTION(true==first, "first should be true"); + NS_ASSERTION(true==any, "any should be true"); + NS_ASSERTION(true==all, "all should be true"); + mEditor->DebugDumpContent(); + + // make all the text underlined, except for the first 2 and last 2 characters + rv = doc->GetElementsByTagName(textTag, getter_AddRefs(nodeList)); + TEST_RESULT(rv); + TEST_POINTER(nodeList.get()); + nodeList->GetLength(&count); + NS_ASSERTION(0!=count, "there are no text nodes in the document!"); + rv = nodeList->Item(count-2, getter_AddRefs(textNode)); + TEST_RESULT(rv); + TEST_POINTER(textNode.get()); + textData = do_QueryInterface(textNode); + textData->GetLength(&length); + NS_ASSERTION(length==915, "wrong text node"); + selection->Collapse(textNode, 1); + selection->Extend(textNode, length-2); + rv = htmlEditor->SetInlineProperty(nsGkAtoms::u, empty, empty); + TEST_RESULT(rv); + rv = htmlEditor->GetInlineProperty(nsGkAtoms::u, empty, empty, &first, + &any, &all); + TEST_RESULT(rv); + NS_ASSERTION(true==first, "first should be true"); + NS_ASSERTION(true==any, "any should be true"); + NS_ASSERTION(true==all, "all should be true"); + mEditor->DebugDumpContent(); + + return rv; +} + +#endif diff --git a/editor/libeditor/TextEditorTest.h b/editor/libeditor/TextEditorTest.h new file mode 100644 index 000000000..0483da463 --- /dev/null +++ b/editor/libeditor/TextEditorTest.h @@ -0,0 +1,42 @@ +/* -*- 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/. */ + +#ifndef __TextEditorTest_h__ +#define __TextEditorTest_h__ + +#include "nscore.h" + +class nsIEditor; +class nsIPlaintextEditor; +#ifdef DEBUG + +#include "nsCOMPtr.h" + +class TextEditorTest +{ +public: + + void Run(nsIEditor *aEditor, int32_t *outNumTests, int32_t *outNumTestsFailed); + TextEditorTest(); + ~TextEditorTest(); + +protected: + + /** create an empty document */ + nsresult InitDoc(); + + nsresult RunUnitTest(int32_t *outNumTests, int32_t *outNumTestsFailed); + + nsresult TestInsertBreak(); + + nsresult TestTextProperties(); + + nsCOMPtr<nsIPlaintextEditor> mTextEditor; + nsCOMPtr<nsIEditor> mEditor; +}; + +#endif /* DEBUG */ + +#endif diff --git a/editor/libeditor/TypeInState.cpp b/editor/libeditor/TypeInState.cpp new file mode 100644 index 000000000..ce43e5e4d --- /dev/null +++ b/editor/libeditor/TypeInState.cpp @@ -0,0 +1,397 @@ +/* -*- 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 "TypeInState.h" + +#include <stddef.h> + +#include "nsError.h" +#include "mozilla/EditorBase.h" +#include "mozilla/mozalloc.h" +#include "mozilla/dom/Selection.h" +#include "nsAString.h" +#include "nsDebug.h" +#include "nsGkAtoms.h" +#include "nsIDOMNode.h" +#include "nsISupportsBase.h" +#include "nsISupportsImpl.h" +#include "nsReadableUtils.h" +#include "nsStringFwd.h" + +class nsIAtom; +class nsIDOMDocument; + +namespace mozilla { + +using namespace dom; + +/******************************************************************** + * mozilla::TypeInState + *******************************************************************/ + +NS_IMPL_CYCLE_COLLECTION(TypeInState, mLastSelectionContainer) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TypeInState) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TypeInState) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TypeInState) + NS_INTERFACE_MAP_ENTRY(nsISelectionListener) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TypeInState::TypeInState() + : mRelativeFontSize(0) + , mLastSelectionOffset(0) +{ + Reset(); +} + +TypeInState::~TypeInState() +{ + // Call Reset() to release any data that may be in + // mClearedArray and mSetArray. + + Reset(); +} + +nsresult +TypeInState::UpdateSelState(Selection* aSelection) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + + if (!aSelection->Collapsed()) { + return NS_OK; + } + + return EditorBase::GetStartNodeAndOffset( + aSelection, getter_AddRefs(mLastSelectionContainer), + &mLastSelectionOffset); +} + + +NS_IMETHODIMP +TypeInState::NotifySelectionChanged(nsIDOMDocument* aDOMDocument, + nsISelection* aSelection, + int16_t aReason) +{ + // XXX: Selection currently generates bogus selection changed notifications + // XXX: (bug 140303). It can notify us when the selection hasn't actually + // XXX: changed, and it notifies us more than once for the same change. + // XXX: + // XXX: The following code attempts to work around the bogus notifications, + // XXX: and should probably be removed once bug 140303 is fixed. + // XXX: + // XXX: This code temporarily fixes the problem where clicking the mouse in + // XXX: the same location clears the type-in-state. + RefPtr<Selection> selection = + aSelection ? aSelection->AsSelection() : nullptr; + + if (aSelection) { + int32_t rangeCount = selection->RangeCount(); + + if (selection->Collapsed() && rangeCount) { + nsCOMPtr<nsIDOMNode> selNode; + int32_t selOffset = 0; + + nsresult rv = + EditorBase::GetStartNodeAndOffset(selection, getter_AddRefs(selNode), + &selOffset); + + NS_ENSURE_SUCCESS(rv, rv); + + if (selNode && + selNode == mLastSelectionContainer && + selOffset == mLastSelectionOffset) { + // We got a bogus selection changed notification! + return NS_OK; + } + + mLastSelectionContainer = selNode; + mLastSelectionOffset = selOffset; + } else { + mLastSelectionContainer = nullptr; + mLastSelectionOffset = 0; + } + } + + Reset(); + return NS_OK; +} + +void +TypeInState::Reset() +{ + for (size_t i = 0, n = mClearedArray.Length(); i < n; i++) { + delete mClearedArray[i]; + } + mClearedArray.Clear(); + for (size_t i = 0, n = mSetArray.Length(); i < n; i++) { + delete mSetArray[i]; + } + mSetArray.Clear(); +} + + +void +TypeInState::SetProp(nsIAtom* aProp, + const nsAString& aAttr, + const nsAString& aValue) +{ + // special case for big/small, these nest + if (nsGkAtoms::big == aProp) { + mRelativeFontSize++; + return; + } + if (nsGkAtoms::small == aProp) { + mRelativeFontSize--; + return; + } + + int32_t index; + if (IsPropSet(aProp, aAttr, nullptr, index)) { + // if it's already set, update the value + mSetArray[index]->value = aValue; + return; + } + + // Make a new propitem and add it to the list of set properties. + mSetArray.AppendElement(new PropItem(aProp, aAttr, aValue)); + + // remove it from the list of cleared properties, if we have a match + RemovePropFromClearedList(aProp, aAttr); +} + + +void +TypeInState::ClearAllProps() +{ + // null prop means "all" props + ClearProp(nullptr, EmptyString()); +} + +void +TypeInState::ClearProp(nsIAtom* aProp, + const nsAString& aAttr) +{ + // if it's already cleared we are done + if (IsPropCleared(aProp, aAttr)) { + return; + } + + // make a new propitem + PropItem* item = new PropItem(aProp, aAttr, EmptyString()); + + // remove it from the list of set properties, if we have a match + RemovePropFromSetList(aProp, aAttr); + + // add it to the list of cleared properties + mClearedArray.AppendElement(item); +} + + +/** + * TakeClearProperty() hands back next property item on the clear list. + * Caller assumes ownership of PropItem and must delete it. + */ +PropItem* +TypeInState::TakeClearProperty() +{ + size_t count = mClearedArray.Length(); + if (!count) { + return nullptr; + } + + --count; // indices are zero based + PropItem* propItem = mClearedArray[count]; + mClearedArray.RemoveElementAt(count); + return propItem; +} + +/** + * TakeSetProperty() hands back next poroperty item on the set list. + * Caller assumes ownership of PropItem and must delete it. + */ +PropItem* +TypeInState::TakeSetProperty() +{ + size_t count = mSetArray.Length(); + if (!count) { + return nullptr; + } + count--; // indices are zero based + PropItem* propItem = mSetArray[count]; + mSetArray.RemoveElementAt(count); + return propItem; +} + +/** + * TakeRelativeFontSize() hands back relative font value, which is then + * cleared out. + */ +int32_t +TypeInState::TakeRelativeFontSize() +{ + int32_t relSize = mRelativeFontSize; + mRelativeFontSize = 0; + return relSize; +} + +void +TypeInState::GetTypingState(bool& isSet, + bool& theSetting, + nsIAtom* aProp) +{ + GetTypingState(isSet, theSetting, aProp, EmptyString(), nullptr); +} + +void +TypeInState::GetTypingState(bool& isSet, + bool& theSetting, + nsIAtom* aProp, + const nsString& aAttr, + nsString* aValue) +{ + if (IsPropSet(aProp, aAttr, aValue)) { + isSet = true; + theSetting = true; + } else if (IsPropCleared(aProp, aAttr)) { + isSet = true; + theSetting = false; + } else { + isSet = false; + } +} + +void +TypeInState::RemovePropFromSetList(nsIAtom* aProp, + const nsAString& aAttr) +{ + int32_t index; + if (!aProp) { + // clear _all_ props + for (size_t i = 0, n = mSetArray.Length(); i < n; i++) { + delete mSetArray[i]; + } + mSetArray.Clear(); + mRelativeFontSize=0; + } else if (FindPropInList(aProp, aAttr, nullptr, mSetArray, index)) { + delete mSetArray[index]; + mSetArray.RemoveElementAt(index); + } +} + +void +TypeInState::RemovePropFromClearedList(nsIAtom* aProp, + const nsAString& aAttr) +{ + int32_t index; + if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, index)) { + delete mClearedArray[index]; + mClearedArray.RemoveElementAt(index); + } +} + +bool +TypeInState::IsPropSet(nsIAtom* aProp, + const nsAString& aAttr, + nsAString* outValue) +{ + int32_t i; + return IsPropSet(aProp, aAttr, outValue, i); +} + +bool +TypeInState::IsPropSet(nsIAtom* aProp, + const nsAString& aAttr, + nsAString* outValue, + int32_t& outIndex) +{ + // linear search. list should be short. + size_t count = mSetArray.Length(); + for (size_t i = 0; i < count; i++) { + PropItem *item = mSetArray[i]; + if (item->tag == aProp && item->attr == aAttr) { + if (outValue) { + *outValue = item->value; + } + outIndex = i; + return true; + } + } + return false; +} + + +bool +TypeInState::IsPropCleared(nsIAtom* aProp, + const nsAString& aAttr) +{ + int32_t i; + return IsPropCleared(aProp, aAttr, i); +} + + +bool +TypeInState::IsPropCleared(nsIAtom* aProp, + const nsAString& aAttr, + int32_t& outIndex) +{ + if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, outIndex)) { + return true; + } + if (FindPropInList(0, EmptyString(), nullptr, mClearedArray, outIndex)) { + // special case for all props cleared + outIndex = -1; + return true; + } + return false; +} + +bool +TypeInState::FindPropInList(nsIAtom* aProp, + const nsAString& aAttr, + nsAString* outValue, + nsTArray<PropItem*>& aList, + int32_t& outIndex) +{ + // linear search. list should be short. + size_t count = aList.Length(); + for (size_t i = 0; i < count; i++) { + PropItem *item = aList[i]; + if (item->tag == aProp && item->attr == aAttr) { + if (outValue) { + *outValue = item->value; + } + outIndex = i; + return true; + } + } + return false; +} + +/******************************************************************** + * mozilla::PropItem: helper struct for mozilla::TypeInState + *******************************************************************/ + +PropItem::PropItem() + : tag(nullptr) +{ + MOZ_COUNT_CTOR(PropItem); +} + +PropItem::PropItem(nsIAtom* aTag, + const nsAString& aAttr, + const nsAString &aValue) + : tag(aTag) + , attr(aAttr) + , value(aValue) +{ + MOZ_COUNT_CTOR(PropItem); +} + +PropItem::~PropItem() +{ + MOZ_COUNT_DTOR(PropItem); +} + +} // namespace mozilla diff --git a/editor/libeditor/TypeInState.h b/editor/libeditor/TypeInState.h new file mode 100644 index 000000000..540b2d9c1 --- /dev/null +++ b/editor/libeditor/TypeInState.h @@ -0,0 +1,111 @@ +/* -*- 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/. */ + +#ifndef TypeInState_h +#define TypeInState_h + +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISelectionListener.h" +#include "nsISupportsImpl.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nscore.h" + +// Workaround for windows headers +#ifdef SetProp +#undef SetProp +#endif + +class nsIAtom; +class nsIDOMNode; + +namespace mozilla { + +class HTMLEditRules; +namespace dom { +class Selection; +} // namespace dom + +struct PropItem +{ + nsIAtom* tag; + nsString attr; + nsString value; + + PropItem(); + PropItem(nsIAtom* aTag, const nsAString& aAttr, const nsAString& aValue); + ~PropItem(); +}; + +class TypeInState final : public nsISelectionListener +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(TypeInState) + + TypeInState(); + void Reset(); + + nsresult UpdateSelState(dom::Selection* aSelection); + + // nsISelectionListener + NS_DECL_NSISELECTIONLISTENER + + void SetProp(nsIAtom* aProp, const nsAString& aAttr, const nsAString& aValue); + + void ClearAllProps(); + void ClearProp(nsIAtom* aProp, const nsAString& aAttr); + + /** + * TakeClearProperty() hands back next property item on the clear list. + * Caller assumes ownership of PropItem and must delete it. + */ + PropItem* TakeClearProperty(); + + /** + * TakeSetProperty() hands back next property item on the set list. + * Caller assumes ownership of PropItem and must delete it. + */ + PropItem* TakeSetProperty(); + + /** + * TakeRelativeFontSize() hands back relative font value, which is then + * cleared out. + */ + int32_t TakeRelativeFontSize(); + + void GetTypingState(bool& isSet, bool& theSetting, nsIAtom* aProp); + void GetTypingState(bool& isSet, bool& theSetting, nsIAtom* aProp, + const nsString& aAttr, nsString* outValue); + + static bool FindPropInList(nsIAtom* aProp, const nsAString& aAttr, + nsAString* outValue, nsTArray<PropItem*>& aList, + int32_t& outIndex); + +protected: + virtual ~TypeInState(); + + void RemovePropFromSetList(nsIAtom* aProp, const nsAString& aAttr); + void RemovePropFromClearedList(nsIAtom* aProp, const nsAString& aAttr); + bool IsPropSet(nsIAtom* aProp, const nsAString& aAttr, nsAString* outValue); + bool IsPropSet(nsIAtom* aProp, const nsAString& aAttr, nsAString* outValue, + int32_t& outIndex); + bool IsPropCleared(nsIAtom* aProp, const nsAString& aAttr); + bool IsPropCleared(nsIAtom* aProp, const nsAString& aAttr, int32_t& outIndex); + + nsTArray<PropItem*> mSetArray; + nsTArray<PropItem*> mClearedArray; + int32_t mRelativeFontSize; + nsCOMPtr<nsIDOMNode> mLastSelectionContainer; + int32_t mLastSelectionOffset; + + friend class HTMLEditRules; +}; + +} // namespace mozilla + +#endif // #ifndef TypeInState_h + diff --git a/editor/libeditor/WSRunObject.cpp b/editor/libeditor/WSRunObject.cpp new file mode 100644 index 000000000..39ac3fee8 --- /dev/null +++ b/editor/libeditor/WSRunObject.cpp @@ -0,0 +1,1926 @@ +/* -*- 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 "WSRunObject.h" + +#include "TextEditUtils.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/mozalloc.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/SelectionState.h" + +#include "nsAString.h" +#include "nsCRT.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIContent.h" +#include "nsIDOMDocument.h" +#include "nsIDOMNode.h" +#include "nsISupportsImpl.h" +#include "nsRange.h" +#include "nsString.h" +#include "nsTextFragment.h" + +namespace mozilla { + +using namespace dom; + +const char16_t nbsp = 160; + +WSRunObject::WSRunObject(HTMLEditor* aHTMLEditor, + nsINode* aNode, + int32_t aOffset) + : mNode(aNode) + , mOffset(aOffset) + , mPRE(false) + , mStartOffset(0) + , mEndOffset(0) + , mFirstNBSPOffset(0) + , mLastNBSPOffset(0) + , mStartRun(nullptr) + , mEndRun(nullptr) + , mHTMLEditor(aHTMLEditor) +{ + GetWSNodes(); + GetRuns(); +} + +WSRunObject::WSRunObject(HTMLEditor* aHTMLEditor, + nsIDOMNode* aNode, + int32_t aOffset) + : mNode(do_QueryInterface(aNode)) + , mOffset(aOffset) + , mPRE(false) + , mStartOffset(0) + , mEndOffset(0) + , mFirstNBSPOffset(0) + , mLastNBSPOffset(0) + , mStartRun(nullptr) + , mEndRun(nullptr) + , mHTMLEditor(aHTMLEditor) +{ + GetWSNodes(); + GetRuns(); +} + +WSRunObject::~WSRunObject() +{ + ClearRuns(); +} + +nsresult +WSRunObject::ScrubBlockBoundary(HTMLEditor* aHTMLEditor, + BlockBoundary aBoundary, + nsINode* aBlock, + int32_t aOffset) +{ + NS_ENSURE_TRUE(aHTMLEditor && aBlock, NS_ERROR_NULL_POINTER); + + int32_t offset; + if (aBoundary == kBlockStart) { + offset = 0; + } else if (aBoundary == kBlockEnd) { + offset = aBlock->Length(); + } else { + // Else we are scrubbing an outer boundary - just before or after a block + // element. + NS_ENSURE_STATE(aOffset >= 0); + offset = aOffset; + } + + WSRunObject theWSObj(aHTMLEditor, aBlock, offset); + return theWSObj.Scrub(); +} + +nsresult +WSRunObject::PrepareToJoinBlocks(HTMLEditor* aHTMLEditor, + Element* aLeftBlock, + Element* aRightBlock) +{ + NS_ENSURE_TRUE(aLeftBlock && aRightBlock && aHTMLEditor, + NS_ERROR_NULL_POINTER); + + WSRunObject leftWSObj(aHTMLEditor, aLeftBlock, aLeftBlock->Length()); + WSRunObject rightWSObj(aHTMLEditor, aRightBlock, 0); + + return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj); +} + +nsresult +WSRunObject::PrepareToDeleteRange(HTMLEditor* aHTMLEditor, + nsCOMPtr<nsINode>* aStartNode, + int32_t* aStartOffset, + nsCOMPtr<nsINode>* aEndNode, + int32_t* aEndOffset) +{ + NS_ENSURE_TRUE(aHTMLEditor && aStartNode && *aStartNode && aStartOffset && + aEndNode && *aEndNode && aEndOffset, NS_ERROR_NULL_POINTER); + + AutoTrackDOMPoint trackerStart(aHTMLEditor->mRangeUpdater, + aStartNode, aStartOffset); + AutoTrackDOMPoint trackerEnd(aHTMLEditor->mRangeUpdater, + aEndNode, aEndOffset); + + WSRunObject leftWSObj(aHTMLEditor, *aStartNode, *aStartOffset); + WSRunObject rightWSObj(aHTMLEditor, *aEndNode, *aEndOffset); + + return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj); +} + +nsresult +WSRunObject::PrepareToDeleteNode(HTMLEditor* aHTMLEditor, + nsIContent* aContent) +{ + NS_ENSURE_TRUE(aContent && aHTMLEditor, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> parent = aContent->GetParentNode(); + NS_ENSURE_STATE(parent); + int32_t offset = parent->IndexOf(aContent); + + WSRunObject leftWSObj(aHTMLEditor, parent, offset); + WSRunObject rightWSObj(aHTMLEditor, parent, offset + 1); + + return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj); +} + +nsresult +WSRunObject::PrepareToSplitAcrossBlocks(HTMLEditor* aHTMLEditor, + nsCOMPtr<nsINode>* aSplitNode, + int32_t* aSplitOffset) +{ + NS_ENSURE_TRUE(aHTMLEditor && aSplitNode && *aSplitNode && aSplitOffset, + NS_ERROR_NULL_POINTER); + + AutoTrackDOMPoint tracker(aHTMLEditor->mRangeUpdater, + aSplitNode, aSplitOffset); + + WSRunObject wsObj(aHTMLEditor, *aSplitNode, *aSplitOffset); + + return wsObj.PrepareToSplitAcrossBlocksPriv(); +} + +already_AddRefed<Element> +WSRunObject::InsertBreak(nsCOMPtr<nsINode>* aInOutParent, + int32_t* aInOutOffset, + nsIEditor::EDirection aSelect) +{ + // MOOSE: for now, we always assume non-PRE formatting. Fix this later. + // meanwhile, the pre case is handled in WillInsertText in + // HTMLEditRules.cpp + NS_ENSURE_TRUE(aInOutParent && aInOutOffset, nullptr); + + WSFragment *beforeRun, *afterRun; + FindRun(*aInOutParent, *aInOutOffset, &beforeRun, false); + FindRun(*aInOutParent, *aInOutOffset, &afterRun, true); + + { + // Some scoping for AutoTrackDOMPoint. This will track our insertion + // point while we tweak any surrounding whitespace + AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, aInOutParent, + aInOutOffset); + + // Handle any changes needed to ws run after inserted br + if (!afterRun || (afterRun->mType & WSType::trailingWS)) { + // Don't need to do anything. Just insert break. ws won't change. + } else if (afterRun->mType & WSType::leadingWS) { + // Delete the leading ws that is after insertion point. We don't + // have to (it would still not be significant after br), but it's + // just more aesthetically pleasing to. + nsresult rv = DeleteChars(*aInOutParent, *aInOutOffset, + afterRun->mEndNode, afterRun->mEndOffset, + eOutsideUserSelectAll); + NS_ENSURE_SUCCESS(rv, nullptr); + } else if (afterRun->mType == WSType::normalWS) { + // Need to determine if break at front of non-nbsp run. If so, convert + // run to nbsp. + WSPoint thePoint = GetCharAfter(*aInOutParent, *aInOutOffset); + if (thePoint.mTextNode && nsCRT::IsAsciiSpace(thePoint.mChar)) { + WSPoint prevPoint = GetCharBefore(thePoint); + if (prevPoint.mTextNode && !nsCRT::IsAsciiSpace(prevPoint.mChar)) { + // We are at start of non-nbsps. Convert to a single nbsp. + nsresult rv = ConvertToNBSP(thePoint); + NS_ENSURE_SUCCESS(rv, nullptr); + } + } + } + + // Handle any changes needed to ws run before inserted br + if (!beforeRun || (beforeRun->mType & WSType::leadingWS)) { + // Don't need to do anything. Just insert break. ws won't change. + } else if (beforeRun->mType & WSType::trailingWS) { + // Need to delete the trailing ws that is before insertion point, because it + // would become significant after break inserted. + nsresult rv = DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset, + *aInOutParent, *aInOutOffset, + eOutsideUserSelectAll); + NS_ENSURE_SUCCESS(rv, nullptr); + } else if (beforeRun->mType == WSType::normalWS) { + // Try to change an nbsp to a space, just to prevent nbsp proliferation + nsresult rv = CheckTrailingNBSP(beforeRun, *aInOutParent, *aInOutOffset); + NS_ENSURE_SUCCESS(rv, nullptr); + } + } + + // ready, aim, fire! + return mHTMLEditor->CreateBRImpl(aInOutParent, aInOutOffset, aSelect); +} + +nsresult +WSRunObject::InsertText(const nsAString& aStringToInsert, + nsCOMPtr<nsINode>* aInOutParent, + int32_t* aInOutOffset, + nsIDocument* aDoc) +{ + // MOOSE: for now, we always assume non-PRE formatting. Fix this later. + // meanwhile, the pre case is handled in WillInsertText in + // HTMLEditRules.cpp + + // MOOSE: for now, just getting the ws logic straight. This implementation + // is very slow. Will need to replace edit rules impl with a more efficient + // text sink here that does the minimal amount of searching/replacing/copying + + NS_ENSURE_TRUE(aInOutParent && aInOutOffset && aDoc, NS_ERROR_NULL_POINTER); + + if (aStringToInsert.IsEmpty()) { + return NS_OK; + } + + nsAutoString theString(aStringToInsert); + + WSFragment *beforeRun, *afterRun; + FindRun(*aInOutParent, *aInOutOffset, &beforeRun, false); + FindRun(*aInOutParent, *aInOutOffset, &afterRun, true); + + { + // Some scoping for AutoTrackDOMPoint. This will track our insertion + // point while we tweak any surrounding whitespace + AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, aInOutParent, + aInOutOffset); + + // Handle any changes needed to ws run after inserted text + if (!afterRun || afterRun->mType & WSType::trailingWS) { + // Don't need to do anything. Just insert text. ws won't change. + } else if (afterRun->mType & WSType::leadingWS) { + // Delete the leading ws that is after insertion point, because it + // would become significant after text inserted. + nsresult rv = + DeleteChars(*aInOutParent, *aInOutOffset, afterRun->mEndNode, + afterRun->mEndOffset, eOutsideUserSelectAll); + NS_ENSURE_SUCCESS(rv, rv); + } else if (afterRun->mType == WSType::normalWS) { + // Try to change an nbsp to a space, if possible, just to prevent nbsp + // proliferation + nsresult rv = CheckLeadingNBSP(afterRun, *aInOutParent, *aInOutOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Handle any changes needed to ws run before inserted text + if (!beforeRun || beforeRun->mType & WSType::leadingWS) { + // Don't need to do anything. Just insert text. ws won't change. + } else if (beforeRun->mType & WSType::trailingWS) { + // Need to delete the trailing ws that is before insertion point, because + // it would become significant after text inserted. + nsresult rv = + DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset, + *aInOutParent, *aInOutOffset, eOutsideUserSelectAll); + NS_ENSURE_SUCCESS(rv, rv); + } else if (beforeRun->mType == WSType::normalWS) { + // Try to change an nbsp to a space, if possible, just to prevent nbsp + // proliferation + nsresult rv = CheckTrailingNBSP(beforeRun, *aInOutParent, *aInOutOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Next up, tweak head and tail of string as needed. First the head: there + // are a variety of circumstances that would require us to convert a leading + // ws char into an nbsp: + + if (nsCRT::IsAsciiSpace(theString[0])) { + // We have a leading space + if (beforeRun) { + if (beforeRun->mType & WSType::leadingWS) { + theString.SetCharAt(nbsp, 0); + } else if (beforeRun->mType & WSType::normalWS) { + WSPoint wspoint = GetCharBefore(*aInOutParent, *aInOutOffset); + if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) { + theString.SetCharAt(nbsp, 0); + } + } + } else if (mStartReason & WSType::block || mStartReason == WSType::br) { + theString.SetCharAt(nbsp, 0); + } + } + + // Then the tail + uint32_t lastCharIndex = theString.Length() - 1; + + if (nsCRT::IsAsciiSpace(theString[lastCharIndex])) { + // We have a leading space + if (afterRun) { + if (afterRun->mType & WSType::trailingWS) { + theString.SetCharAt(nbsp, lastCharIndex); + } else if (afterRun->mType & WSType::normalWS) { + WSPoint wspoint = GetCharAfter(*aInOutParent, *aInOutOffset); + if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) { + theString.SetCharAt(nbsp, lastCharIndex); + } + } + } else if (mEndReason & WSType::block) { + theString.SetCharAt(nbsp, lastCharIndex); + } + } + + // Next, scan string for adjacent ws and convert to nbsp/space combos + // MOOSE: don't need to convert tabs here since that is done by + // WillInsertText() before we are called. Eventually, all that logic will be + // pushed down into here and made more efficient. + bool prevWS = false; + for (uint32_t i = 0; i <= lastCharIndex; i++) { + if (nsCRT::IsAsciiSpace(theString[i])) { + if (prevWS) { + // i - 1 can't be negative because prevWS starts out false + theString.SetCharAt(nbsp, i - 1); + } else { + prevWS = true; + } + } else { + prevWS = false; + } + } + + // Ready, aim, fire! + mHTMLEditor->InsertTextImpl(theString, aInOutParent, aInOutOffset, aDoc); + return NS_OK; +} + +nsresult +WSRunObject::DeleteWSBackward() +{ + WSPoint point = GetCharBefore(mNode, mOffset); + NS_ENSURE_TRUE(point.mTextNode, NS_OK); // nothing to delete + + // Easy case, preformatted ws. + if (mPRE && (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp)) { + return DeleteChars(point.mTextNode, point.mOffset, + point.mTextNode, point.mOffset + 1); + } + + // Caller's job to ensure that previous char is really ws. If it is normal + // ws, we need to delete the whole run. + if (nsCRT::IsAsciiSpace(point.mChar)) { + RefPtr<Text> startNodeText, endNodeText; + int32_t startOffset, endOffset; + GetAsciiWSBounds(eBoth, point.mTextNode, point.mOffset + 1, + getter_AddRefs(startNodeText), &startOffset, + getter_AddRefs(endNodeText), &endOffset); + + // adjust surrounding ws + nsCOMPtr<nsINode> startNode = startNodeText.get(); + nsCOMPtr<nsINode> endNode = endNodeText.get(); + nsresult rv = + WSRunObject::PrepareToDeleteRange(mHTMLEditor, + address_of(startNode), &startOffset, + address_of(endNode), &endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // finally, delete that ws + return DeleteChars(startNode, startOffset, endNode, endOffset); + } + + if (point.mChar == nbsp) { + nsCOMPtr<nsINode> node(point.mTextNode); + // adjust surrounding ws + int32_t startOffset = point.mOffset; + int32_t endOffset = point.mOffset + 1; + nsresult rv = + WSRunObject::PrepareToDeleteRange(mHTMLEditor, + address_of(node), &startOffset, + address_of(node), &endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // finally, delete that ws + return DeleteChars(node, startOffset, node, endOffset); + } + + return NS_OK; +} + +nsresult +WSRunObject::DeleteWSForward() +{ + WSPoint point = GetCharAfter(mNode, mOffset); + NS_ENSURE_TRUE(point.mTextNode, NS_OK); // nothing to delete + + // Easy case, preformatted ws. + if (mPRE && (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp)) { + return DeleteChars(point.mTextNode, point.mOffset, + point.mTextNode, point.mOffset + 1); + } + + // Caller's job to ensure that next char is really ws. If it is normal ws, + // we need to delete the whole run. + if (nsCRT::IsAsciiSpace(point.mChar)) { + RefPtr<Text> startNodeText, endNodeText; + int32_t startOffset, endOffset; + GetAsciiWSBounds(eBoth, point.mTextNode, point.mOffset + 1, + getter_AddRefs(startNodeText), &startOffset, + getter_AddRefs(endNodeText), &endOffset); + + // Adjust surrounding ws + nsCOMPtr<nsINode> startNode(startNodeText), endNode(endNodeText); + nsresult rv = + WSRunObject::PrepareToDeleteRange(mHTMLEditor, + address_of(startNode), &startOffset, + address_of(endNode), &endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, delete that ws + return DeleteChars(startNode, startOffset, endNode, endOffset); + } + + if (point.mChar == nbsp) { + nsCOMPtr<nsINode> node(point.mTextNode); + // Adjust surrounding ws + int32_t startOffset = point.mOffset; + int32_t endOffset = point.mOffset+1; + nsresult rv = + WSRunObject::PrepareToDeleteRange(mHTMLEditor, + address_of(node), &startOffset, + address_of(node), &endOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, delete that ws + return DeleteChars(node, startOffset, node, endOffset); + } + + return NS_OK; +} + +void +WSRunObject::PriorVisibleNode(nsINode* aNode, + int32_t aOffset, + nsCOMPtr<nsINode>* outVisNode, + int32_t* outVisOffset, + WSType* outType) +{ + // Find first visible thing before the point. Position + // outVisNode/outVisOffset just _after_ that thing. If we don't find + // anything return start of ws. + MOZ_ASSERT(aNode && outVisNode && outVisOffset && outType); + + WSFragment* run; + FindRun(aNode, aOffset, &run, false); + + // Is there a visible run there or earlier? + for (; run; run = run->mLeft) { + if (run->mType == WSType::normalWS) { + WSPoint point = GetCharBefore(aNode, aOffset); + // When it's a non-empty text node, return it. + if (point.mTextNode && point.mTextNode->Length()) { + *outVisNode = point.mTextNode; + *outVisOffset = point.mOffset + 1; + if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) { + *outType = WSType::normalWS; + } else { + *outType = WSType::text; + } + return; + } + // If no text node, keep looking. We should eventually fall out of loop + } + } + + // If we get here, then nothing in ws data to find. Return start reason. + *outVisNode = mStartReasonNode; + // This really isn't meaningful if mStartReasonNode != mStartNode + *outVisOffset = mStartOffset; + *outType = mStartReason; +} + + +void +WSRunObject::NextVisibleNode(nsINode* aNode, + int32_t aOffset, + nsCOMPtr<nsINode>* outVisNode, + int32_t* outVisOffset, + WSType* outType) +{ + // Find first visible thing after the point. Position + // outVisNode/outVisOffset just _before_ that thing. If we don't find + // anything return end of ws. + MOZ_ASSERT(aNode && outVisNode && outVisOffset && outType); + + WSFragment* run; + FindRun(aNode, aOffset, &run, true); + + // Is there a visible run there or later? + for (; run; run = run->mRight) { + if (run->mType == WSType::normalWS) { + WSPoint point = GetCharAfter(aNode, aOffset); + // When it's a non-empty text node, return it. + if (point.mTextNode && point.mTextNode->Length()) { + *outVisNode = point.mTextNode; + *outVisOffset = point.mOffset; + if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) { + *outType = WSType::normalWS; + } else { + *outType = WSType::text; + } + return; + } + // If no text node, keep looking. We should eventually fall out of loop + } + } + + // If we get here, then nothing in ws data to find. Return end reason + *outVisNode = mEndReasonNode; + // This really isn't meaningful if mEndReasonNode != mEndNode + *outVisOffset = mEndOffset; + *outType = mEndReason; +} + +nsresult +WSRunObject::AdjustWhitespace() +{ + // this routine examines a run of ws and tries to get rid of some unneeded nbsp's, + // replacing them with regualr ascii space if possible. Keeping things simple + // for now and just trying to fix up the trailing ws in the run. + if (!mLastNBSPNode) { + // nothing to do! + return NS_OK; + } + WSFragment *curRun = mStartRun; + while (curRun) { + // look for normal ws run + if (curRun->mType == WSType::normalWS) { + nsresult rv = CheckTrailingNBSPOfRun(curRun); + if (NS_FAILED(rv)) { + return rv; + } + } + curRun = curRun->mRight; + } + return NS_OK; +} + + +//-------------------------------------------------------------------------------------------- +// protected methods +//-------------------------------------------------------------------------------------------- + +nsINode* +WSRunObject::GetWSBoundingParent() +{ + NS_ENSURE_TRUE(mNode, nullptr); + OwningNonNull<nsINode> wsBoundingParent = *mNode; + while (!IsBlockNode(wsBoundingParent)) { + nsCOMPtr<nsINode> parent = wsBoundingParent->GetParentNode(); + if (!parent || !mHTMLEditor->IsEditable(parent)) { + break; + } + wsBoundingParent = parent; + } + return wsBoundingParent; +} + +nsresult +WSRunObject::GetWSNodes() +{ + // collect up an array of nodes that are contiguous with the insertion point + // and which contain only whitespace. Stop if you reach non-ws text or a new + // block boundary. + EditorDOMPoint start(mNode, mOffset), end(mNode, mOffset); + nsCOMPtr<nsINode> wsBoundingParent = GetWSBoundingParent(); + + // first look backwards to find preceding ws nodes + if (RefPtr<Text> textNode = mNode->GetAsText()) { + const nsTextFragment* textFrag = textNode->GetText(); + + mNodeArray.InsertElementAt(0, textNode); + if (mOffset) { + for (int32_t pos = mOffset - 1; pos >= 0; pos--) { + // sanity bounds check the char position. bug 136165 + if (uint32_t(pos) >= textFrag->GetLength()) { + NS_NOTREACHED("looking beyond end of text fragment"); + continue; + } + char16_t theChar = textFrag->CharAt(pos); + if (!nsCRT::IsAsciiSpace(theChar)) { + if (theChar != nbsp) { + mStartNode = textNode; + mStartOffset = pos + 1; + mStartReason = WSType::text; + mStartReasonNode = textNode; + break; + } + // as we look backwards update our earliest found nbsp + mFirstNBSPNode = textNode; + mFirstNBSPOffset = pos; + // also keep track of latest nbsp so far + if (!mLastNBSPNode) { + mLastNBSPNode = textNode; + mLastNBSPOffset = pos; + } + } + start.node = textNode; + start.offset = pos; + } + } + } + + while (!mStartNode) { + // we haven't found the start of ws yet. Keep looking + nsCOMPtr<nsIContent> priorNode = GetPreviousWSNode(start, wsBoundingParent); + if (priorNode) { + if (IsBlockNode(priorNode)) { + mStartNode = start.node; + mStartOffset = start.offset; + mStartReason = WSType::otherBlock; + mStartReasonNode = priorNode; + } else if (RefPtr<Text> textNode = priorNode->GetAsText()) { + mNodeArray.InsertElementAt(0, textNode); + const nsTextFragment *textFrag; + if (!textNode || !(textFrag = textNode->GetText())) { + return NS_ERROR_NULL_POINTER; + } + uint32_t len = textNode->TextLength(); + + if (len < 1) { + // Zero length text node. Set start point to it + // so we can get past it! + start.SetPoint(priorNode, 0); + } else { + for (int32_t pos = len - 1; pos >= 0; pos--) { + // sanity bounds check the char position. bug 136165 + if (uint32_t(pos) >= textFrag->GetLength()) { + NS_NOTREACHED("looking beyond end of text fragment"); + continue; + } + char16_t theChar = textFrag->CharAt(pos); + if (!nsCRT::IsAsciiSpace(theChar)) { + if (theChar != nbsp) { + mStartNode = textNode; + mStartOffset = pos + 1; + mStartReason = WSType::text; + mStartReasonNode = textNode; + break; + } + // as we look backwards update our earliest found nbsp + mFirstNBSPNode = textNode; + mFirstNBSPOffset = pos; + // also keep track of latest nbsp so far + if (!mLastNBSPNode) { + mLastNBSPNode = textNode; + mLastNBSPOffset = pos; + } + } + start.SetPoint(textNode, pos); + } + } + } else { + // it's a break or a special node, like <img>, that is not a block and not + // a break but still serves as a terminator to ws runs. + mStartNode = start.node; + mStartOffset = start.offset; + if (TextEditUtils::IsBreak(priorNode)) { + mStartReason = WSType::br; + } else { + mStartReason = WSType::special; + } + mStartReasonNode = priorNode; + } + } else { + // no prior node means we exhausted wsBoundingParent + mStartNode = start.node; + mStartOffset = start.offset; + mStartReason = WSType::thisBlock; + mStartReasonNode = wsBoundingParent; + } + } + + // then look ahead to find following ws nodes + if (RefPtr<Text> textNode = mNode->GetAsText()) { + // don't need to put it on list. it already is from code above + const nsTextFragment *textFrag = textNode->GetText(); + + uint32_t len = textNode->TextLength(); + if (uint16_t(mOffset)<len) { + for (uint32_t pos = mOffset; pos < len; pos++) { + // sanity bounds check the char position. bug 136165 + if (pos >= textFrag->GetLength()) { + NS_NOTREACHED("looking beyond end of text fragment"); + continue; + } + char16_t theChar = textFrag->CharAt(pos); + if (!nsCRT::IsAsciiSpace(theChar)) { + if (theChar != nbsp) { + mEndNode = textNode; + mEndOffset = pos; + mEndReason = WSType::text; + mEndReasonNode = textNode; + break; + } + // as we look forwards update our latest found nbsp + mLastNBSPNode = textNode; + mLastNBSPOffset = pos; + // also keep track of earliest nbsp so far + if (!mFirstNBSPNode) { + mFirstNBSPNode = textNode; + mFirstNBSPOffset = pos; + } + } + end.SetPoint(textNode, pos + 1); + } + } + } + + while (!mEndNode) { + // we haven't found the end of ws yet. Keep looking + nsCOMPtr<nsIContent> nextNode = GetNextWSNode(end, wsBoundingParent); + if (nextNode) { + if (IsBlockNode(nextNode)) { + // we encountered a new block. therefore no more ws. + mEndNode = end.node; + mEndOffset = end.offset; + mEndReason = WSType::otherBlock; + mEndReasonNode = nextNode; + } else if (RefPtr<Text> textNode = nextNode->GetAsText()) { + mNodeArray.AppendElement(textNode); + const nsTextFragment *textFrag; + if (!textNode || !(textFrag = textNode->GetText())) { + return NS_ERROR_NULL_POINTER; + } + uint32_t len = textNode->TextLength(); + + if (len < 1) { + // Zero length text node. Set end point to it + // so we can get past it! + end.SetPoint(textNode, 0); + } else { + for (uint32_t pos = 0; pos < len; pos++) { + // sanity bounds check the char position. bug 136165 + if (pos >= textFrag->GetLength()) { + NS_NOTREACHED("looking beyond end of text fragment"); + continue; + } + char16_t theChar = textFrag->CharAt(pos); + if (!nsCRT::IsAsciiSpace(theChar)) { + if (theChar != nbsp) { + mEndNode = textNode; + mEndOffset = pos; + mEndReason = WSType::text; + mEndReasonNode = textNode; + break; + } + // as we look forwards update our latest found nbsp + mLastNBSPNode = textNode; + mLastNBSPOffset = pos; + // also keep track of earliest nbsp so far + if (!mFirstNBSPNode) { + mFirstNBSPNode = textNode; + mFirstNBSPOffset = pos; + } + } + end.SetPoint(textNode, pos + 1); + } + } + } else { + // we encountered a break or a special node, like <img>, + // that is not a block and not a break but still + // serves as a terminator to ws runs. + mEndNode = end.node; + mEndOffset = end.offset; + if (TextEditUtils::IsBreak(nextNode)) { + mEndReason = WSType::br; + } else { + mEndReason = WSType::special; + } + mEndReasonNode = nextNode; + } + } else { + // no next node means we exhausted wsBoundingParent + mEndNode = end.node; + mEndOffset = end.offset; + mEndReason = WSType::thisBlock; + mEndReasonNode = wsBoundingParent; + } + } + + return NS_OK; +} + +void +WSRunObject::GetRuns() +{ + ClearRuns(); + + // handle some easy cases first + mHTMLEditor->IsPreformatted(GetAsDOMNode(mNode), &mPRE); + // if it's preformatedd, or if we are surrounded by text or special, it's all one + // big normal ws run + if (mPRE || + ((mStartReason == WSType::text || mStartReason == WSType::special) && + (mEndReason == WSType::text || mEndReason == WSType::special || + mEndReason == WSType::br))) { + MakeSingleWSRun(WSType::normalWS); + return; + } + + // if we are before or after a block (or after a break), and there are no nbsp's, + // then it's all non-rendering ws. + if (!mFirstNBSPNode && !mLastNBSPNode && + ((mStartReason & WSType::block) || mStartReason == WSType::br || + (mEndReason & WSType::block))) { + WSType wstype; + if ((mStartReason & WSType::block) || mStartReason == WSType::br) { + wstype = WSType::leadingWS; + } + if (mEndReason & WSType::block) { + wstype |= WSType::trailingWS; + } + MakeSingleWSRun(wstype); + return; + } + + // otherwise a little trickier. shucks. + mStartRun = new WSFragment(); + mStartRun->mStartNode = mStartNode; + mStartRun->mStartOffset = mStartOffset; + + if (mStartReason & WSType::block || mStartReason == WSType::br) { + // set up mStartRun + mStartRun->mType = WSType::leadingWS; + mStartRun->mEndNode = mFirstNBSPNode; + mStartRun->mEndOffset = mFirstNBSPOffset; + mStartRun->mLeftType = mStartReason; + mStartRun->mRightType = WSType::normalWS; + + // set up next run + WSFragment *normalRun = new WSFragment(); + mStartRun->mRight = normalRun; + normalRun->mType = WSType::normalWS; + normalRun->mStartNode = mFirstNBSPNode; + normalRun->mStartOffset = mFirstNBSPOffset; + normalRun->mLeftType = WSType::leadingWS; + normalRun->mLeft = mStartRun; + if (mEndReason != WSType::block) { + // then no trailing ws. this normal run ends the overall ws run. + normalRun->mRightType = mEndReason; + normalRun->mEndNode = mEndNode; + normalRun->mEndOffset = mEndOffset; + mEndRun = normalRun; + } else { + // we might have trailing ws. + // it so happens that *if* there is an nbsp at end, {mEndNode,mEndOffset-1} + // will point to it, even though in general start/end points not + // guaranteed to be in text nodes. + if (mLastNBSPNode == mEndNode && mLastNBSPOffset == mEndOffset - 1) { + // normal ws runs right up to adjacent block (nbsp next to block) + normalRun->mRightType = mEndReason; + normalRun->mEndNode = mEndNode; + normalRun->mEndOffset = mEndOffset; + mEndRun = normalRun; + } else { + normalRun->mEndNode = mLastNBSPNode; + normalRun->mEndOffset = mLastNBSPOffset+1; + normalRun->mRightType = WSType::trailingWS; + + // set up next run + WSFragment *lastRun = new WSFragment(); + lastRun->mType = WSType::trailingWS; + lastRun->mStartNode = mLastNBSPNode; + lastRun->mStartOffset = mLastNBSPOffset+1; + lastRun->mEndNode = mEndNode; + lastRun->mEndOffset = mEndOffset; + lastRun->mLeftType = WSType::normalWS; + lastRun->mLeft = normalRun; + lastRun->mRightType = mEndReason; + mEndRun = lastRun; + normalRun->mRight = lastRun; + } + } + } else { + // mStartReason is not WSType::block or WSType::br; set up mStartRun + mStartRun->mType = WSType::normalWS; + mStartRun->mEndNode = mLastNBSPNode; + mStartRun->mEndOffset = mLastNBSPOffset+1; + mStartRun->mLeftType = mStartReason; + + // we might have trailing ws. + // it so happens that *if* there is an nbsp at end, {mEndNode,mEndOffset-1} + // will point to it, even though in general start/end points not + // guaranteed to be in text nodes. + if (mLastNBSPNode == mEndNode && mLastNBSPOffset == (mEndOffset - 1)) { + mStartRun->mRightType = mEndReason; + mStartRun->mEndNode = mEndNode; + mStartRun->mEndOffset = mEndOffset; + mEndRun = mStartRun; + } else { + // set up next run + WSFragment *lastRun = new WSFragment(); + lastRun->mType = WSType::trailingWS; + lastRun->mStartNode = mLastNBSPNode; + lastRun->mStartOffset = mLastNBSPOffset+1; + lastRun->mLeftType = WSType::normalWS; + lastRun->mLeft = mStartRun; + lastRun->mRightType = mEndReason; + mEndRun = lastRun; + mStartRun->mRight = lastRun; + mStartRun->mRightType = WSType::trailingWS; + } + } +} + +void +WSRunObject::ClearRuns() +{ + WSFragment *tmp, *run; + run = mStartRun; + while (run) { + tmp = run->mRight; + delete run; + run = tmp; + } + mStartRun = 0; + mEndRun = 0; +} + +void +WSRunObject::MakeSingleWSRun(WSType aType) +{ + mStartRun = new WSFragment(); + + mStartRun->mStartNode = mStartNode; + mStartRun->mStartOffset = mStartOffset; + mStartRun->mType = aType; + mStartRun->mEndNode = mEndNode; + mStartRun->mEndOffset = mEndOffset; + mStartRun->mLeftType = mStartReason; + mStartRun->mRightType = mEndReason; + + mEndRun = mStartRun; +} + +nsIContent* +WSRunObject::GetPreviousWSNodeInner(nsINode* aStartNode, + nsINode* aBlockParent) +{ + // Can't really recycle various getnext/prior routines because we have + // special needs here. Need to step into inline containers but not block + // containers. + MOZ_ASSERT(aStartNode && aBlockParent); + + nsCOMPtr<nsIContent> priorNode = aStartNode->GetPreviousSibling(); + OwningNonNull<nsINode> curNode = *aStartNode; + while (!priorNode) { + // We have exhausted nodes in parent of aStartNode. + nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); + NS_ENSURE_TRUE(curParent, nullptr); + if (curParent == aBlockParent) { + // We have exhausted nodes in the block parent. The convention here is + // to return null. + return nullptr; + } + // We have a parent: look for previous sibling + priorNode = curParent->GetPreviousSibling(); + curNode = curParent; + } + // We have a prior node. If it's a block, return it. + if (IsBlockNode(priorNode)) { + return priorNode; + } + if (mHTMLEditor->IsContainer(priorNode)) { + // Else if it's a container, get deep rightmost child + nsCOMPtr<nsIContent> child = mHTMLEditor->GetRightmostChild(priorNode); + if (child) { + return child; + } + } + // Else return the node itself + return priorNode; +} + +nsIContent* +WSRunObject::GetPreviousWSNode(EditorDOMPoint aPoint, + nsINode* aBlockParent) +{ + // Can't really recycle various getnext/prior routines because we + // have special needs here. Need to step into inline containers but + // not block containers. + MOZ_ASSERT(aPoint.node && aBlockParent); + + if (aPoint.node->NodeType() == nsIDOMNode::TEXT_NODE) { + return GetPreviousWSNodeInner(aPoint.node, aBlockParent); + } + if (!mHTMLEditor->IsContainer(aPoint.node)) { + return GetPreviousWSNodeInner(aPoint.node, aBlockParent); + } + + if (!aPoint.offset) { + if (aPoint.node == aBlockParent) { + // We are at start of the block. + return nullptr; + } + + // We are at start of non-block container + return GetPreviousWSNodeInner(aPoint.node, aBlockParent); + } + + nsCOMPtr<nsIContent> startContent = do_QueryInterface(aPoint.node); + NS_ENSURE_TRUE(startContent, nullptr); + nsCOMPtr<nsIContent> priorNode = startContent->GetChildAt(aPoint.offset - 1); + NS_ENSURE_TRUE(priorNode, nullptr); + // We have a prior node. If it's a block, return it. + if (IsBlockNode(priorNode)) { + return priorNode; + } + if (mHTMLEditor->IsContainer(priorNode)) { + // Else if it's a container, get deep rightmost child + nsCOMPtr<nsIContent> child = mHTMLEditor->GetRightmostChild(priorNode); + if (child) { + return child; + } + } + // Else return the node itself + return priorNode; +} + +nsIContent* +WSRunObject::GetNextWSNodeInner(nsINode* aStartNode, + nsINode* aBlockParent) +{ + // Can't really recycle various getnext/prior routines because we have + // special needs here. Need to step into inline containers but not block + // containers. + MOZ_ASSERT(aStartNode && aBlockParent); + + nsCOMPtr<nsIContent> nextNode = aStartNode->GetNextSibling(); + nsCOMPtr<nsINode> curNode = aStartNode; + while (!nextNode) { + // We have exhausted nodes in parent of aStartNode. + nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); + NS_ENSURE_TRUE(curParent, nullptr); + if (curParent == aBlockParent) { + // We have exhausted nodes in the block parent. The convention here is + // to return null. + return nullptr; + } + // We have a parent: look for next sibling + nextNode = curParent->GetNextSibling(); + curNode = curParent; + } + // We have a next node. If it's a block, return it. + if (IsBlockNode(nextNode)) { + return nextNode; + } + if (mHTMLEditor->IsContainer(nextNode)) { + // Else if it's a container, get deep leftmost child + nsCOMPtr<nsIContent> child = mHTMLEditor->GetLeftmostChild(nextNode); + if (child) { + return child; + } + } + // Else return the node itself + return nextNode; +} + +nsIContent* +WSRunObject::GetNextWSNode(EditorDOMPoint aPoint, + nsINode* aBlockParent) +{ + // Can't really recycle various getnext/prior routines because we have + // special needs here. Need to step into inline containers but not block + // containers. + MOZ_ASSERT(aPoint.node && aBlockParent); + + if (aPoint.node->NodeType() == nsIDOMNode::TEXT_NODE) { + return GetNextWSNodeInner(aPoint.node, aBlockParent); + } + if (!mHTMLEditor->IsContainer(aPoint.node)) { + return GetNextWSNodeInner(aPoint.node, aBlockParent); + } + + nsCOMPtr<nsIContent> startContent = do_QueryInterface(aPoint.node); + NS_ENSURE_TRUE(startContent, nullptr); + + nsCOMPtr<nsIContent> nextNode = startContent->GetChildAt(aPoint.offset); + if (!nextNode) { + if (aPoint.node == aBlockParent) { + // We are at end of the block. + return nullptr; + } + + // We are at end of non-block container + return GetNextWSNodeInner(aPoint.node, aBlockParent); + } + + // We have a next node. If it's a block, return it. + if (IsBlockNode(nextNode)) { + return nextNode; + } + if (mHTMLEditor->IsContainer(nextNode)) { + // else if it's a container, get deep leftmost child + nsCOMPtr<nsIContent> child = mHTMLEditor->GetLeftmostChild(nextNode); + if (child) { + return child; + } + } + // Else return the node itself + return nextNode; +} + +nsresult +WSRunObject::PrepareToDeleteRangePriv(WSRunObject* aEndObject) +{ + // this routine adjust whitespace before *this* and after aEndObject + // in preperation for the two areas to become adjacent after the + // intervening content is deleted. It's overly agressive right + // now. There might be a block boundary remaining between them after + // the deletion, in which case these adjstments are unneeded (though + // I don't think they can ever be harmful?) + + NS_ENSURE_TRUE(aEndObject, NS_ERROR_NULL_POINTER); + + // get the runs before and after selection + WSFragment *beforeRun, *afterRun; + FindRun(mNode, mOffset, &beforeRun, false); + aEndObject->FindRun(aEndObject->mNode, aEndObject->mOffset, &afterRun, true); + + // trim after run of any leading ws + if (afterRun && (afterRun->mType & WSType::leadingWS)) { + nsresult rv = + aEndObject->DeleteChars(aEndObject->mNode, aEndObject->mOffset, + afterRun->mEndNode, afterRun->mEndOffset, + eOutsideUserSelectAll); + NS_ENSURE_SUCCESS(rv, rv); + } + // adjust normal ws in afterRun if needed + if (afterRun && afterRun->mType == WSType::normalWS && !aEndObject->mPRE) { + if ((beforeRun && (beforeRun->mType & WSType::leadingWS)) || + (!beforeRun && ((mStartReason & WSType::block) || + mStartReason == WSType::br))) { + // make sure leading char of following ws is an nbsp, so that it will show up + WSPoint point = aEndObject->GetCharAfter(aEndObject->mNode, + aEndObject->mOffset); + if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) { + nsresult rv = aEndObject->ConvertToNBSP(point, eOutsideUserSelectAll); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + // trim before run of any trailing ws + if (beforeRun && (beforeRun->mType & WSType::trailingWS)) { + nsresult rv = DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset, + mNode, mOffset, eOutsideUserSelectAll); + NS_ENSURE_SUCCESS(rv, rv); + } else if (beforeRun && beforeRun->mType == WSType::normalWS && !mPRE) { + if ((afterRun && (afterRun->mType & WSType::trailingWS)) || + (afterRun && afterRun->mType == WSType::normalWS) || + (!afterRun && (aEndObject->mEndReason & WSType::block))) { + // make sure trailing char of starting ws is an nbsp, so that it will show up + WSPoint point = GetCharBefore(mNode, mOffset); + if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) { + RefPtr<Text> wsStartNode, wsEndNode; + int32_t wsStartOffset, wsEndOffset; + GetAsciiWSBounds(eBoth, mNode, mOffset, + getter_AddRefs(wsStartNode), &wsStartOffset, + getter_AddRefs(wsEndNode), &wsEndOffset); + point.mTextNode = wsStartNode; + point.mOffset = wsStartOffset; + nsresult rv = ConvertToNBSP(point, eOutsideUserSelectAll); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + return NS_OK; +} + +nsresult +WSRunObject::PrepareToSplitAcrossBlocksPriv() +{ + // used to prepare ws to be split across two blocks. The main issue + // here is make sure normalWS doesn't end up becoming non-significant + // leading or trailing ws after the split. + + // get the runs before and after selection + WSFragment *beforeRun, *afterRun; + FindRun(mNode, mOffset, &beforeRun, false); + FindRun(mNode, mOffset, &afterRun, true); + + // adjust normal ws in afterRun if needed + if (afterRun && afterRun->mType == WSType::normalWS) { + // make sure leading char of following ws is an nbsp, so that it will show up + WSPoint point = GetCharAfter(mNode, mOffset); + if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) { + nsresult rv = ConvertToNBSP(point); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // adjust normal ws in beforeRun if needed + if (beforeRun && beforeRun->mType == WSType::normalWS) { + // make sure trailing char of starting ws is an nbsp, so that it will show up + WSPoint point = GetCharBefore(mNode, mOffset); + if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) { + RefPtr<Text> wsStartNode, wsEndNode; + int32_t wsStartOffset, wsEndOffset; + GetAsciiWSBounds(eBoth, mNode, mOffset, + getter_AddRefs(wsStartNode), &wsStartOffset, + getter_AddRefs(wsEndNode), &wsEndOffset); + point.mTextNode = wsStartNode; + point.mOffset = wsStartOffset; + nsresult rv = ConvertToNBSP(point); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +nsresult +WSRunObject::DeleteChars(nsINode* aStartNode, + int32_t aStartOffset, + nsINode* aEndNode, + int32_t aEndOffset, + AreaRestriction aAR) +{ + // MOOSE: this routine needs to be modified to preserve the integrity of the + // wsFragment info. + NS_ENSURE_TRUE(aStartNode && aEndNode, NS_ERROR_NULL_POINTER); + + if (aAR == eOutsideUserSelectAll) { + nsCOMPtr<nsIDOMNode> san = + mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aStartNode)); + if (san) { + return NS_OK; + } + + if (aStartNode != aEndNode) { + san = mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aEndNode)); + if (san) { + return NS_OK; + } + } + } + + if (aStartNode == aEndNode && aStartOffset == aEndOffset) { + // Nothing to delete + return NS_OK; + } + + int32_t idx = mNodeArray.IndexOf(aStartNode); + if (idx == -1) { + // If our strarting point wasn't one of our ws text nodes, then just go + // through them from the beginning. + idx = 0; + } + + if (aStartNode == aEndNode && aStartNode->GetAsText()) { + return mHTMLEditor->DeleteText(*aStartNode->GetAsText(), + static_cast<uint32_t>(aStartOffset), + static_cast<uint32_t>(aEndOffset - aStartOffset)); + } + + RefPtr<nsRange> range; + int32_t count = mNodeArray.Length(); + for (; idx < count; idx++) { + RefPtr<Text> node = mNodeArray[idx]; + if (!node) { + // We ran out of ws nodes; must have been deleting to end + return NS_OK; + } + if (node == aStartNode) { + uint32_t len = node->Length(); + if (uint32_t(aStartOffset) < len) { + nsresult rv = + mHTMLEditor->DeleteText(*node, AssertedCast<uint32_t>(aStartOffset), + len - aStartOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + } else if (node == aEndNode) { + if (aEndOffset) { + nsresult rv = + mHTMLEditor->DeleteText(*node, 0, AssertedCast<uint32_t>(aEndOffset)); + NS_ENSURE_SUCCESS(rv, rv); + } + break; + } else { + if (!range) { + range = new nsRange(aStartNode); + nsresult rv = + range->Set(aStartNode, aStartOffset, aEndNode, aEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + bool nodeBefore, nodeAfter; + nsresult rv = + nsRange::CompareNodeToRange(node, range, &nodeBefore, &nodeAfter); + NS_ENSURE_SUCCESS(rv, rv); + if (nodeAfter) { + break; + } + if (!nodeBefore) { + rv = mHTMLEditor->DeleteNode(node); + NS_ENSURE_SUCCESS(rv, rv); + mNodeArray.RemoveElement(node); + --count; + --idx; + } + } + } + return NS_OK; +} + +WSRunObject::WSPoint +WSRunObject::GetCharAfter(nsINode* aNode, + int32_t aOffset) +{ + MOZ_ASSERT(aNode); + + int32_t idx = mNodeArray.IndexOf(aNode); + if (idx == -1) { + // Use range comparisons to get right ws node + return GetWSPointAfter(aNode, aOffset); + } + // Use WSPoint version of GetCharAfter() + return GetCharAfter(WSPoint(mNodeArray[idx], aOffset, 0)); +} + +WSRunObject::WSPoint +WSRunObject::GetCharBefore(nsINode* aNode, + int32_t aOffset) +{ + MOZ_ASSERT(aNode); + + int32_t idx = mNodeArray.IndexOf(aNode); + if (idx == -1) { + // Use range comparisons to get right ws node + return GetWSPointBefore(aNode, aOffset); + } + // Use WSPoint version of GetCharBefore() + return GetCharBefore(WSPoint(mNodeArray[idx], aOffset, 0)); +} + +WSRunObject::WSPoint +WSRunObject::GetCharAfter(const WSPoint &aPoint) +{ + MOZ_ASSERT(aPoint.mTextNode); + + WSPoint outPoint; + outPoint.mTextNode = nullptr; + outPoint.mOffset = 0; + outPoint.mChar = 0; + + int32_t idx = mNodeArray.IndexOf(aPoint.mTextNode); + if (idx == -1) { + // Can't find point, but it's not an error + return outPoint; + } + + if (static_cast<uint16_t>(aPoint.mOffset) < aPoint.mTextNode->TextLength()) { + outPoint = aPoint; + outPoint.mChar = GetCharAt(aPoint.mTextNode, aPoint.mOffset); + return outPoint; + } + + int32_t numNodes = mNodeArray.Length(); + if (idx + 1 < numNodes) { + outPoint.mTextNode = mNodeArray[idx + 1]; + MOZ_ASSERT(outPoint.mTextNode); + outPoint.mOffset = 0; + outPoint.mChar = GetCharAt(outPoint.mTextNode, 0); + } + + return outPoint; +} + +WSRunObject::WSPoint +WSRunObject::GetCharBefore(const WSPoint &aPoint) +{ + MOZ_ASSERT(aPoint.mTextNode); + + WSPoint outPoint; + outPoint.mTextNode = nullptr; + outPoint.mOffset = 0; + outPoint.mChar = 0; + + int32_t idx = mNodeArray.IndexOf(aPoint.mTextNode); + if (idx == -1) { + // Can't find point, but it's not an error + return outPoint; + } + + if (aPoint.mOffset) { + outPoint = aPoint; + outPoint.mOffset--; + outPoint.mChar = GetCharAt(aPoint.mTextNode, aPoint.mOffset - 1); + return outPoint; + } + + if (idx) { + outPoint.mTextNode = mNodeArray[idx - 1]; + + uint32_t len = outPoint.mTextNode->TextLength(); + if (len) { + outPoint.mOffset = len - 1; + outPoint.mChar = GetCharAt(outPoint.mTextNode, len - 1); + } + } + return outPoint; +} + +nsresult +WSRunObject::ConvertToNBSP(WSPoint aPoint, AreaRestriction aAR) +{ + // MOOSE: this routine needs to be modified to preserve the integrity of the + // wsFragment info. + NS_ENSURE_TRUE(aPoint.mTextNode, NS_ERROR_NULL_POINTER); + + if (aAR == eOutsideUserSelectAll) { + nsCOMPtr<nsIDOMNode> san = + mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aPoint.mTextNode)); + if (san) { + return NS_OK; + } + } + + // First, insert an nbsp + AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); + nsAutoString nbspStr(nbsp); + nsresult rv = + mHTMLEditor->InsertTextIntoTextNodeImpl(nbspStr, *aPoint.mTextNode, + aPoint.mOffset, true); + NS_ENSURE_SUCCESS(rv, rv); + + // Next, find range of ws it will replace + RefPtr<Text> startNode, endNode; + int32_t startOffset = 0, endOffset = 0; + + GetAsciiWSBounds(eAfter, aPoint.mTextNode, aPoint.mOffset + 1, + getter_AddRefs(startNode), &startOffset, + getter_AddRefs(endNode), &endOffset); + + // Finally, delete that replaced ws, if any + if (startNode) { + rv = DeleteChars(startNode, startOffset, endNode, endOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +void +WSRunObject::GetAsciiWSBounds(int16_t aDir, + nsINode* aNode, + int32_t aOffset, + Text** outStartNode, + int32_t* outStartOffset, + Text** outEndNode, + int32_t* outEndOffset) +{ + MOZ_ASSERT(aNode && outStartNode && outStartOffset && outEndNode && + outEndOffset); + + RefPtr<Text> startNode, endNode; + int32_t startOffset = 0, endOffset = 0; + + if (aDir & eAfter) { + WSPoint point = GetCharAfter(aNode, aOffset); + if (point.mTextNode) { + // We found a text node, at least + startNode = endNode = point.mTextNode; + startOffset = endOffset = point.mOffset; + + // Scan ahead to end of ASCII ws + for (; nsCRT::IsAsciiSpace(point.mChar) && point.mTextNode; + point = GetCharAfter(point)) { + endNode = point.mTextNode; + // endOffset is _after_ ws + point.mOffset++; + endOffset = point.mOffset; + } + } + } + + if (aDir & eBefore) { + WSPoint point = GetCharBefore(aNode, aOffset); + if (point.mTextNode) { + // We found a text node, at least + startNode = point.mTextNode; + startOffset = point.mOffset + 1; + if (!endNode) { + endNode = startNode; + endOffset = startOffset; + } + + // Scan back to start of ASCII ws + for (; nsCRT::IsAsciiSpace(point.mChar) && point.mTextNode; + point = GetCharBefore(point)) { + startNode = point.mTextNode; + startOffset = point.mOffset; + } + } + } + + startNode.forget(outStartNode); + *outStartOffset = startOffset; + endNode.forget(outEndNode); + *outEndOffset = endOffset; +} + +/** + * Given a dompoint, find the ws run that is before or after it, as caller + * needs + */ +void +WSRunObject::FindRun(nsINode* aNode, + int32_t aOffset, + WSFragment** outRun, + bool after) +{ + MOZ_ASSERT(aNode && outRun); + *outRun = nullptr; + + for (WSFragment* run = mStartRun; run; run = run->mRight) { + int32_t comp = run->mStartNode ? nsContentUtils::ComparePoints(aNode, + aOffset, run->mStartNode, run->mStartOffset) : -1; + if (comp <= 0) { + if (after) { + *outRun = run; + } else { + // before + *outRun = nullptr; + } + return; + } + comp = run->mEndNode ? nsContentUtils::ComparePoints(aNode, aOffset, + run->mEndNode, run->mEndOffset) : -1; + if (comp < 0) { + *outRun = run; + return; + } else if (!comp) { + if (after) { + *outRun = run->mRight; + } else { + // before + *outRun = run; + } + return; + } + if (!run->mRight) { + if (after) { + *outRun = nullptr; + } else { + // before + *outRun = run; + } + return; + } + } +} + +char16_t +WSRunObject::GetCharAt(Text* aTextNode, + int32_t aOffset) +{ + // return 0 if we can't get a char, for whatever reason + NS_ENSURE_TRUE(aTextNode, 0); + + int32_t len = int32_t(aTextNode->TextLength()); + if (aOffset < 0 || aOffset >= len) { + return 0; + } + return aTextNode->GetText()->CharAt(aOffset); +} + +WSRunObject::WSPoint +WSRunObject::GetWSPointAfter(nsINode* aNode, + int32_t aOffset) +{ + // Note: only to be called if aNode is not a ws node. + + // Binary search on wsnodes + uint32_t numNodes = mNodeArray.Length(); + + if (!numNodes) { + // Do nothing if there are no nodes to search + WSPoint outPoint; + return outPoint; + } + + uint32_t firstNum = 0, curNum = numNodes/2, lastNum = numNodes; + int16_t cmp = 0; + RefPtr<Text> curNode; + + // Begin binary search. We do this because we need to minimize calls to + // ComparePoints(), which is expensive. + while (curNum != lastNum) { + curNode = mNodeArray[curNum]; + cmp = nsContentUtils::ComparePoints(aNode, aOffset, curNode, 0); + if (cmp < 0) { + lastNum = curNum; + } else { + firstNum = curNum + 1; + } + curNum = (lastNum - firstNum)/2 + firstNum; + MOZ_ASSERT(firstNum <= curNum && curNum <= lastNum, "Bad binary search"); + } + + // When the binary search is complete, we always know that the current node + // is the same as the end node, which is always past our range. Therefore, + // we've found the node immediately after the point of interest. + if (curNum == mNodeArray.Length()) { + // hey asked for past our range (it's after the last node). GetCharAfter + // will do the work for us when we pass it the last index of the last node. + RefPtr<Text> textNode(mNodeArray[curNum - 1]); + WSPoint point(textNode, textNode->TextLength(), 0); + return GetCharAfter(point); + } else { + // The char after the point is the first character of our range. + RefPtr<Text> textNode(mNodeArray[curNum]); + WSPoint point(textNode, 0, 0); + return GetCharAfter(point); + } +} + +WSRunObject::WSPoint +WSRunObject::GetWSPointBefore(nsINode* aNode, + int32_t aOffset) +{ + // Note: only to be called if aNode is not a ws node. + + // Binary search on wsnodes + uint32_t numNodes = mNodeArray.Length(); + + if (!numNodes) { + // Do nothing if there are no nodes to search + WSPoint outPoint; + return outPoint; + } + + uint32_t firstNum = 0, curNum = numNodes/2, lastNum = numNodes; + int16_t cmp = 0; + RefPtr<Text> curNode; + + // Begin binary search. We do this because we need to minimize calls to + // ComparePoints(), which is expensive. + while (curNum != lastNum) { + curNode = mNodeArray[curNum]; + cmp = nsContentUtils::ComparePoints(aNode, aOffset, curNode, 0); + if (cmp < 0) { + lastNum = curNum; + } else { + firstNum = curNum + 1; + } + curNum = (lastNum - firstNum)/2 + firstNum; + MOZ_ASSERT(firstNum <= curNum && curNum <= lastNum, "Bad binary search"); + } + + // When the binary search is complete, we always know that the current node + // is the same as the end node, which is always past our range. Therefore, + // we've found the node immediately after the point of interest. + if (curNum == mNodeArray.Length()) { + // Get the point before the end of the last node, we can pass the length of + // the node into GetCharBefore, and it will return the last character. + RefPtr<Text> textNode(mNodeArray[curNum - 1]); + WSPoint point(textNode, textNode->TextLength(), 0); + return GetCharBefore(point); + } else { + // We can just ask the current node for the point immediately before it, + // it will handle moving to the previous node (if any) and returning the + // appropriate character + RefPtr<Text> textNode(mNodeArray[curNum]); + WSPoint point(textNode, 0, 0); + return GetCharBefore(point); + } +} + +nsresult +WSRunObject::CheckTrailingNBSPOfRun(WSFragment *aRun) +{ + // Try to change an nbsp to a space, if possible, just to prevent nbsp + // proliferation. Examine what is before and after the trailing nbsp, if + // any. + NS_ENSURE_TRUE(aRun, NS_ERROR_NULL_POINTER); + bool leftCheck = false; + bool spaceNBSP = false; + bool rightCheck = false; + + // confirm run is normalWS + if (aRun->mType != WSType::normalWS) { + return NS_ERROR_FAILURE; + } + + // first check for trailing nbsp + WSPoint thePoint = GetCharBefore(aRun->mEndNode, aRun->mEndOffset); + if (thePoint.mTextNode && thePoint.mChar == nbsp) { + // now check that what is to the left of it is compatible with replacing nbsp with space + WSPoint prevPoint = GetCharBefore(thePoint); + if (prevPoint.mTextNode) { + if (!nsCRT::IsAsciiSpace(prevPoint.mChar)) { + leftCheck = true; + } else { + spaceNBSP = true; + } + } else if (aRun->mLeftType == WSType::text || + aRun->mLeftType == WSType::special) { + leftCheck = true; + } + if (leftCheck || spaceNBSP) { + // now check that what is to the right of it is compatible with replacing + // nbsp with space + if (aRun->mRightType == WSType::text || + aRun->mRightType == WSType::special || + aRun->mRightType == WSType::br) { + rightCheck = true; + } + if ((aRun->mRightType & WSType::block) && + IsBlockNode(GetWSBoundingParent())) { + // We are at a block boundary. Insert a <br>. Why? Well, first note + // that the br will have no visible effect since it is up against a + // block boundary. |foo<br><p>bar| renders like |foo<p>bar| and + // similarly |<p>foo<br></p>bar| renders like |<p>foo</p>bar|. What + // this <br> addition gets us is the ability to convert a trailing nbsp + // to a space. Consider: |<body>foo. '</body>|, where ' represents + // selection. User types space attempting to put 2 spaces after the + // end of their sentence. We used to do this as: |<body>foo. + //  </body>| This caused problems with soft wrapping: the nbsp + // would wrap to the next line, which looked attrocious. If you try to + // do: |<body>foo.  </body>| instead, the trailing space is + // invisible because it is against a block boundary. If you do: + // |<body>foo.  </body>| then you get an even uglier soft + // wrapping problem, where foo is on one line until you type the final + // space, and then "foo " jumps down to the next line. Ugh. The best + // way I can find out of this is to throw in a harmless <br> here, + // which allows us to do: |<body>foo.  <br></body>|, which doesn't + // cause foo to jump lines, doesn't cause spaces to show up at the + // beginning of soft wrapped lines, and lets the user see 2 spaces when + // they type 2 spaces. + + nsCOMPtr<Element> brNode = + mHTMLEditor->CreateBR(aRun->mEndNode, aRun->mEndOffset); + NS_ENSURE_TRUE(brNode, NS_ERROR_FAILURE); + + // Refresh thePoint, prevPoint + thePoint = GetCharBefore(aRun->mEndNode, aRun->mEndOffset); + prevPoint = GetCharBefore(thePoint); + rightCheck = true; + } + } + if (leftCheck && rightCheck) { + // Now replace nbsp with space. First, insert a space + AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); + nsAutoString spaceStr(char16_t(32)); + nsresult rv = + mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode, + thePoint.mOffset, true); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, delete that nbsp + rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1, + thePoint.mTextNode, thePoint.mOffset + 2); + NS_ENSURE_SUCCESS(rv, rv); + } else if (!mPRE && spaceNBSP && rightCheck) { + // Don't mess with this preformatted for now. We have a run of ASCII + // whitespace (which will render as one space) followed by an nbsp (which + // is at the end of the whitespace run). Let's switch their order. This + // will ensure that if someone types two spaces after a sentence, and the + // editor softwraps at this point, the spaces won't be split across lines, + // which looks ugly and is bad for the moose. + + RefPtr<Text> startNode, endNode; + int32_t startOffset, endOffset; + GetAsciiWSBounds(eBoth, prevPoint.mTextNode, prevPoint.mOffset + 1, + getter_AddRefs(startNode), &startOffset, + getter_AddRefs(endNode), &endOffset); + + // Delete that nbsp + nsresult rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset, + thePoint.mTextNode, thePoint.mOffset + 1); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, insert that nbsp before the ASCII ws run + AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); + nsAutoString nbspStr(nbsp); + rv = mHTMLEditor->InsertTextIntoTextNodeImpl(nbspStr, *startNode, + startOffset, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +nsresult +WSRunObject::CheckTrailingNBSP(WSFragment* aRun, + nsINode* aNode, + int32_t aOffset) +{ + // Try to change an nbsp to a space, if possible, just to prevent nbsp + // proliferation. This routine is called when we are about to make this + // point in the ws abut an inserted break or text, so we don't have to worry + // about what is after it. What is after it now will end up after the + // inserted object. + NS_ENSURE_TRUE(aRun && aNode, NS_ERROR_NULL_POINTER); + bool canConvert = false; + WSPoint thePoint = GetCharBefore(aNode, aOffset); + if (thePoint.mTextNode && thePoint.mChar == nbsp) { + WSPoint prevPoint = GetCharBefore(thePoint); + if (prevPoint.mTextNode) { + if (!nsCRT::IsAsciiSpace(prevPoint.mChar)) { + canConvert = true; + } + } else if (aRun->mLeftType == WSType::text || + aRun->mLeftType == WSType::special) { + canConvert = true; + } + } + if (canConvert) { + // First, insert a space + AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); + nsAutoString spaceStr(char16_t(32)); + nsresult rv = + mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode, + thePoint.mOffset, true); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, delete that nbsp + rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1, + thePoint.mTextNode, thePoint.mOffset + 2); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult +WSRunObject::CheckLeadingNBSP(WSFragment* aRun, + nsINode* aNode, + int32_t aOffset) +{ + // Try to change an nbsp to a space, if possible, just to prevent nbsp + // proliferation This routine is called when we are about to make this point + // in the ws abut an inserted text, so we don't have to worry about what is + // before it. What is before it now will end up before the inserted text. + bool canConvert = false; + WSPoint thePoint = GetCharAfter(aNode, aOffset); + if (thePoint.mChar == nbsp) { + WSPoint tmp = thePoint; + // we want to be after thePoint + tmp.mOffset++; + WSPoint nextPoint = GetCharAfter(tmp); + if (nextPoint.mTextNode) { + if (!nsCRT::IsAsciiSpace(nextPoint.mChar)) { + canConvert = true; + } + } else if (aRun->mRightType == WSType::text || + aRun->mRightType == WSType::special || + aRun->mRightType == WSType::br) { + canConvert = true; + } + } + if (canConvert) { + // First, insert a space + AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); + nsAutoString spaceStr(char16_t(32)); + nsresult rv = + mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode, + thePoint.mOffset, true); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, delete that nbsp + rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1, + thePoint.mTextNode, thePoint.mOffset + 2); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + + +nsresult +WSRunObject::Scrub() +{ + WSFragment *run = mStartRun; + while (run) { + if (run->mType & (WSType::leadingWS | WSType::trailingWS)) { + nsresult rv = DeleteChars(run->mStartNode, run->mStartOffset, + run->mEndNode, run->mEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + run = run->mRight; + } + return NS_OK; +} + +bool +WSRunObject::IsBlockNode(nsINode* aNode) +{ + return aNode && aNode->IsElement() && + HTMLEditor::NodeIsBlockStatic(aNode->AsElement()); +} + +} // namespace mozilla diff --git a/editor/libeditor/WSRunObject.h b/editor/libeditor/WSRunObject.h new file mode 100644 index 000000000..215e3eb2f --- /dev/null +++ b/editor/libeditor/WSRunObject.h @@ -0,0 +1,411 @@ +/* -*- 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/. */ + +#ifndef WSRunObject_h +#define WSRunObject_h + +#include "nsCOMPtr.h" +#include "nsIEditor.h" // for EDirection +#include "nsINode.h" +#include "nscore.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/Text.h" + +class nsIDOMNode; + +namespace mozilla { + +class HTMLEditor; +class HTMLEditRules; +struct EditorDOMPoint; + +// class WSRunObject represents the entire whitespace situation +// around a given point. It collects up a list of nodes that contain +// whitespace and categorizes in up to 3 different WSFragments (detailed +// below). Each WSFragment is a collection of whitespace that is +// either all insignificant, or that is significant. A WSFragment could +// consist of insignificant whitespace because it is after a block +// boundary or after a break. Or it could be insignificant because it +// is before a block. Or it could be significant because it is +// surrounded by text, or starts and ends with nbsps, etc. + +// Throughout I refer to LeadingWS, NormalWS, TrailingWS. LeadingWS & TrailingWS +// are runs of ascii ws that are insignificant (do not render) because they +// are adjacent to block boundaries, or after a break. NormalWS is ws that +// does cause soem rendering. Note that not all the ws in a NormalWS run need +// render. For example, two ascii spaces surrounded by text on both sides +// will only render as one space (in non-preformatted stlye html), yet both +// spaces count as NormalWS. Together, they render as the one visible space. + +/** + * A type-safe bitfield indicating various types of whitespace or other things. + * Used as a member variable in WSRunObject and WSFragment. + * + * XXX: If this idea is useful in other places, we should generalize it using a + * template. + */ +class WSType +{ +public: + enum Enum + { + none = 0, + leadingWS = 1, // leading insignificant ws, ie, after block or br + trailingWS = 1 << 1, // trailing insignificant ws, ie, before block + normalWS = 1 << 2, // normal significant ws, ie, after text, image, ... + text = 1 << 3, // indicates regular (non-ws) text + special = 1 << 4, // indicates an inline non-container, like image + br = 1 << 5, // indicates a br node + otherBlock = 1 << 6, // indicates a block other than one ws run is in + thisBlock = 1 << 7, // indicates the block ws run is in + block = otherBlock | thisBlock // block found + }; + + /** + * Implicit constructor, because the enums are logically just WSTypes + * themselves, and are only a separate type because there's no other obvious + * way to name specific WSType values. + */ + MOZ_IMPLICIT WSType(const Enum& aEnum = none) + : mEnum(aEnum) + {} + + // operator==, &, and | need to access mEnum + friend bool operator==(const WSType& aLeft, const WSType& aRight); + friend const WSType operator&(const WSType& aLeft, const WSType& aRight); + friend const WSType operator|(const WSType& aLeft, const WSType& aRight); + WSType& operator=(const WSType& aOther) + { + // This handles self-assignment fine + mEnum = aOther.mEnum; + return *this; + } + WSType& operator&=(const WSType& aOther) + { + mEnum &= aOther.mEnum; + return *this; + } + WSType& operator|=(const WSType& aOther) + { + mEnum |= aOther.mEnum; + return *this; + } + +private: + uint16_t mEnum; + void bool_conversion_helper() {} + +public: + // Allow boolean conversion with no numeric conversion + typedef void (WSType::*bool_type)(); + operator bool_type() const + { + return mEnum ? &WSType::bool_conversion_helper : nullptr; + } +}; + +/** + * These are declared as global functions so "WSType::Enum == WSType" et al. + * will work using the implicit constructor. + */ +inline bool operator==(const WSType& aLeft, const WSType& aRight) +{ + return aLeft.mEnum == aRight.mEnum; +} + +inline bool operator!=(const WSType& aLeft, const WSType& aRight) +{ + return !(aLeft == aRight); +} + +inline const WSType operator&(const WSType& aLeft, const WSType& aRight) +{ + WSType ret; + ret.mEnum = aLeft.mEnum & aRight.mEnum; + return ret; +} + +inline const WSType operator|(const WSType& aLeft, const WSType& aRight) +{ + WSType ret; + ret.mEnum = aLeft.mEnum | aRight.mEnum; + return ret; +} + +/** + * Make sure that & and | of WSType::Enum creates a WSType instead of an int, + * because operators between WSType and int shouldn't work + */ +inline const WSType operator&(const WSType::Enum& aLeft, + const WSType::Enum& aRight) +{ + return WSType(aLeft) & WSType(aRight); +} + +inline const WSType operator|(const WSType::Enum& aLeft, + const WSType::Enum& aRight) +{ + return WSType(aLeft) | WSType(aRight); +} + +class MOZ_STACK_CLASS WSRunObject final +{ +public: + enum BlockBoundary + { + kBeforeBlock, + kBlockStart, + kBlockEnd, + kAfterBlock + }; + + enum {eBefore = 1}; + enum {eAfter = 1 << 1}; + enum {eBoth = eBefore | eAfter}; + + WSRunObject(HTMLEditor* aHTMLEditor, nsINode* aNode, int32_t aOffset); + WSRunObject(HTMLEditor* aHTMLEditor, nsIDOMNode* aNode, int32_t aOffset); + ~WSRunObject(); + + // ScrubBlockBoundary removes any non-visible whitespace at the specified + // location relative to a block node. + static nsresult ScrubBlockBoundary(HTMLEditor* aHTMLEditor, + BlockBoundary aBoundary, + nsINode* aBlock, + int32_t aOffset = -1); + + // PrepareToJoinBlocks fixes up ws at the end of aLeftBlock and the + // beginning of aRightBlock in preperation for them to be joined. Example + // of fixup: trailingws in aLeftBlock needs to be removed. + static nsresult PrepareToJoinBlocks(HTMLEditor* aHTMLEditor, + dom::Element* aLeftBlock, + dom::Element* aRightBlock); + + // PrepareToDeleteRange fixes up ws before {aStartNode,aStartOffset} + // and after {aEndNode,aEndOffset} in preperation for content + // in that range to be deleted. Note that the nodes and offsets + // are adjusted in response to any dom changes we make while + // adjusting ws. + // example of fixup: trailingws before {aStartNode,aStartOffset} + // needs to be removed. + static nsresult PrepareToDeleteRange(HTMLEditor* aHTMLEditor, + nsCOMPtr<nsINode>* aStartNode, + int32_t* aStartOffset, + nsCOMPtr<nsINode>* aEndNode, + int32_t* aEndOffset); + + // PrepareToDeleteNode fixes up ws before and after aContent in preparation + // for aContent to be deleted. Example of fixup: trailingws before + // aContent needs to be removed. + static nsresult PrepareToDeleteNode(HTMLEditor* aHTMLEditor, + nsIContent* aContent); + + // PrepareToSplitAcrossBlocks fixes up ws before and after + // {aSplitNode,aSplitOffset} in preparation for a block parent to be split. + // Note that the aSplitNode and aSplitOffset are adjusted in response to + // any DOM changes we make while adjusting ws. Example of fixup: normalws + // before {aSplitNode,aSplitOffset} needs to end with nbsp. + static nsresult PrepareToSplitAcrossBlocks(HTMLEditor* aHTMLEditor, + nsCOMPtr<nsINode>* aSplitNode, + int32_t* aSplitOffset); + + // InsertBreak inserts a br node at {aInOutParent,aInOutOffset} + // and makes any needed adjustments to ws around that point. + // example of fixup: normalws after {aInOutParent,aInOutOffset} + // needs to begin with nbsp. + already_AddRefed<dom::Element> InsertBreak(nsCOMPtr<nsINode>* aInOutParent, + int32_t* aInOutOffset, + nsIEditor::EDirection aSelect); + + // InsertText inserts a string at {aInOutParent,aInOutOffset} and makes any + // needed adjustments to ws around that point. Example of fixup: + // trailingws before {aInOutParent,aInOutOffset} needs to be removed. + nsresult InsertText(const nsAString& aStringToInsert, + nsCOMPtr<nsINode>* aInOutNode, + int32_t* aInOutOffset, + nsIDocument* aDoc); + + // DeleteWSBackward deletes a single visible piece of ws before the ws + // point (the point to create the wsRunObject, passed to its constructor). + // It makes any needed conversion to adjacent ws to retain its + // significance. + nsresult DeleteWSBackward(); + + // DeleteWSForward deletes a single visible piece of ws after the ws point + // (the point to create the wsRunObject, passed to its constructor). It + // makes any needed conversion to adjacent ws to retain its significance. + nsresult DeleteWSForward(); + + // PriorVisibleNode returns the first piece of visible thing before + // {aNode,aOffset}. If there is no visible ws qualifying it returns what + // is before the ws run. Note that {outVisNode,outVisOffset} is set to + // just AFTER the visible object. + void PriorVisibleNode(nsINode* aNode, + int32_t aOffset, + nsCOMPtr<nsINode>* outVisNode, + int32_t* outVisOffset, + WSType* outType); + + // NextVisibleNode returns the first piece of visible thing after + // {aNode,aOffset}. If there is no visible ws qualifying it returns what + // is after the ws run. Note that {outVisNode,outVisOffset} is set to just + // BEFORE the visible object. + void NextVisibleNode(nsINode* aNode, + int32_t aOffset, + nsCOMPtr<nsINode>* outVisNode, + int32_t* outVisOffset, + WSType* outType); + + // AdjustWhitespace examines the ws object for nbsp's that can + // be safely converted to regular ascii space and converts them. + nsresult AdjustWhitespace(); + +protected: + // WSFragment represents a single run of ws (all leadingws, or all normalws, + // or all trailingws, or all leading+trailingws). Note that this single run + // may still span multiple nodes. + struct WSFragment final + { + nsCOMPtr<nsINode> mStartNode; // node where ws run starts + nsCOMPtr<nsINode> mEndNode; // node where ws run ends + int32_t mStartOffset; // offset where ws run starts + int32_t mEndOffset; // offset where ws run ends + // type of ws, and what is to left and right of it + WSType mType, mLeftType, mRightType; + // other ws runs to left or right. may be null. + WSFragment *mLeft, *mRight; + + WSFragment() + : mStartOffset(0) + , mEndOffset(0) + , mLeft(nullptr) + , mRight(nullptr) + {} + }; + + // A WSPoint struct represents a unique location within the ws run. It is + // always within a textnode that is one of the nodes stored in the list + // in the wsRunObject. For convenience, the character at that point is also + // stored in the struct. + struct MOZ_STACK_CLASS WSPoint final + { + RefPtr<dom::Text> mTextNode; + uint32_t mOffset; + char16_t mChar; + + WSPoint() + : mTextNode(nullptr) + , mOffset(0) + , mChar(0) + {} + + WSPoint(dom::Text* aTextNode, int32_t aOffset, char16_t aChar) + : mTextNode(aTextNode) + , mOffset(aOffset) + , mChar(aChar) + {} + }; + + enum AreaRestriction + { + eAnywhere, eOutsideUserSelectAll + }; + + /** + * Return the node which we will handle white-space under. This is the + * closest block within the DOM subtree we're editing, or if none is + * found, the (inline) root of the editable subtree. + */ + nsINode* GetWSBoundingParent(); + + nsresult GetWSNodes(); + void GetRuns(); + void ClearRuns(); + void MakeSingleWSRun(WSType aType); + nsIContent* GetPreviousWSNodeInner(nsINode* aStartNode, + nsINode* aBlockParent); + nsIContent* GetPreviousWSNode(EditorDOMPoint aPoint, nsINode* aBlockParent); + nsIContent* GetNextWSNodeInner(nsINode* aStartNode, nsINode* aBlockParent); + nsIContent* GetNextWSNode(EditorDOMPoint aPoint, nsINode* aBlockParent); + nsresult PrepareToDeleteRangePriv(WSRunObject* aEndObject); + nsresult PrepareToSplitAcrossBlocksPriv(); + nsresult DeleteChars(nsINode* aStartNode, int32_t aStartOffset, + nsINode* aEndNode, int32_t aEndOffset, + AreaRestriction aAR = eAnywhere); + WSPoint GetCharAfter(nsINode* aNode, int32_t aOffset); + WSPoint GetCharBefore(nsINode* aNode, int32_t aOffset); + WSPoint GetCharAfter(const WSPoint& aPoint); + WSPoint GetCharBefore(const WSPoint& aPoint); + nsresult ConvertToNBSP(WSPoint aPoint, + AreaRestriction aAR = eAnywhere); + void GetAsciiWSBounds(int16_t aDir, nsINode* aNode, int32_t aOffset, + dom::Text** outStartNode, int32_t* outStartOffset, + dom::Text** outEndNode, int32_t* outEndOffset); + void FindRun(nsINode* aNode, int32_t aOffset, WSFragment** outRun, + bool after); + char16_t GetCharAt(dom::Text* aTextNode, int32_t aOffset); + WSPoint GetWSPointAfter(nsINode* aNode, int32_t aOffset); + WSPoint GetWSPointBefore(nsINode* aNode, int32_t aOffset); + nsresult CheckTrailingNBSPOfRun(WSFragment *aRun); + nsresult CheckTrailingNBSP(WSFragment* aRun, nsINode* aNode, + int32_t aOffset); + nsresult CheckLeadingNBSP(WSFragment* aRun, nsINode* aNode, + int32_t aOffset); + + nsresult Scrub(); + bool IsBlockNode(nsINode* aNode); + + // The node passed to our constructor. + nsCOMPtr<nsINode> mNode; + // The offset passed to our contructor. + int32_t mOffset; + // Together, the above represent the point at which we are building up ws info. + + // true if we are in preformatted whitespace context. + bool mPRE; + // Node/offset where ws starts. + nsCOMPtr<nsINode> mStartNode; + int32_t mStartOffset; + // Reason why ws starts (eText, eOtherBlock, etc.). + WSType mStartReason; + // The node that implicated by start reason. + nsCOMPtr<nsINode> mStartReasonNode; + + // Node/offset where ws ends. + nsCOMPtr<nsINode> mEndNode; + int32_t mEndOffset; + // Reason why ws ends (eText, eOtherBlock, etc.). + WSType mEndReason; + // The node that implicated by end reason. + nsCOMPtr<nsINode> mEndReasonNode; + + // Location of first nbsp in ws run, if any. + RefPtr<dom::Text> mFirstNBSPNode; + int32_t mFirstNBSPOffset; + + // Location of last nbsp in ws run, if any. + RefPtr<dom::Text> mLastNBSPNode; + int32_t mLastNBSPOffset; + + // The list of nodes containing ws in this run. + nsTArray<RefPtr<dom::Text>> mNodeArray; + + // The first WSFragment in the run. + WSFragment* mStartRun; + // The last WSFragment in the run, may be same as first. + WSFragment* mEndRun; + + // Non-owning. + HTMLEditor* mHTMLEditor; + + // Opening this class up for pillaging. + friend class HTMLEditRules; + // Opening this class up for more pillaging. + friend class HTMLEditor; +}; + +} // namespace mozilla + +#endif // #ifndef WSRunObject_h diff --git a/editor/libeditor/crashtests/1057677.html b/editor/libeditor/crashtests/1057677.html new file mode 100644 index 000000000..d0b9497a5 --- /dev/null +++ b/editor/libeditor/crashtests/1057677.html @@ -0,0 +1,9 @@ +<html><body></body><script> +document.designMode = "on"; +var hrElem = document.createElement("HR"); +var select = window.getSelection(); +document.body.appendChild(hrElem); +select.collapse(hrElem,0); +document.execCommand("InsertHTML", false, "<div>foo</div><div>bar</div>"); +</script> +</html> diff --git a/editor/libeditor/crashtests/1128787.html b/editor/libeditor/crashtests/1128787.html new file mode 100644 index 000000000..fc6bff097 --- /dev/null +++ b/editor/libeditor/crashtests/1128787.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<title>Bug 1128787</title> +</head> +<body> + <input type="button"/> + <script> + window.onload = function () { + document.designMode = "on"; + } + var input = document.getElementsByTagName("input")[0]; + input.focus(); + input.type = "text"; + </script> +</body> +</html> diff --git a/editor/libeditor/crashtests/1134545.html b/editor/libeditor/crashtests/1134545.html new file mode 100644 index 000000000..4e871804f --- /dev/null +++ b/editor/libeditor/crashtests/1134545.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- saved from url=(0065)https://bug1134545.bugzilla.mozilla.org/attachment.cgi?id=8566418 --> +<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252"> +<script> + +function boom() +{ + textNode = document.createTextNode(" "); + x.appendChild(textNode); + x.setAttribute('contenteditable', "true"); + textNode.remove(); + window.getSelection().selectAllChildren(textNode); + document.execCommand("increasefontsize", false, null); +} + +</script> +</head> +<body onload="boom();"> +<div id="x" contenteditable="true"></div> + + +</body></html>
\ No newline at end of file diff --git a/editor/libeditor/crashtests/1158452.html b/editor/libeditor/crashtests/1158452.html new file mode 100644 index 000000000..56c74abd6 --- /dev/null +++ b/editor/libeditor/crashtests/1158452.html @@ -0,0 +1,10 @@ + +<div> +<div> +aaaaaaa +</script> +<script type="text/javascript"> +document.designMode = "on" +window.getSelection().modify("extend", "backward", "line") +document.execCommand("increasefontsize","",null); +</script> diff --git a/editor/libeditor/crashtests/1158651.html b/editor/libeditor/crashtests/1158651.html new file mode 100644 index 000000000..27278b523 --- /dev/null +++ b/editor/libeditor/crashtests/1158651.html @@ -0,0 +1,18 @@ +<script> +onload = function() { + var testContainer = document.createElement("span"); + testContainer.contentEditable = true; + document.body.appendChild(testContainer); + + function queryFormatBlock(content) + { + testContainer.innerHTML = content; + while (testContainer.firstChild) + testContainer = testContainer.firstChild; + window.getSelection().collapse(testContainer, 0); + document.queryCommandValue('formatBlock'); + } + + queryFormatBlock('<ol>hello</ol>'); +}; +</script> diff --git a/editor/libeditor/crashtests/1244894.xhtml b/editor/libeditor/crashtests/1244894.xhtml new file mode 100644 index 000000000..89a24751e --- /dev/null +++ b/editor/libeditor/crashtests/1244894.xhtml @@ -0,0 +1,21 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> + +function boom() +{ + document.designMode = 'on'; + document.execCommand("indent", false, null); + document.execCommand("insertText", false, "a"); + document.execCommand("forwardDelete", false, null); + document.execCommand("justifyfull", false, null); +} + +window.addEventListener("load", boom, false); + +</script> +</head> + +<body> <span class="v"></span></body><body><input type="file" /></body> + +</html> diff --git a/editor/libeditor/crashtests/1272490.html b/editor/libeditor/crashtests/1272490.html new file mode 100644 index 000000000..a3c8ecd81 --- /dev/null +++ b/editor/libeditor/crashtests/1272490.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<script> +window.onload = function () { + var childDocument = document.getElementsByTagName("iframe")[0].contentDocument; + childDocument.designMode = "on"; + function onAttrModified(aEvent) { + childDocument.removeEventListener("DOMAttrModified", onAttrModified, false); + // Remove the editor from document during executing "insertOrderedList". + document.body.innerHTML = ""; + } + childDocument.addEventListener("DOMAttrModified", onAttrModified, false); + childDocument.execCommand("insertOrderedList", false, "1"); +} +</script> +</head> +<body><iframe></iframe></body> +</html> diff --git a/editor/libeditor/crashtests/1317704.html b/editor/libeditor/crashtests/1317704.html new file mode 100644 index 000000000..64359c796 --- /dev/null +++ b/editor/libeditor/crashtests/1317704.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<script> +addEventListener('DOMContentLoaded', function(){ + document.documentElement.className = 'lizard'; + setTimeout(function(){ + document.execCommand('selectAll', false, null); + document.designMode = 'on'; + document.execCommand('removeformat', false, null); + }, 0); +}); +</script> +<style> +.lizard{ + -webkit-user-select:all; +} +*{ + position:fixed; + display:table-column; +} +</style> +</head> +<body> +<span> +<span contenteditable> +<span class=lizard></span> +<span class=lizard></span> +<span /> +</span> +</span> +</span> +</span> +</body> +</html> diff --git a/editor/libeditor/crashtests/336081-1.xhtml b/editor/libeditor/crashtests/336081-1.xhtml new file mode 100644 index 000000000..da653c601 --- /dev/null +++ b/editor/libeditor/crashtests/336081-1.xhtml @@ -0,0 +1,52 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> +<head> +<script> +<![CDATA[ + +function foop(targetWindow) +{ + var targetDocument = targetWindow.document; + + var r1 = targetDocument.createRange(); + r1.setStart(targetDocument.getElementById("out1"), 0); + r1.setEnd (targetDocument.getElementById("out2"), 0); + targetWindow.getSelection().addRange(r1); + + var r2 = targetDocument.createRange(); + r2.setStart(targetDocument.getElementById("in1"), 0); + r2.setEnd (targetDocument.getElementById("in2"), 0); + targetWindow.getSelection().addRange(r2); + + targetDocument.execCommand('removeformat', false, null); + targetDocument.execCommand('outdent', false, null); +} + +function init() +{ + setTimeout(function() + { + var fd = window.frames[0].document; + fd.body.appendChild(fd.importNode(document.getElementById('rootish'), true)); + fd.designMode = 'on'; + foop(window.frames[0]); + document.documentElement.removeAttribute("class"); + }, 100); +} + +]]> +</script> +</head> + +<body onload="init()"> + +<iframe src="data:text/html," style="width: 95%; height: 500px;"/> + +<div id="rootish"> +<div id="out1"/> +<div id="in1"/> +<div id="in2"/> +<div id="out2"/> +</div> + +</body> +</html>
\ No newline at end of file diff --git a/editor/libeditor/crashtests/336104.html b/editor/libeditor/crashtests/336104.html new file mode 100644 index 000000000..32f11b745 --- /dev/null +++ b/editor/libeditor/crashtests/336104.html @@ -0,0 +1,37 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +function init() +{ + var targetWindow = window.frames[0]; + var targetDocument = targetWindow.document; + var rootish = document.getElementById('rootish'); + + targetDocument.body.appendChild(targetDocument.adoptNode(rootish)); + targetDocument.designMode = 'on'; + + targetWindow.getSelection().removeAllRanges(); + + var r = targetDocument.createRange(); + r.setStart(targetDocument.getElementById("start"), 0); + r.setEnd (targetDocument.getElementById("endparent").firstChild, 0); + targetWindow.getSelection().addRange(r); + + targetDocument.execCommand('outdent', false, null); +} +</script> + +</head> + +<body onload="setTimeout(init, 300);"> + +<iframe src="data:text/html," style="width: 95%; height: 500px;"></iframe> + +<div id="rootish"> + <div id="start"></div> + <p>Huh</p> + <svg xmlns="http://www.w3.org/2000/svg" id="endparent"> </svg> +</div> + +</body> +</html> diff --git a/editor/libeditor/crashtests/382527-1.html b/editor/libeditor/crashtests/382527-1.html new file mode 100644 index 000000000..2441dcd87 --- /dev/null +++ b/editor/libeditor/crashtests/382527-1.html @@ -0,0 +1,58 @@ +<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+
+function init1()
+{
+ targetIframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
+ targetIframe.src = "data:text/html,";
+ targetIframe.setAttribute("style", "width: 300px; height: 200px; border: 1px dotted green;");
+ targetIframe.addEventListener("load", init2, false);
+ document.body.appendChild(targetIframe);
+}
+
+
+function init2()
+{
+ targetWindow = targetIframe.contentWindow;
+ targetDocument = targetWindow.document;
+
+ var div = document.getElementById("div");
+ textNode = div.firstChild;
+
+ targetDocument.body.appendChild(targetDocument.adoptNode(div, true));
+
+ targetDocument.designMode = 'on';
+ setTimeout(init3, 0);
+}
+
+
+function init3()
+{
+ var rng = targetDocument.createRange();
+ rng.setStart(textNode, 1);
+ rng.setEnd(textNode, 1);
+ targetWindow.getSelection().addRange(rng);
+
+ try {
+ targetDocument.execCommand("inserthtml", false, "<p>");
+ } catch(e) {}
+
+ document.documentElement.removeAttribute("class");
+}
+
+
+</script>
+
+</head>
+
+<body onload="init1();">
+
+<div id="div"> </div>
+
+<script>
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/382778-1.html b/editor/libeditor/crashtests/382778-1.html new file mode 100644 index 000000000..960b16630 --- /dev/null +++ b/editor/libeditor/crashtests/382778-1.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> + +function init1() +{ + // Create an html:iframe in HTML mode (so designMode can be used 320092) + targetIframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe'); + targetIframe.src = "data:text/html,"; + targetIframe.setAttribute("style", "width: 700px; height: 500px; border: 1px dotted green;"); + targetIframe.addEventListener("load", init2, false); + document.body.appendChild(targetIframe); +} + + +function init2() +{ + targetWindow = targetIframe.contentWindow; + targetDocument = targetWindow.document; + + p = document.getElementById("p"); + pText = p.firstChild; + + targetDocument.body.appendChild(targetDocument.adoptNode(p, true)); + + targetDocument.designMode = 'on'; + + setTimeout(boom, 0); +} + + +function boom() +{ + var rng = targetDocument.createRange(); + rng.setStart(pText, 3); + rng.setEnd(pText, 3); + + targetWindow.getSelection().addRange(rng); + + targetDocument.execCommand("insertorderedlist", false, null); + + document.documentElement.removeAttribute("class") +} + +</script> +</head> + +<body onload="init1();"> +<p id="p">word word</p> +</body> + +</html> diff --git a/editor/libeditor/crashtests/402172-1.html b/editor/libeditor/crashtests/402172-1.html new file mode 100644 index 000000000..4022523fa --- /dev/null +++ b/editor/libeditor/crashtests/402172-1.html @@ -0,0 +1,23 @@ +<html> +<head> +<script> + +function boom() +{ + document.getElementById("div").contentEditable = "true"; + document.getElementById("div").focus(); + document.getElementById("div").contentEditable = "false"; + + document.getElementById("table").contentEditable = "true"; +} + +</script> +</head> + +<body onload="boom();"> + +<table id="table"><td></td></table><div id="div"></div> + +</body> + +</html> diff --git a/editor/libeditor/crashtests/403965-1.xhtml b/editor/libeditor/crashtests/403965-1.xhtml new file mode 100644 index 000000000..02993914d --- /dev/null +++ b/editor/libeditor/crashtests/403965-1.xhtml @@ -0,0 +1,7 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<tbody contenteditable="true"/> +<frameset/> +<xul:box onbeforecopy="event.explicitOriginalTarget.parentNode.parentNode.removeChild(event.explicitOriginalTarget.parentNode)"> +<xul:box/> +</xul:box> +</html>
\ No newline at end of file diff --git a/editor/libeditor/crashtests/407074-1.html b/editor/libeditor/crashtests/407074-1.html new file mode 100644 index 000000000..22f172856 --- /dev/null +++ b/editor/libeditor/crashtests/407074-1.html @@ -0,0 +1,7 @@ +<html> +<head> +</head> + +<body onload="try { document.execCommand('inserthtml', false, '0'); } catch(e) { }"><span id="textarea" contenteditable="true">is</span></body> + +</html> diff --git a/editor/libeditor/crashtests/407079-1.html b/editor/libeditor/crashtests/407079-1.html new file mode 100644 index 000000000..8b0e36cd6 --- /dev/null +++ b/editor/libeditor/crashtests/407079-1.html @@ -0,0 +1,15 @@ +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + document.execCommand("selectAll", false, null); + document.execCommand("inserthtml", false, "<p>"); +} + +</script> +</head> + +<body onload="boom();"><textarea contenteditable="true"></textarea></body> +</html> diff --git a/editor/libeditor/crashtests/407256-1.html b/editor/libeditor/crashtests/407256-1.html new file mode 100644 index 000000000..824162ac5 --- /dev/null +++ b/editor/libeditor/crashtests/407256-1.html @@ -0,0 +1,23 @@ +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + document.addEventListener("DOMNodeInserted", x, false); + + function x() + { + document.removeEventListener("DOMNodeInserted", x, false); + document.execCommand("insertParagraph", false, ""); + } + + document.execCommand("insertorderedlist", false, ""); +} + +</script> +</head> + +<body contenteditable="true" onload="boom()"></body> + +</html> diff --git a/editor/libeditor/crashtests/407277-1.html b/editor/libeditor/crashtests/407277-1.html new file mode 100644 index 000000000..41c6bf280 --- /dev/null +++ b/editor/libeditor/crashtests/407277-1.html @@ -0,0 +1,7 @@ +<html> +<head> +</head> +<body style="margin: initial;" + contenteditable="true" + onload="document.execCommand('outdent', false, null);"></body> +</html> diff --git a/editor/libeditor/crashtests/414178-1.html b/editor/libeditor/crashtests/414178-1.html new file mode 100644 index 000000000..19cc205b9 --- /dev/null +++ b/editor/libeditor/crashtests/414178-1.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + var table = document.createElement("table"); + document.body.appendChild(table); + table.contentEditable = "true"; + table.focus(); + try { + // This will throw, since it's attempting to inject a list inside a table + document.execCommand("insertunorderedlist", false, null); + } catch (e) {} +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/418923-1.html b/editor/libeditor/crashtests/418923-1.html new file mode 100644 index 000000000..786ea25d9 --- /dev/null +++ b/editor/libeditor/crashtests/418923-1.html @@ -0,0 +1,19 @@ +<html><head><script type="text/javascript"> + +function boom() +{ + var dE = document.documentElement; + var head = document.getElementsByTagName("head")[0]; + dE.removeChild(document.body); + dE.contentEditable = "true"; + dE.focus(); + dE.contentEditable = "false"; + head.focus(); + head.contentEditable = "true"; + try { + document.execCommand("selectAll", false, ""); + } catch(e) { + } +} + +</script></head><body onload="boom();"></body></html>
\ No newline at end of file diff --git a/editor/libeditor/crashtests/420439.html b/editor/libeditor/crashtests/420439.html new file mode 100644 index 000000000..e1303307d --- /dev/null +++ b/editor/libeditor/crashtests/420439.html @@ -0,0 +1,30 @@ +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + function x() + { + document.removeEventListener("DOMAttrModified", x, false); + document.execCommand("backcolor", false, "green"); + } + + document.getElementById("td").focus(); + + document.addEventListener("DOMAttrModified", x, false); + try { + document.execCommand("subscript", false, null); + } catch(e) { + } + document.removeEventListener("DOMAttrModified", x, false); +} + +</script> +</head> + +<body contenteditable="true" onload="setTimeout(boom, 30);"> +<table><tbody contenteditable="false"><tr><td contenteditable="true" id="td"></td></tr></tbody></table> +</body> + +</html> diff --git a/editor/libeditor/crashtests/428489-1.html b/editor/libeditor/crashtests/428489-1.html new file mode 100644 index 000000000..8eec1268b --- /dev/null +++ b/editor/libeditor/crashtests/428489-1.html @@ -0,0 +1,8 @@ +<html>
+<head>
+<title>Crash [@ nsHTMLEditor::GetPositionAndDimensions] when window gets removed during click on contenteditable absolute positioned element</title>
+</head>
+<body>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%0A%3Cscript%3E%0Awindow.addEventListener%28%27DOMAttrModified%27%2C%20function%28e%29%20%7Bdump%28%27DOMAttrModified\n%27%29%3Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%3B%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/head%3E%0A%3Cbody%3E%0A%3Cdiv%20style%3D%22position%3A%20absolute%3B%22%20contenteditable%3D%22true%22%3EClicking%20on%20this%20should%20not%20crash%20Mozilla%0A%3C/body%3E%0A%3C/html%3E" style="width:1000px;height: 300px;"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/429586-1.html b/editor/libeditor/crashtests/429586-1.html new file mode 100644 index 000000000..a32df3b72 --- /dev/null +++ b/editor/libeditor/crashtests/429586-1.html @@ -0,0 +1,8 @@ +<html>
+<head>
+<title>Bug 429586 - Crash [@ nsEditor::EndUpdateViewBatch] with pasting and domattrmodified removing iframe</title>
+</head>
+<body>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Chtml%3E%0A%3Chead%3E%0A%3C/head%3E%0A%3Cbody%20contenteditable%3D%22true%22%3E%0A%0A%3Cscript%3E%0Afunction%20dokey%28%29%7B%0Adocument.body.focus%28%29%3B%0Adocument.execCommand%28%27insertParagraph%27%2C%20false%2C%20%27%27%29%3B%0A%7D%0AsetTimeout%28dokey%2C200%29%3B%0A%0Adocument.addEventListener%28%27DOMAttrModified%27%2C%20function%28%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/body%3E%0A%3C/html%3E"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/430624-1.html b/editor/libeditor/crashtests/430624-1.html new file mode 100644 index 000000000..bfa95c662 --- /dev/null +++ b/editor/libeditor/crashtests/430624-1.html @@ -0,0 +1,14 @@ +<html> +<head> +<script> +function crash() { + window.frames[0].onload = null; + window.frames[0].location = 'data:text/html;charset=utf-8,2nd%20page'; +} +</script> +</head> +<body onload="crash()"> + <!-- iframe contents: <html><body onload="document.body.setAttribute('spellcheck', true);"></body></html> --> + <iframe src="data:text/html;charset=utf-8;base64,PGh0bWw%2BPGJvZHkgb25sb2FkPSJkb2N1bWVudC5ib2R5LnNldEF0dHJpYnV0ZSgnc3BlbGxjaGVjaycsIHRydWUpOyI%2BPC9ib2R5PjwvaHRtbD4%3D"></iframe> +</body> +</html>
\ No newline at end of file diff --git a/editor/libeditor/crashtests/431086-1.xhtml b/editor/libeditor/crashtests/431086-1.xhtml new file mode 100644 index 000000000..c6c5d8d99 --- /dev/null +++ b/editor/libeditor/crashtests/431086-1.xhtml @@ -0,0 +1,22 @@ +<div xmlns="http://www.w3.org/1999/xhtml"> + +<script type="text/javascript"> + +function boom() +{ + var r = document.documentElement; + r.style.position = "absolute"; + r.contentEditable = "true"; + r.focus(); + r.contentEditable = "false"; + r.focus(); + r.contentEditable = "true"; + document.execCommand("subscript", false, null); + r.contentEditable = "false"; +} + +window.addEventListener("load", boom, false); + +</script> + +</div> diff --git a/editor/libeditor/crashtests/448329-1.html b/editor/libeditor/crashtests/448329-1.html new file mode 100644 index 000000000..99d0f63b3 --- /dev/null +++ b/editor/libeditor/crashtests/448329-1.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html><head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Testcase for bug 448329</title> +</head> +<body> + +<iframe id="frame448329"></iframe> + +<script> + +function test448329(id,cmd) { + + var elm = document.getElementById(id); + var doc = elm.contentDocument; + doc.designMode = "On"; + + // Work around getSelection depending on a presshell but not flushing to get + // one. + doc.body.offsetWidth; + var s = doc.defaultView.getSelection(); + + // Test document node + if (s.rangeCount > 0) + s.removeAllRanges(); + var range = doc.createRange(); + range.setStart(doc, 0); + range.setEnd(doc, 0); + s.addRange(range); + doc.queryCommandIndeterm(cmd); + + // Test HTML node + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + range.setStart(doc.documentElement, 0); + range.setEnd(doc.documentElement, 0); + s.addRange(range); + doc.queryCommandIndeterm(cmd); + + // Test BODY node + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + var body = doc.documentElement.childNodes[1]; + range.setStart(body, 0); + range.setEnd(body, 0); + s.addRange(range); + doc.queryCommandIndeterm(cmd); + + var text = doc.createTextNode("Hello Kitty"); + body.insertBefore(text, null) + + // Test TEXT node + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + range.setStart(text, 0); + range.setEnd(text, 1); + s.addRange(range); + doc.queryCommandIndeterm(cmd); + +} + +test448329("frame448329", "backcolor") +test448329("frame448329", "hilitecolor") + +</script> + + +</body> +</html> diff --git a/editor/libeditor/crashtests/448329-2.html b/editor/libeditor/crashtests/448329-2.html new file mode 100644 index 000000000..fd4707b54 --- /dev/null +++ b/editor/libeditor/crashtests/448329-2.html @@ -0,0 +1,21 @@ +<html> +<head> + <title>Testcase for bug 448329</title> +<script> +function go() { + test("myFrame", "backcolor"); +} +function test(id,cmd) { + var doc = document.getElementById(id).contentDocument; + doc.designMode = "On"; + + var s = doc.defaultView.getSelection(); + s.removeAllRanges(); + s.addRange(doc.createRange()); + + doc.queryCommandIndeterm(cmd); +} +</script> +</head> +<body onload="go()"><iframe id="myFrame"></iframe></body> +</html> diff --git a/editor/libeditor/crashtests/448329-3.html b/editor/libeditor/crashtests/448329-3.html new file mode 100644 index 000000000..0a48c1818 --- /dev/null +++ b/editor/libeditor/crashtests/448329-3.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html><head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Testcase #3 for bug 448329</title> +</head> +<body> + +<iframe id="frame448329"></iframe> + +<script> + +function test448329(id,cmd,val) { + + var elm = document.getElementById(id); + var doc = elm.contentDocument; + doc.designMode = "On"; + + // Work around getSelection depending on a presshell but not flushing to get + // one. + doc.body.offsetWidth; + var s = doc.defaultView.getSelection(); + + // Test document node + if (s.rangeCount > 0) + s.removeAllRanges(); + var range = doc.createRange(); + range.setStart(doc, 0); + range.setEnd(doc, 0); + s.addRange(range); + doc.execCommand(cmd,false,val); + + // Test HTML node + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + range.setStart(doc.documentElement, 0); + range.setEnd(doc.documentElement, 0); + s.addRange(range); + doc.execCommand(cmd,false,val); + + // Test BODY node + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + var body = doc.documentElement.childNodes[1]; + range.setStart(body, 0); + range.setEnd(body, 0); + s.addRange(range); + doc.execCommand(cmd,false,val); + + var text = doc.createTextNode("Hello Kitty"); + body.insertBefore(text, null) + + // Test TEXT node + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + range.setStart(text, 0); + range.setEnd(text, 1); + s.addRange(range); + doc.execCommand(cmd,false,val); + + // Test BODY[0,0] + TEXT node + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + range.setStart(body, 0); + range.setEnd(body, 0); + s.addRange(range); + range = doc.createRange(); + range.setStart(text, 0); + range.setEnd(text, 1); + s.addRange(range); + doc.execCommand(cmd,false,val); + + // Test BODY[0,1] + TEXT node + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + range.setStart(body, 0); + range.setEnd(body, 1); + s.addRange(range); + range = doc.createRange(); + range.setStart(text, 0); + range.setEnd(text, 1); + s.addRange(range); + doc.execCommand(cmd,false,val); + + // Test BODY[0,1] + TEXT node without a parent + if (s.rangeCount > 0) + s.removeAllRanges(); + range = doc.createRange(); + range.setStart(body, 0); + range.setEnd(body, 1); + s.addRange(range); + range = doc.createRange(); + text = doc.createTextNode("Hello Kitty"); // not in doc + range.setStart(text, 0); + range.setEnd(text, 1); + s.addRange(range); + doc.execCommand(cmd,false,val); + +} + +test448329("frame448329", "backcolor", "green") +test448329("frame448329", "hilitecolor", "green") + +</script> + + +</body> +</html> diff --git a/editor/libeditor/crashtests/456727-1.html b/editor/libeditor/crashtests/456727-1.html new file mode 100644 index 000000000..d14422c93 --- /dev/null +++ b/editor/libeditor/crashtests/456727-1.html @@ -0,0 +1,8 @@ +<html> +<BODY onload=" +document.designMode='on'; +document.replaceChild(document.createElement('HTML'), document.firstChild); +document.queryCommandValue('backcolor'); +"> +</body> +</html>
\ No newline at end of file diff --git a/editor/libeditor/crashtests/456727-2.html b/editor/libeditor/crashtests/456727-2.html new file mode 100644 index 000000000..1c8fe5db9 --- /dev/null +++ b/editor/libeditor/crashtests/456727-2.html @@ -0,0 +1,8 @@ +<html> +<BODY onload=" +document.designMode='on'; +document.removeChild(document.firstChild); +document.queryCommandState('BackColor'); +"> +</body> +</html> diff --git a/editor/libeditor/crashtests/459613-iframe.html b/editor/libeditor/crashtests/459613-iframe.html new file mode 100644 index 000000000..9f6e558d0 --- /dev/null +++ b/editor/libeditor/crashtests/459613-iframe.html @@ -0,0 +1 @@ +<html><body><textarea>notaword</textarea></body></html> diff --git a/editor/libeditor/crashtests/459613.html b/editor/libeditor/crashtests/459613.html new file mode 100644 index 000000000..7a335f22e --- /dev/null +++ b/editor/libeditor/crashtests/459613.html @@ -0,0 +1,17 @@ +<html class="reftest-wait"> +<head> +<script type="text/javascript"> + +function finish() { + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + +<body> + +<iframe src="459613-iframe.html" onload="finish();"></iframe> + +</body> +</html> diff --git a/editor/libeditor/crashtests/467647-1.html b/editor/libeditor/crashtests/467647-1.html new file mode 100644 index 000000000..7bb4271d7 --- /dev/null +++ b/editor/libeditor/crashtests/467647-1.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + document.getElementById("s").focus(); + try { + document.execCommand("insertorderedlist", false, null); + } catch(e) { } +} + +</script> +</head> + +<body onload="boom();"><span id="s" contenteditable="true">One<div></div></span><marquee></marquee></body> + +</html> diff --git a/editor/libeditor/crashtests/475132-1.xhtml b/editor/libeditor/crashtests/475132-1.xhtml new file mode 100644 index 000000000..8b4dd688c --- /dev/null +++ b/editor/libeditor/crashtests/475132-1.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script type="text/javascript"> + +function boom() +{ + document.getElementsByTagName("td")[0].contentEditable = "true"; + document.getElementsByTagName("td")[0].focus(); + document.documentElement.contentEditable = "true"; + document.documentElement.focus(); + document.execCommand("indent", false, null); + document.execCommand("insertParagraph", false, null); +} + +</script> +</head> + +<body onload="boom();" contenteditable="false"><td></td></body> + +</html> diff --git a/editor/libeditor/crashtests/499844-1.html b/editor/libeditor/crashtests/499844-1.html new file mode 100644 index 000000000..4fa509c4a --- /dev/null +++ b/editor/libeditor/crashtests/499844-1.html @@ -0,0 +1,15 @@ +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + document.body.contentEditable = "true"; + document.execCommand("outdent", false, null); +} + +</script> +</head> + +<body style="word-spacing: 3px;" onload="boom();"> ́</body> +</html> diff --git a/editor/libeditor/crashtests/503709-1.xhtml b/editor/libeditor/crashtests/503709-1.xhtml new file mode 100644 index 000000000..867bebf1a --- /dev/null +++ b/editor/libeditor/crashtests/503709-1.xhtml @@ -0,0 +1,11 @@ +<html contenteditable="true" xmlns="http://www.w3.org/1999/xhtml"><head><script> + +function boom() +{ + document.execCommand("selectAll", false, ""); + try { document.execCommand("justifyfull", false, null); } catch(e) { } + try { document.execCommand("inserthorizontalrule", false, "false"); } catch(e) { } + document.execCommand("delete", false, null); +} + +</script></head>x y z<body onload="boom();"><div/></body></html> diff --git a/editor/libeditor/crashtests/513375-1.xhtml b/editor/libeditor/crashtests/513375-1.xhtml new file mode 100644 index 000000000..25e5e1c34 --- /dev/null +++ b/editor/libeditor/crashtests/513375-1.xhtml @@ -0,0 +1,19 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head contenteditable="true"> +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + var r = document.createRange(); + r.selectNode(document.body); + r.deleteContents(); + try { document.execCommand("selectAll", false, null); } catch(e) { } +} + +]]> +</script> +</head> + +<body onload="boom();" contenteditable="true"></body> +</html> diff --git a/editor/libeditor/crashtests/535632-1.xhtml b/editor/libeditor/crashtests/535632-1.xhtml new file mode 100644 index 000000000..92470b825 --- /dev/null +++ b/editor/libeditor/crashtests/535632-1.xhtml @@ -0,0 +1 @@ +<body xmlns="http://www.w3.org/1999/xhtml" style="margin: 200px;" contenteditable="true" onload="document.execCommand('outdent', false, null);" />
\ No newline at end of file diff --git a/editor/libeditor/crashtests/574558-1.xhtml b/editor/libeditor/crashtests/574558-1.xhtml new file mode 100644 index 000000000..6aac47072 --- /dev/null +++ b/editor/libeditor/crashtests/574558-1.xhtml @@ -0,0 +1,15 @@ +<html xmlns="http://www.w3.org/1999/xhtml"><head><script> +<![CDATA[ + +function boom() +{ + document.execCommand("selectAll", false, null); + document.execCommand("selectAll", false, null); + document.execCommand("inserthtml", false, "<span><div>"); + var textarea = document.getElementById("textarea"); + var span = document.createElementNS("http://www.w3.org/1999/xhtml", "span"); + textarea.appendChild(span); +} + +]]> +</script></head><div contenteditable="true"></div><body onload="boom();"><textarea id="textarea">f</textarea></body></html> diff --git a/editor/libeditor/crashtests/580151-1.xhtml b/editor/libeditor/crashtests/580151-1.xhtml new file mode 100644 index 000000000..379941111 --- /dev/null +++ b/editor/libeditor/crashtests/580151-1.xhtml @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +var t; + +function boom() +{ + var b = document.createElementNS("http://www.w3.org/1999/xhtml", "body"); + t = document.createElementNS("http://www.w3.org/1999/xhtml", "textarea"); + b.appendChild(t); + document.removeChild(document.documentElement) + document.appendChild(b) + document.removeChild(document.documentElement) + var ns = document.createElementNS("http://www.w3.org/1999/xhtml", "script"); + var nt = document.createTextNode("t.appendChild(document.createTextNode(' '));"); + ns.appendChild(nt); + b.appendChild(ns); + document.appendChild(b); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/582138-1.xhtml b/editor/libeditor/crashtests/582138-1.xhtml new file mode 100644 index 000000000..afcec2eba --- /dev/null +++ b/editor/libeditor/crashtests/582138-1.xhtml @@ -0,0 +1,10 @@ +<html xmlns="http://www.w3.org/1999/xhtml"><mtr xmlns="http://www.w3.org/1998/Math/MathML"><td id="cell" xmlns="http://www.w3.org/1999/xhtml"></td></mtr><script> +function boom() +{ + document.getElementById("cell").contentEditable = true; + document.getElementById("cell").focus(); + document.execCommand("inserthtml", false, "x"); +} + +window.addEventListener("load", boom, false); +</script></html> diff --git a/editor/libeditor/crashtests/612565-1.html b/editor/libeditor/crashtests/612565-1.html new file mode 100644 index 000000000..1b059aa66 --- /dev/null +++ b/editor/libeditor/crashtests/612565-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <body> + <iframe></iframe> + </body> + <script> + onload = function() { + var i = document.querySelector("iframe"); + var doc = i.contentDocument; + doc.body.appendChild(doc.createTextNode("foo")); + doc.designMode = "on"; + while (doc.body.firstChild) { + doc.body.removeChild(doc.body.firstChild); + } + }; + </script> +</html> diff --git a/editor/libeditor/crashtests/615015-1.html b/editor/libeditor/crashtests/615015-1.html new file mode 100644 index 000000000..a383f9e75 --- /dev/null +++ b/editor/libeditor/crashtests/615015-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + document.getElementById("j").focus(); + try { + document.execCommand("insertunorderedlist", false, null); + } catch(e) { } +} + +</script> +</head> +<body onload="boom();"><span><span contenteditable id="j"></span>T</span></body> +</html> diff --git a/editor/libeditor/crashtests/615450-1.html b/editor/libeditor/crashtests/615450-1.html new file mode 100644 index 000000000..fb36bddc9 --- /dev/null +++ b/editor/libeditor/crashtests/615450-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html contenteditable="true"> +<head> +<script> + +function boom() +{ + document.documentElement.appendChild(document.body); + document.documentElement.contentEditable = "false"; + try { document.execCommand("outdent", false, null); } catch(e) { } + document.body.contentEditable = "true"; + try { document.execCommand("inserthtml", false, "x"); } catch(e) { } +} + +</script> +</head> +<body onload="boom();"><div style="margin-left: 40px;"><span contenteditable="true">p q r s</span> T</div></body></html> diff --git a/editor/libeditor/crashtests/633709.xhtml b/editor/libeditor/crashtests/633709.xhtml new file mode 100644 index 000000000..139389001 --- /dev/null +++ b/editor/libeditor/crashtests/633709.xhtml @@ -0,0 +1,36 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> + +<body><div contenteditable="true"></div><div><input id="i"><div></div></input></div></body> + +<script id="s"> +<![CDATA[ + +function boom() +{ + document.getElementById("i").focus(); + + try { document.execCommand("stylewithcss", false, "true") } catch(e) { } + try { document.execCommand("inserthtml", false, "<x>X</x>"); } catch(e) { } + try { document.execCommand("underline", false, null); } catch(e) { } + try { document.execCommand("justifyfull", false, null); } catch(e) { } + try { document.execCommand("underline", false, null); } catch(e) { } + try { document.execCommand("insertParagraph", false, null); } catch(e) { } + try { document.execCommand("delete", false, null); } catch(e) { } + + try { document.execCommand("stylewithcss", false, "false") } catch(e) { } + try { document.execCommand("inserthtml", false, "<x>X</x>"); } catch(e) { } + try { document.execCommand("underline", false, null); } catch(e) { } + try { document.execCommand("justifyfull", false, null); } catch(e) { } + try { document.execCommand("underline", false, null); } catch(e) { } + try { document.execCommand("insertParagraph", false, null); } catch(e) { } + try { document.execCommand("delete", false, null); } catch(e) { } + + document.documentElement.removeAttribute("class"); +} + +setTimeout(boom, 10); + +]]> +</script> + +</html> diff --git a/editor/libeditor/crashtests/636074-1.html b/editor/libeditor/crashtests/636074-1.html new file mode 100644 index 000000000..e99c42ea6 --- /dev/null +++ b/editor/libeditor/crashtests/636074-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + document.getElementById("i").focus(); + document.documentElement.contentEditable = "true"; + document.execCommand("inserthtml", false, "<table>"); + document.execCommand("indent", false, null); + document.execCommand("delete", false, null); +} + +</script> +</head> +<body onload="boom();"><input id="i"></body> +</html> diff --git a/editor/libeditor/crashtests/639736-1.xhtml b/editor/libeditor/crashtests/639736-1.xhtml new file mode 100644 index 000000000..4692daee7 --- /dev/null +++ b/editor/libeditor/crashtests/639736-1.xhtml @@ -0,0 +1,14 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> + +function boom() +{ + try { document.execCommand("removeformat", false, null); } catch(e) { } + document.adoptNode(document.documentElement); +} + +</script> +</head> +<body onload="boom();"><td contenteditable="true" /></body> +</html> diff --git a/editor/libeditor/crashtests/643786-1.html b/editor/libeditor/crashtests/643786-1.html new file mode 100644 index 000000000..3f0b27a54 --- /dev/null +++ b/editor/libeditor/crashtests/643786-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var f = document.getElementById("f"); + var fw = f.contentWindow; + fw.document.designMode = 'on'; + f.style.content = "'m'"; + fw.document.removeChild(fw.document.documentElement) +} + +</script> +</head> +<body onload="boom();"><iframe id="f" src="data:text/html,"></iframe></body> +</html> diff --git a/editor/libeditor/crashtests/650572-1.html b/editor/libeditor/crashtests/650572-1.html new file mode 100644 index 000000000..a86f6e618 --- /dev/null +++ b/editor/libeditor/crashtests/650572-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + document.documentElement.offsetHeight; + document.getElementById("d").focus(); + document.getElementById("b").style.display = "inline"; + document.getElementById("c").contentEditable = "false"; +} + +</script> +</head> +<body contenteditable="true" id="b" onload="setTimeout(boom, 200);"><div id="c"><input id="d"></div></body> +</html> + diff --git a/editor/libeditor/crashtests/667321-1.html b/editor/libeditor/crashtests/667321-1.html new file mode 100644 index 000000000..275269522 --- /dev/null +++ b/editor/libeditor/crashtests/667321-1.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + document.body.style.cssFloat = "left"; + document.createElement("div").appendChild(document.querySelector("legend")); +} + +</script> +</head> +<body onload="boom();"><fieldset><legend></legend><textarea></textarea></fieldset></body> +</html> diff --git a/editor/libeditor/crashtests/682650-1.html b/editor/libeditor/crashtests/682650-1.html new file mode 100644 index 000000000..66ebc2f62 --- /dev/null +++ b/editor/libeditor/crashtests/682650-1.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function repeatChar(s, n) +{ + while (s.length < n) + s += s; + return s.substr(0, n); +} + +function boom() +{ + document.documentElement.contentEditable = "true"; + document.execCommand("inserthtml", false, "<button><\/button>"); + document.execCommand("inserthtml", false, repeatChar("i", 34646)); + document.execCommand("contentReadOnly", false, null); + document.execCommand("removeformat", false, null); + document.execCommand("hilitecolor", false, "red"); + document.execCommand("inserthtml", false, "a"); + document.execCommand("delete", false, null); +} + +</script> +</head> + +<body onload="boom();"></body> + +</html> diff --git a/editor/libeditor/crashtests/713427-1.html b/editor/libeditor/crashtests/713427-1.html new file mode 100644 index 000000000..21da24693 --- /dev/null +++ b/editor/libeditor/crashtests/713427-1.html @@ -0,0 +1,9 @@ +<span>
+<script contenteditable="true"></script>
+<blockquote>
+<input>
+<code style="display: table-row;">
+<html contenteditable="true">
+</blockquote>
+
+
diff --git a/editor/libeditor/crashtests/713427-2.xhtml b/editor/libeditor/crashtests/713427-2.xhtml new file mode 100644 index 000000000..b04a5d773 --- /dev/null +++ b/editor/libeditor/crashtests/713427-2.xhtml @@ -0,0 +1,28 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +<![CDATA[ + +function boom() +{ + while (document.documentElement.firstChild) { + document.documentElement.removeChild(document.documentElement.firstChild); + } + + var td = document.createElementNS("http://www.w3.org/1999/xhtml", "td"); + td.setAttributeNS(null, "contenteditable", "true"); + (document.documentElement).appendChild(td); + var head = document.createElementNS("http://www.w3.org/1999/xhtml", "head"); + (document.documentElement).appendChild(head); + + head.appendChild(td); +} + +window.addEventListener("load", boom, false); + +]]> +</script> +</head> + +<body></body> +</html> diff --git a/editor/libeditor/crashtests/716456-1.html b/editor/libeditor/crashtests/716456-1.html new file mode 100644 index 000000000..a5ef5a2cc --- /dev/null +++ b/editor/libeditor/crashtests/716456-1.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> + +function boom() +{ + var div = document.querySelector("div"); + div.contentEditable = "true"; + div.focus(); + + var r = document.documentElement; + document["removeChild"](r); + document["appendChild"](r); + + setTimeout(function() { + getSelection().collapse(div, 0); + document.execCommand("inserthtml", false, "a"); + setTimeout(function() { + document.documentElement.removeAttribute("class"); + }, 0); + }, 0); +} + +</script> +</head> + +<body onload="boom();"><div></div></body> +</html> diff --git a/editor/libeditor/crashtests/759748.html b/editor/libeditor/crashtests/759748.html new file mode 100644 index 000000000..1e85a3877 --- /dev/null +++ b/editor/libeditor/crashtests/759748.html @@ -0,0 +1,58 @@ +<!doctype html> +<body> +<script> +var cmds = { + bold: "", + italic: "", + underline: "", + strikethrough: "", + subscript: "", + superscript: "", + cut: "", + copy: "", + paste: "", + delete: "", + forwarddelete: "", + selectall: "", + undo: "", + redo: "", + indent: "", + outdent: "", + backcolor: "#888888", + forecolor: "#888888", + hilitecolor: "#888888", + fontname: "Courier", + fontsize: "6", + increasefontsize: "", + decreasefontsize: "", + inserthorizontalrule: "", + createlink: "foo", + insertimage: "foo", + inserthtml: "foo", + inserttext: "foo", + insertparagraph: "", + gethtml: "", + justifyleft: "", + justifyright: "", + justifycenter: "", + justifyfull: "", + removeformat: "", + unlink: "", + insertorderedlist: "", + insertunorderedlist: "", + formatblock: "h1", + heading: "h1", + stylewithcss: "true", + usecss: "true", + contentreadonly: "true", + readonly: "true", + insertbronreturn: "true", + enableobjectresizing: "true", + enableinlinetableediting: "true", +}; +for (var k in cmds) { + document.body.innerHTML = "<div contenteditable>abc</div>"; + getSelection().removeAllRanges(); + try { document.execCommand(k, false, cmds[k]) } catch(e) {} +} +</script> diff --git a/editor/libeditor/crashtests/761861.html b/editor/libeditor/crashtests/761861.html new file mode 100644 index 000000000..0c1f3f521 --- /dev/null +++ b/editor/libeditor/crashtests/761861.html @@ -0,0 +1,15 @@ +<!doctype html> +<script> +function boom() { + var r = document.documentElement; + while (r.firstChild) { + r.removeChild(r.firstChild); + } + + document.documentElement.contentEditable = "true"; + document.documentElement.appendChild(document.createElement("span")); + document.documentElement.firstChild.appendChild(document.createTextNode("_")); + document.execCommand("forwarddelete"); +} +</script> +<body onload="boom()"> diff --git a/editor/libeditor/crashtests/762183.html b/editor/libeditor/crashtests/762183.html new file mode 100644 index 000000000..1916ac6fb --- /dev/null +++ b/editor/libeditor/crashtests/762183.html @@ -0,0 +1,6 @@ +<body contenteditable=true>x y +<script> +document.body.firstChild.splitText(2).splitText(1).splitText(1); +getSelection().collapse(document.body, 1); +document.execCommand("forwardDelete", false, null); +</script> diff --git a/editor/libeditor/crashtests/766305.html b/editor/libeditor/crashtests/766305.html new file mode 100644 index 000000000..a5000fe73 --- /dev/null +++ b/editor/libeditor/crashtests/766305.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var s = "x"; + for (var i = 0; i < 15; ++i) + s = s + s; + var t = document.createTextNode(s); + document.body.appendChild(t); + window.getSelection().collapse(t, s.length); + document.execCommand("insertText", false, "a"); +} + +</script> +</head> + +<body contenteditable="true" onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/766360.html b/editor/libeditor/crashtests/766360.html new file mode 100644 index 000000000..76c30456d --- /dev/null +++ b/editor/libeditor/crashtests/766360.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var r = document.createRange(); + r.setEnd(document.createTextNode("x"), 0); + window.getSelection().addRange(r); + document.execCommand("inserthtml", false, "y"); +} + +</script> +</head> + +<body contenteditable="true" onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/766387.html b/editor/libeditor/crashtests/766387.html new file mode 100644 index 000000000..20ccc60d4 --- /dev/null +++ b/editor/libeditor/crashtests/766387.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + window.getSelection().removeAllRanges(); + var r = document.createRange(); + r.setStart(document.getElementById("x"), 1); + r.setEnd(document.getElementById("y"), 0); + window.getSelection().addRange(r); + document.execCommand("insertorderedlist", false, null); +} + +</script> +</head> + +<body onload="boom();"><div id="x" contenteditable="true">a</div><div id="y" contenteditable="true"></div></body> +</html> diff --git a/editor/libeditor/crashtests/766413.html b/editor/libeditor/crashtests/766413.html new file mode 100644 index 000000000..c5d9835e3 --- /dev/null +++ b/editor/libeditor/crashtests/766413.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var root = document.documentElement; + while (root.firstChild) { + root.removeChild(root.firstChild); + } + + var space = document.createTextNode(" "); + var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body"); + root.contentEditable = "true"; + root.focus(); + document.execCommand("contentReadOnly", false, null); + root.appendChild(body); + root.contentEditable = "false"; + root.appendChild(space); + root.removeChild(body); + root.contentEditable = "true"; + + window.getSelection().removeAllRanges(); + var r1 = document.createRange(); + r1.setStart(root, 0); + r1.setEnd(root, 0); + window.getSelection().addRange(r1); + looseText = document.createTextNode("c"); + var r2 = document.createRange(); + r2.setStart(looseText, 0); + r2.setEnd(looseText, 0); + window.getSelection().addRange(r2); + + document.execCommand("forwardDelete", false, null); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/766795.html b/editor/libeditor/crashtests/766795.html new file mode 100644 index 000000000..b4ade3020 --- /dev/null +++ b/editor/libeditor/crashtests/766795.html @@ -0,0 +1,21 @@ +<html> +<head> +<script> + +function boom() +{ + var fragEl = document.createElement("span"); + fragEl.setAttribute("contenteditable", "true"); + fragEl.setAttribute("style", "position: absolute;"); + + var frag = document.createDocumentFragment(); + frag.appendChild(fragEl); + + window.getSelection().selectAllChildren(fragEl); +} + +</script> +</head> + +<body contenteditable="true" onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/766845.xhtml b/editor/libeditor/crashtests/766845.xhtml new file mode 100644 index 000000000..409e21010 --- /dev/null +++ b/editor/libeditor/crashtests/766845.xhtml @@ -0,0 +1,27 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +<![CDATA[ + +function boom() +{ + window.getSelection().removeAllRanges(); + var r1 = document.createRange(); + r1.setStart(document.body, 0); + r1.setEnd (document.body, 1); + window.getSelection().addRange(r1); + var r2 = document.createRange(); + r2.setStart(document.body, 1); + r2.setEnd (document.body, 2); + window.getSelection().addRange(r2); + if (document.queryCommandEnabled("inserthtml")) + document.execCommand("inserthtml", false, "1"); +} + +]]> +</script> +</head> + +<body contenteditable="true" onload="boom();"><div></div><div></div></body> + +</html> diff --git a/editor/libeditor/crashtests/767169.html b/editor/libeditor/crashtests/767169.html new file mode 100644 index 000000000..3dfad160c --- /dev/null +++ b/editor/libeditor/crashtests/767169.html @@ -0,0 +1,23 @@ +<html> +<head> +<script> + +// Document must not have a doctype to trigger the bug + +function boom() +{ + var root = document.documentElement; + while (root.firstChild) { root.removeChild(root.firstChild); } + root.contentEditable = "true"; + document.removeChild(root); + document.appendChild(root); + window.getSelection().collapse(root, 0); + window.getSelection().extend(document, 1); + document.removeChild(root); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/768748.html b/editor/libeditor/crashtests/768748.html new file mode 100644 index 000000000..09206dce3 --- /dev/null +++ b/editor/libeditor/crashtests/768748.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html contenteditable="true"> +<head> +<script> + +function boom() +{ + var looseText = document.createTextNode("x"); + window.getSelection().collapse(looseText, 0); + document.queryCommandState("insertorderedlist"); +} + +</script> +</head> +<body onload="setTimeout(boom, 0)"></body> +</html> diff --git a/editor/libeditor/crashtests/768765.html b/editor/libeditor/crashtests/768765.html new file mode 100644 index 000000000..060e5161b --- /dev/null +++ b/editor/libeditor/crashtests/768765.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var root = document.documentElement; + + while (root.firstChild) { root.removeChild(root.firstChild); } + + var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body"); + var div = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + root.contentEditable = "true"; + root.appendChild(div); + root.removeChild(div); + root.insertBefore(body, root.firstChild); + + window.getSelection().removeAllRanges(); + var r0 = document.createRange(); + r0.setStart(body, 0); + r0.setEnd(body, 0); + window.getSelection().addRange(r0); + var r1 = document.createRange(); + r1.setStart(div, 0); + r1.setEnd(div, 0); + window.getSelection().addRange(r1); + + document.execCommand("inserthtml", false, "1"); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/769008-1.html b/editor/libeditor/crashtests/769008-1.html new file mode 100644 index 000000000..8ea8a3601 --- /dev/null +++ b/editor/libeditor/crashtests/769008-1.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var x = document.getElementById("x"); + + window.getSelection().removeAllRanges(); + + var range = document.createRange(); + range.setStart(x, 0); + range.setEnd(x, 0); + window.getSelection().addRange(range); + + document.execCommand("delete", false, "null"); +} + +</script> +</head> +<body contenteditable="true" onload="boom();"><div></div><span id="x"></span></body> +</html> diff --git a/editor/libeditor/crashtests/769967.xhtml b/editor/libeditor/crashtests/769967.xhtml new file mode 100644 index 000000000..724f6b899 --- /dev/null +++ b/editor/libeditor/crashtests/769967.xhtml @@ -0,0 +1,16 @@ +<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true" style="-moz-user-select: all;"><sub>x</sub><script> +function boom() +{ + window.getSelection().removeAllRanges(); + var r = document.createRange(); + r.setStart(document.documentElement, 0); + r.setEnd(document.documentElement, 0); + window.getSelection().addRange(r); + + document.execCommand("subscript", false, null); + document.execCommand("insertText", false, "y"); +} + +window.addEventListener("load", boom, false); + +</script></html> diff --git a/editor/libeditor/crashtests/771749.html b/editor/libeditor/crashtests/771749.html new file mode 100644 index 000000000..9237364f2 --- /dev/null +++ b/editor/libeditor/crashtests/771749.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var root = document.documentElement; + root.contentEditable = "true"; + document.removeChild(root); + document.appendChild(root); + document.execCommand("insertunorderedlist", false, null); + document.execCommand("inserthtml", false, "<span></span>"); + document.execCommand("outdent", false, null); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/772282.html b/editor/libeditor/crashtests/772282.html new file mode 100644 index 000000000..f6259b344 --- /dev/null +++ b/editor/libeditor/crashtests/772282.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var root = document.documentElement; + while(root.firstChild) { root.removeChild(root.firstChild); } + var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body"); + body.setAttributeNS(null, "contenteditable", "true"); + var img = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); + body.appendChild(img); + root.appendChild(body); + document.removeChild(root); + document.appendChild(root); + document.execCommand("insertText", false, "5"); + document.execCommand("selectAll", false, null); + document.execCommand("insertParagraph", false, null); + document.execCommand("increasefontsize", false, null); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/776323.html b/editor/libeditor/crashtests/776323.html new file mode 100644 index 000000000..9fc2776c3 --- /dev/null +++ b/editor/libeditor/crashtests/776323.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html contenteditable="true"> +<head> +<script> + +function boom() +{ + document.execCommand("inserthtml", false, "b"); + var myrange = document.createRange(); + myrange.selectNodeContents(document.getElementsByTagName("img")[0]); + window.getSelection().addRange(myrange); + document.execCommand("strikethrough", false, null); +} + +</script> +</head> +<body onload="boom();"><img></body> +</html> diff --git a/editor/libeditor/crashtests/793866.html b/editor/libeditor/crashtests/793866.html new file mode 100644 index 000000000..4984474db --- /dev/null +++ b/editor/libeditor/crashtests/793866.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var b = document.body; + b.contentEditable = "true"; + document.execCommand("contentReadOnly", false, null); + b.focus(); + b.contentEditable = "false"; + document.documentElement.contentEditable = "true"; + document.createDocumentFragment().appendChild(b); + document.documentElement.focus(); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/editor/libeditor/crashtests/crashtests.list b/editor/libeditor/crashtests/crashtests.list new file mode 100644 index 000000000..3fbc6b196 --- /dev/null +++ b/editor/libeditor/crashtests/crashtests.list @@ -0,0 +1,71 @@ +load 336081-1.xhtml +load 336104.html +load 382527-1.html +load 382778-1.html +load 402172-1.html +load 403965-1.xhtml +load 407074-1.html +load 407079-1.html +load 407256-1.html +load 407277-1.html +load 414178-1.html +load 418923-1.html +load 420439.html +load 428489-1.html +load 429586-1.html +load 430624-1.html +load 431086-1.xhtml +load 448329-1.html +load 448329-2.html +load 448329-3.html +load 456727-1.html +load 456727-2.html +load 459613.html +needs-focus load 467647-1.html +load 475132-1.xhtml +load 499844-1.html +load 503709-1.xhtml +load 513375-1.xhtml +load 535632-1.xhtml +load 574558-1.xhtml +load 580151-1.xhtml +load 582138-1.xhtml +load 612565-1.html +load 615015-1.html +load 615450-1.html +load 633709.xhtml +load 636074-1.html +load 639736-1.xhtml +load 643786-1.html +load 650572-1.html +load 667321-1.html +load 682650-1.html +load 713427-1.html +load 713427-2.xhtml +load 716456-1.html +load 759748.html +load 761861.html +load 762183.html +load 766305.html +load 766360.html +load 766387.html +load 766413.html +load 766795.html +load 766845.xhtml +load 767169.html +load 768748.html +load 768765.html +load 769008-1.html +load 769967.xhtml +needs-focus load 771749.html +load 772282.html +load 776323.html +needs-focus load 793866.html +load 1057677.html +needs-focus load 1128787.html +load 1134545.html +load 1158452.html +load 1158651.html +load 1244894.xhtml +load 1272490.html +load 1317704.html diff --git a/editor/libeditor/moz.build b/editor/libeditor/moz.build new file mode 100644 index 000000000..998ef3d39 --- /dev/null +++ b/editor/libeditor/moz.build @@ -0,0 +1,100 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +MOCHITEST_MANIFESTS += [ + 'tests/browserscope/mochitest.ini', + 'tests/mochitest.ini', +] + +MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini'] + +BROWSER_CHROME_MANIFESTS += ['tests/browser.ini'] + +EXPORTS += [ + 'nsIEditRules.h', +] + +EXPORTS.mozilla += [ + 'ChangeStyleTransaction.h', + 'CSSEditUtils.h', + 'EditorBase.h', + 'EditorController.h', + 'EditorUtils.h', + 'EditTransactionBase.h', + 'HTMLEditor.h', + 'SelectionState.h', + 'TextEditor.h', + 'TextEditRules.h', +] + +UNIFIED_SOURCES += [ + 'ChangeAttributeTransaction.cpp', + 'ChangeStyleTransaction.cpp', + 'CompositionTransaction.cpp', + 'CreateElementTransaction.cpp', + 'CSSEditUtils.cpp', + 'DeleteNodeTransaction.cpp', + 'DeleteRangeTransaction.cpp', + 'DeleteTextTransaction.cpp', + 'EditAggregateTransaction.cpp', + 'EditorBase.cpp', + 'EditorCommands.cpp', + 'EditorController.cpp', + 'EditorEventListener.cpp', + 'EditorUtils.cpp', + 'EditTransactionBase.cpp', + 'HTMLAbsPositionEditor.cpp', + 'HTMLAnonymousNodeEditor.cpp', + 'HTMLEditor.cpp', + 'HTMLEditorDataTransfer.cpp', + 'HTMLEditorEventListener.cpp', + 'HTMLEditorObjectResizer.cpp', + 'HTMLEditRules.cpp', + 'HTMLEditUtils.cpp', + 'HTMLInlineTableEditor.cpp', + 'HTMLStyleEditor.cpp', + 'HTMLTableEditor.cpp', + 'HTMLURIRefObject.cpp', + 'InsertNodeTransaction.cpp', + 'InsertTextTransaction.cpp', + 'InternetCiter.cpp', + 'JoinNodeTransaction.cpp', + 'PlaceholderTransaction.cpp', + 'SelectionState.cpp', + 'SetDocumentTitleTransaction.cpp', + 'SplitNodeTransaction.cpp', + 'StyleSheetTransactions.cpp', + 'TextEditor.cpp', + 'TextEditorDataTransfer.cpp', + 'TextEditorTest.cpp', + 'TextEditRules.cpp', + 'TextEditRulesBidi.cpp', + 'TextEditUtils.cpp', + 'TypeInState.cpp', + 'WSRunObject.cpp', +] + +LOCAL_INCLUDES += [ + '/dom/base', + '/editor/txmgr', + '/extensions/spellcheck/src', + '/layout/generic', + '/layout/style', + '/layout/tables', + '/layout/xul', +] + +EXTRA_COMPONENTS += [ + 'EditorUtils.js', + 'EditorUtils.manifest', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/editor/libeditor/nsIAbsorbingTransaction.h b/editor/libeditor/nsIAbsorbingTransaction.h new file mode 100644 index 000000000..e22caed4a --- /dev/null +++ b/editor/libeditor/nsIAbsorbingTransaction.h @@ -0,0 +1,57 @@ +/* -*- 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/. */ + +#ifndef nsIAbsorbingTransaction_h__ +#define nsIAbsorbingTransaction_h__ + +#include "nsISupports.h" + +/* +Transaction interface to outside world +*/ + +#define NS_IABSORBINGTRANSACTION_IID \ +{ /* a6cf9116-15b3-11d2-932e-00805f8add32 */ \ + 0xa6cf9116, \ + 0x15b3, \ + 0x11d2, \ + {0x93, 0x2e, 0x00, 0x80, 0x5f, 0x8a, 0xdd, 0x32} } + +class nsIAtom; + +namespace mozilla { +class EditorBase; +class SelectionState; +} // namespace mozilla + +/** + * A transaction interface mixin - for transactions that can support. + * the placeholder absorbtion idiom. + */ +class nsIAbsorbingTransaction : public nsISupports{ +public: + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IABSORBINGTRANSACTION_IID) + + NS_IMETHOD Init(nsIAtom* aName, mozilla::SelectionState* aSelState, + mozilla::EditorBase* aEditorBase) = 0; + + NS_IMETHOD EndPlaceHolderBatch()=0; + + NS_IMETHOD GetTxnName(nsIAtom **aName)=0; + + NS_IMETHOD StartSelectionEquals(mozilla::SelectionState* aSelState, + bool* aResult) = 0; + + NS_IMETHOD ForwardEndBatchTo(nsIAbsorbingTransaction *aForwardingAddress)=0; + + NS_IMETHOD Commit()=0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIAbsorbingTransaction, + NS_IABSORBINGTRANSACTION_IID) + +#endif // nsIAbsorbingTransaction_h__ + diff --git a/editor/libeditor/nsIEditRules.h b/editor/libeditor/nsIEditRules.h new file mode 100644 index 000000000..b186895ae --- /dev/null +++ b/editor/libeditor/nsIEditRules.h @@ -0,0 +1,68 @@ +/* -*- 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/. */ + +#ifndef nsIEditRules_h +#define nsIEditRules_h + +#define NS_IEDITRULES_IID \ +{ 0x3836386d, 0x806a, 0x488d, \ + { 0x8b, 0xab, 0xaf, 0x42, 0xbb, 0x4c, 0x90, 0x66 } } + +#include "mozilla/EditorBase.h" // for EditAction enum + +namespace mozilla { + +class TextEditor; +namespace dom { +class Selection; +} // namespace dom + +/** + * Base for an object to encapsulate any additional info needed to be passed + * to rules system by the editor. + */ +class RulesInfo +{ +public: + explicit RulesInfo(EditAction aAction) + : action(aAction) + {} + virtual ~RulesInfo() {} + + EditAction action; +}; + +} // namespace mozilla + +/** + * Interface of editing rules. + */ +class nsIEditRules : public nsISupports +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IEDITRULES_IID) + +//Interfaces for addref and release and queryinterface +//NOTE: Use NS_DECL_ISUPPORTS_INHERITED in any class inherited from nsIEditRules + + NS_IMETHOD Init(mozilla::TextEditor* aTextEditor) = 0; + NS_IMETHOD SetInitialValue(const nsAString& aValue) = 0; + NS_IMETHOD DetachEditor() = 0; + NS_IMETHOD BeforeEdit(EditAction action, + nsIEditor::EDirection aDirection) = 0; + NS_IMETHOD AfterEdit(EditAction action, + nsIEditor::EDirection aDirection) = 0; + NS_IMETHOD WillDoAction(mozilla::dom::Selection* aSelection, + mozilla::RulesInfo* aInfo, bool* aCancel, + bool* aHandled) = 0; + NS_IMETHOD DidDoAction(mozilla::dom::Selection* aSelection, + mozilla::RulesInfo* aInfo, nsresult aResult) = 0; + NS_IMETHOD DocumentIsEmpty(bool* aDocumentIsEmpty) = 0; + NS_IMETHOD DocumentModified() = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIEditRules, NS_IEDITRULES_IID) + +#endif // #ifndef nsIEditRules_h diff --git a/editor/libeditor/tests/browser.ini b/editor/libeditor/tests/browser.ini new file mode 100644 index 000000000..249f59aa8 --- /dev/null +++ b/editor/libeditor/tests/browser.ini @@ -0,0 +1,6 @@ +[browser_bug527935.js] +skip-if = toolkit == 'android' +support-files = bug527935.html +[browser_bug629172.js] +skip-if = toolkit == 'android' +support-files = bug629172.html diff --git a/editor/libeditor/tests/browser_bug527935.js b/editor/libeditor/tests/browser_bug527935.js new file mode 100644 index 000000000..dc6e74d3e --- /dev/null +++ b/editor/libeditor/tests/browser_bug527935.js @@ -0,0 +1,63 @@ +add_task(function*() { + yield new Promise(resolve => waitForFocus(resolve, window)); + + const kPageURL = "http://example.org/browser/editor/libeditor/tests/bug527935.html"; + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: kPageURL + }, function*(aBrowser) { + var popupShown = false; + function listener() { + popupShown = true; + } + SpecialPowers.addAutoCompletePopupEventListener(window, "popupshowing", listener); + + yield ContentTask.spawn(aBrowser, {}, function*() { + var window = content.window.wrappedJSObject; + var document = window.document; + var formTarget = document.getElementById("formTarget"); + var initValue = document.getElementById("initValue"); + + window.loadPromise = new Promise(resolve => { + formTarget.onload = resolve; + }); + + initValue.focus(); + initValue.value = "foo"; + }); + + EventUtils.synthesizeKey("VK_RETURN", {}); + + yield ContentTask.spawn(aBrowser, {}, function*() { + var window = content.window.wrappedJSObject; + var document = window.document; + + yield window.loadPromise; + + var newInput = document.createElement("input"); + newInput.setAttribute("name", "test"); + document.body.appendChild(newInput); + + var event = document.createEvent("KeyboardEvent"); + + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "f".charCodeAt(0)); + newInput.value = ""; + newInput.focus(); + newInput.dispatchEvent(event); + }); + + yield new Promise(resolve => hitEventLoop(resolve, 100)); + + ok(!popupShown, "Popup must not be opened"); + SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshowing", listener); + }); +}); + +function hitEventLoop(func, times) { + if (times > 0) { + setTimeout(hitEventLoop, 0, func, times - 1); + } else { + setTimeout(func, 0); + } +} diff --git a/editor/libeditor/tests/browser_bug629172.js b/editor/libeditor/tests/browser_bug629172.js new file mode 100644 index 000000000..0c4f34069 --- /dev/null +++ b/editor/libeditor/tests/browser_bug629172.js @@ -0,0 +1,106 @@ +add_task(function*() { + yield new Promise(resolve => waitForFocus(resolve, window)); + + const kPageURL = "http://example.org/browser/editor/libeditor/tests/bug629172.html"; + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: kPageURL + }, function*(aBrowser) { + yield ContentTask.spawn(aBrowser, {}, function*() { + var window = content.window.wrappedJSObject; + var document = window.document; + + // Note: Using the with keyword, we would have been able to write this as: + // + // with (window) { + // Screenshots = {}; + // // so on + // } + // + // However, browser-chrome tests are somehow forced to run in strict mode, + // which doesn't permit the usage of the with keyword, turning the following + // into the ugliest test you've ever seen. :( + var LTRRef = document.getElementById("ltr-ref"); + var RTLRef = document.getElementById("rtl-ref"); + window.Screenshots = {}; + + // generate the reference screenshots + LTRRef.style.display = ""; + document.body.clientWidth; + window.Screenshots.ltr = window.snapshotWindow(window); + LTRRef.parentNode.removeChild(LTRRef); + RTLRef.style.display = ""; + document.body.clientWidth; + window.Screenshots.rtl = window.snapshotWindow(window); + RTLRef.parentNode.removeChild(RTLRef); + window.Screenshots.get = function(dir, flip) { + if (flip) { + return this[dir == "rtl" ? "ltr" : "rtl"]; + } else { + return this[dir]; + } + }; + }); + + function simulateCtrlShiftX(aBrowser) { + // In e10s, the keypress event will be dispatched to the content process, + // but in non-e10s it is handled by the browser UI code and hence won't + // reach the web page. As a result, we need to observe the event in + // the content process only in e10s mode. + var waitForKeypressContent = BrowserTestUtils.waitForContentEvent(aBrowser, "keypress"); + EventUtils.synthesizeKey("x", {accelKey: true, shiftKey: true}); + if (gMultiProcessBrowser) { + return waitForKeypressContent; + } + return Promise.resolve(); + } + + function* testDirection(initialDir, aBrowser) { + yield ContentTask.spawn(aBrowser, {initialDir}, function({initialDir}) { + var window = content.window.wrappedJSObject; + var document = window.document; + + var t = window.t = document.createElement("textarea"); + t.setAttribute("dir", initialDir); + t.value = "test."; + window.inputEventCount = 0; + t.oninput = function() { window.inputEventCount++; }; + document.getElementById("content").appendChild(t); + document.body.clientWidth; + var s1 = window.snapshotWindow(window); + ok(window.compareSnapshots(s1, window.Screenshots.get(initialDir, false), true)[0], + "Textarea should appear correctly before switching the direction (" + initialDir + ")"); + t.focus(); + is(window.inputEventCount, 0, "input event count must be 0 before"); + }); + yield simulateCtrlShiftX(aBrowser); + yield ContentTask.spawn(aBrowser, {initialDir}, function({initialDir}) { + var window = content.window.wrappedJSObject; + + is(window.t.getAttribute("dir"), initialDir == "ltr" ? "rtl" : "ltr", "The dir attribute must be correctly updated"); + is(window.inputEventCount, 1, "input event count must be 1 after"); + window.t.blur(); + var s2 = window.snapshotWindow(window); + ok(window.compareSnapshots(s2, window.Screenshots.get(initialDir, true), true)[0], + "Textarea should appear correctly after switching the direction (" + initialDir + ")"); + window.t.focus(); + is(window.inputEventCount, 1, "input event count must be 1 before"); + }); + yield simulateCtrlShiftX(aBrowser); + yield ContentTask.spawn(aBrowser, {initialDir}, function({initialDir}) { + var window = content.window.wrappedJSObject; + + is(window.inputEventCount, 2, "input event count must be 2 after"); + is(window.t.getAttribute("dir"), initialDir == "ltr" ? "ltr" : "rtl", "The dir attribute must be correctly updated"); + window.t.blur(); + var s3 = window.snapshotWindow(window); + ok(window.compareSnapshots(s3, window.Screenshots.get(initialDir, false), true)[0], + "Textarea should appear correctly after switching back the direction (" + initialDir + ")"); + window.t.parentNode.removeChild(window.t); + }); + } + + yield testDirection("ltr", aBrowser); + yield testDirection("rtl", aBrowser); + }); +}); diff --git a/editor/libeditor/tests/browserscope/lib/richtext/LICENSE b/editor/libeditor/tests/browserscope/lib/richtext/LICENSE new file mode 100644 index 000000000..57bc88a15 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/editor/libeditor/tests/browserscope/lib/richtext/README b/editor/libeditor/tests/browserscope/lib/richtext/README new file mode 100644 index 000000000..a3bc3110f --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/README @@ -0,0 +1,58 @@ +README FOR BROWSERSCOPE +----------------------- + +Hey there - thanks for downloading the code. This file has instructions +for getting setup so that you can run the codebase locally. + +This project is built on Google App Engine using the +Django web application framework and written in Python. + +To get started, you'll need to first download the App Engine SDK at: +http://code.google.com/appengine/downloads.html + +For local development, just startup the server: +./pathto/google_appengine/dev_appserver.py --port=8080 browserscope + +You should then be able to access the local application at: +http://localhost:8080/ + +Note: the first time you hit the homepage it may take a little +while - that's because it's trying to read out median times for all +of the tests from a nonexistent datastore and write to memcache. +Just be a lil patient. + +You can run the unit tests at: + http://localhost:8080/test + + +CONTRIBUTING +------------------ + +Most likely you are interested in adding new tests or creating +a new test category. If you are interested in adding tests to an existing +"category" you may want to get in touch with the maintainer for that +branch of the tree. We are really looking forward to receiving your +code in patch format. Currently the category maintainers are: +Network: Steve Souders <souders@gmail.com> +Reflow: Lindsey Simon <elsigh@gmail.com> +Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com> + + +To create a completely new test category: + * Copy one of the existing directories in categories/ + * Edit your test_set.py, handlers.py + * Add your files in templates/ and static/ + * Update urls.py and settings.CATEGORIES + * Follow the examples of other tests re: + * beaconing using/testdriver_base + * your GetScoreAndDisplayValue method + * your GetRowScoreAndDisplayValue method + +References: + * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html + * App Engine Group - http://groups.google.com/group/google-appengine + * Python Docs - http://www.python.org/doc/ + * Django - http://www.djangoproject.com/ + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla b/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla new file mode 100644 index 000000000..5d304943f --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla @@ -0,0 +1,17 @@ +The BrowserScope project provides a set of cross-browser HTML editor tests, +which we import in our test suite in order to run them as part of our +continuous integration system. + +We pull tests occasionally from their Subversion repository using the pull +script which can be found in this directory. We also record the revision ID +which we've used in the current_revision file inside this directory. + +Using the pull script is quite easy, just switch to this directory, and say: + +sh update_from_upstream + +There are tests which we're currently failing on, and there will probably be +more of those in the future. We should maintain a list of the failing tests +manually in currentStatus.js (which can also be found in this directory), to +make sure that the suite passes entirely, with failing tests marked as todo +items. diff --git a/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js new file mode 100644 index 000000000..b30775d04 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js @@ -0,0 +1,46 @@ +/** + * This file lists the tests in the BrowserScope suite which we are currently + * failing. We mark them as todo items to keep track of them. + */ + +var knownFailures = { + // Dummy result items. There is one for each category. + 'apply' : { + '0-undefined' : true + }, + 'unapply' : { + '0-undefined' : true + }, + 'change' : { + '0-undefined' : true + }, + 'query' : { + '0-undefined' : true + }, + 'a' : { + 'createbookmark-0' : true, + 'fontsize-1' : true, + 'subscript-1' : true, + 'superscript-1' : true, + }, + 'u': { + 'removeformat-1' : true, + 'removeformat-2' : true, + 'strikethrough-2' : true, + 'subscript-1' : true, + 'superscript-1' : true, + 'unbookmark-0' : true, + }, + 'q': { + 'fontsize-1' : true, + 'fontsize-2' : true, + }, + 'c': { + 'fontsize-1' : true, + 'fontsize-2' : true, + }, +}; + +function isKnownFailure(type, test, param) { + return (type in knownFailures) && ((test + "-" + param) in knownFailures[type]); +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext/current_revision b/editor/libeditor/tests/browserscope/lib/richtext/current_revision new file mode 100644 index 000000000..1e2569914 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/current_revision @@ -0,0 +1 @@ +775 diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html b/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html new file mode 100644 index 000000000..a294f0b56 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html @@ -0,0 +1,11 @@ +<html>
+<head>
+ <script>
+ function load(){
+ window.document.designMode = "On";
+ }
+ </script>
+</head>
+<body contentEditable="true" onload="load()">
+</body>
+</html>
\ No newline at end of file diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js b/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js new file mode 100644 index 000000000..3e4463e11 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js @@ -0,0 +1,1069 @@ +var goog$global = this, goog$isString = function(val) { + return typeof val == "string" +}; +Math.floor(Math.random() * 2147483648).toString(36); +var goog$now = Date.now || function() { + return(new Date).getTime() +}, goog$inherits = function(childCtor, parentCtor) { + function tempCtor() { + } + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor +};var goog$array$peek = function(array) { + return array[array.length - 1] +}, goog$array$indexOf = function(arr, obj, opt_fromIndex) { + if(arr.indexOf)return arr.indexOf(obj, opt_fromIndex); + if(Array.indexOf)return Array.indexOf(arr, obj, opt_fromIndex); + for(var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex, i = fromIndex;i < arr.length;i++)if(i in arr && arr[i] === obj)return i; + return-1 +}, goog$array$map = function(arr, f, opt_obj) { + if(arr.map)return arr.map(f, opt_obj); + if(Array.map)return Array.map(arr, f, opt_obj); + for(var l = arr.length, res = [], resLength = 0, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2)res[resLength++] = f.call(opt_obj, arr2[i], i, arr); + return res +}, goog$array$some = function(arr, f, opt_obj) { + if(arr.some)return arr.some(f, opt_obj); + if(Array.some)return Array.some(arr, f, opt_obj); + for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && f.call(opt_obj, arr2[i], i, arr))return true; + return false +}, goog$array$every = function(arr, f, opt_obj) { + if(arr.every)return arr.every(f, opt_obj); + if(Array.every)return Array.every(arr, f, opt_obj); + for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr))return false; + return true +}, goog$array$find = function(arr, f, opt_obj) { + var i; + JSCompiler_inline_label_goog$array$findIndex_12: { + for(var JSCompiler_inline_l = arr.length, JSCompiler_inline_arr2 = goog$isString(arr) ? arr.split("") : arr, JSCompiler_inline_i = 0;JSCompiler_inline_i < JSCompiler_inline_l;JSCompiler_inline_i++)if(JSCompiler_inline_i in JSCompiler_inline_arr2 && f.call(opt_obj, JSCompiler_inline_arr2[JSCompiler_inline_i], JSCompiler_inline_i, arr)) { + i = JSCompiler_inline_i; + break JSCompiler_inline_label_goog$array$findIndex_12 + }i = -1 + }return i < 0 ? null : goog$isString(arr) ? arr.charAt(i) : arr[i] +};var goog$string$trim = function(str) { + return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "") +}, goog$string$htmlEscape = function(str, opt_isLikelyToContainHtmlChars) { + if(opt_isLikelyToContainHtmlChars)return str.replace(goog$string$amperRe_, "&").replace(goog$string$ltRe_, "<").replace(goog$string$gtRe_, ">").replace(goog$string$quotRe_, """); + else { + if(!goog$string$allRe_.test(str))return str; + if(str.indexOf("&") != -1)str = str.replace(goog$string$amperRe_, "&"); + if(str.indexOf("<") != -1)str = str.replace(goog$string$ltRe_, "<"); + if(str.indexOf(">") != -1)str = str.replace(goog$string$gtRe_, ">"); + if(str.indexOf('"') != -1)str = str.replace(goog$string$quotRe_, """); + return str + } +}, goog$string$amperRe_ = /&/g, goog$string$ltRe_ = /</g, goog$string$gtRe_ = />/g, goog$string$quotRe_ = /\"/g, goog$string$allRe_ = /[&<>\"]/, goog$string$contains = function(s, ss) { + return s.indexOf(ss) != -1 +}, goog$string$compareVersions = function(version1, version2) { + for(var order = 0, v1Subs = goog$string$trim(String(version1)).split("."), v2Subs = goog$string$trim(String(version2)).split("."), subCount = Math.max(v1Subs.length, v2Subs.length), subIdx = 0;order == 0 && subIdx < subCount;subIdx++) { + var v1Sub = v1Subs[subIdx] || "", v2Sub = v2Subs[subIdx] || "", v1CompParser = new RegExp("(\\d*)(\\D*)", "g"), v2CompParser = new RegExp("(\\d*)(\\D*)", "g"); + do { + var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""], v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""]; + if(v1Comp[0].length == 0 && v2Comp[0].length == 0)break; + var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10), v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10); + order = goog$string$compareElements_(v1CompNum, v2CompNum) || goog$string$compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog$string$compareElements_(v1Comp[2], v2Comp[2]) + }while(order == 0) + }return order +}, goog$string$compareElements_ = function(left, right) { + if(left < right)return-1; + else if(left > right)return 1; + return 0 +}; +goog$now();var goog$userAgent$detectedOpera_, goog$userAgent$detectedIe_, goog$userAgent$detectedWebkit_, goog$userAgent$detectedMobile_, goog$userAgent$detectedGecko_, goog$userAgent$detectedCamino_, goog$userAgent$detectedMac_, goog$userAgent$detectedWindows_, goog$userAgent$detectedLinux_, goog$userAgent$detectedX11_, goog$userAgent$getUserAgentString = function() { + return goog$global.navigator ? goog$global.navigator.userAgent : null +}, goog$userAgent$getNavigator = function() { + return goog$global.navigator +}; +goog$userAgent$detectedCamino_ = goog$userAgent$detectedGecko_ = goog$userAgent$detectedMobile_ = goog$userAgent$detectedWebkit_ = goog$userAgent$detectedIe_ = goog$userAgent$detectedOpera_ = false; +var JSCompiler_inline_ua_15; +if(JSCompiler_inline_ua_15 = goog$userAgent$getUserAgentString()) { + var JSCompiler_inline_navigator$$1_16 = goog$userAgent$getNavigator(); + goog$userAgent$detectedOpera_ = JSCompiler_inline_ua_15.indexOf("Opera") == 0; + goog$userAgent$detectedIe_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.indexOf("MSIE") != -1; + goog$userAgent$detectedMobile_ = (goog$userAgent$detectedWebkit_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.indexOf("WebKit") != -1) && JSCompiler_inline_ua_15.indexOf("Mobile") != -1; + goog$userAgent$detectedCamino_ = (goog$userAgent$detectedGecko_ = !goog$userAgent$detectedOpera_ && !goog$userAgent$detectedWebkit_ && JSCompiler_inline_navigator$$1_16.product == "Gecko") && JSCompiler_inline_navigator$$1_16.vendor == "Camino" +}var goog$userAgent$OPERA = goog$userAgent$detectedOpera_, goog$userAgent$IE = goog$userAgent$detectedIe_, goog$userAgent$GECKO = goog$userAgent$detectedGecko_, goog$userAgent$WEBKIT = goog$userAgent$detectedWebkit_, goog$userAgent$MOBILE = goog$userAgent$detectedMobile_, goog$userAgent$PLATFORM, JSCompiler_inline_navigator$$2_19 = goog$userAgent$getNavigator(); +goog$userAgent$PLATFORM = JSCompiler_inline_navigator$$2_19 && JSCompiler_inline_navigator$$2_19.platform || ""; +goog$userAgent$detectedMac_ = goog$string$contains(goog$userAgent$PLATFORM, "Mac"); +goog$userAgent$detectedWindows_ = goog$string$contains(goog$userAgent$PLATFORM, "Win"); +goog$userAgent$detectedLinux_ = goog$string$contains(goog$userAgent$PLATFORM, "Linux"); +goog$userAgent$detectedX11_ = !!goog$userAgent$getNavigator() && goog$string$contains(goog$userAgent$getNavigator().appVersion || "", "X11"); +var goog$userAgent$VERSION, JSCompiler_inline_version$$6_26 = "", JSCompiler_inline_re$$2_27; +if(goog$userAgent$OPERA && goog$global.opera) { + var JSCompiler_inline_operaVersion_28 = goog$global.opera.version; + JSCompiler_inline_version$$6_26 = typeof JSCompiler_inline_operaVersion_28 == "function" ? JSCompiler_inline_operaVersion_28() : JSCompiler_inline_operaVersion_28 +}else { + if(goog$userAgent$GECKO)JSCompiler_inline_re$$2_27 = /rv\:([^\);]+)(\)|;)/; + else if(goog$userAgent$IE)JSCompiler_inline_re$$2_27 = /MSIE\s+([^\);]+)(\)|;)/; + else if(goog$userAgent$WEBKIT)JSCompiler_inline_re$$2_27 = /WebKit\/(\S+)/; + if(JSCompiler_inline_re$$2_27) { + var JSCompiler_inline_arr$$41_29 = JSCompiler_inline_re$$2_27.exec(goog$userAgent$getUserAgentString()); + JSCompiler_inline_version$$6_26 = JSCompiler_inline_arr$$41_29 ? JSCompiler_inline_arr$$41_29[1] : "" + } +}goog$userAgent$VERSION = JSCompiler_inline_version$$6_26; +var goog$userAgent$isVersionCache_ = {}, goog$userAgent$isVersion = function(version) { + return goog$userAgent$isVersionCache_[version] || (goog$userAgent$isVersionCache_[version] = goog$string$compareVersions(goog$userAgent$VERSION, version) >= 0) +};var goog$dom$getWindow = function(opt_doc) { + return opt_doc ? goog$dom$getWindow_(opt_doc) : window +}, goog$dom$getWindow_ = function(doc) { + if(doc.parentWindow)return doc.parentWindow; + if(goog$userAgent$WEBKIT && !goog$userAgent$isVersion("500") && !goog$userAgent$MOBILE) { + var scriptElement = doc.createElement("script"); + scriptElement.innerHTML = "document.parentWindow=window"; + var parentElement = doc.documentElement; + parentElement.appendChild(scriptElement); + parentElement.removeChild(scriptElement); + return doc.parentWindow + }return doc.defaultView +}, goog$dom$appendChild = function(parent, child) { + parent.appendChild(child) +}, goog$dom$BAD_CONTAINS_WEBKIT_ = goog$userAgent$WEBKIT && goog$userAgent$isVersion("522"), goog$dom$contains = function(parent, descendant) { + if(typeof parent.contains != "undefined" && !goog$dom$BAD_CONTAINS_WEBKIT_ && descendant.nodeType == 1)return parent == descendant || parent.contains(descendant); + if(typeof parent.compareDocumentPosition != "undefined")return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16); + for(;descendant && parent != descendant;)descendant = descendant.parentNode; + return descendant == parent +}, goog$dom$compareNodeOrder = function(node1, node2) { + if(node1 == node2)return 0; + if(node1.compareDocumentPosition)return node1.compareDocumentPosition(node2) & 2 ? 1 : -1; + if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) { + var isElement1 = node1.nodeType == 1, isElement2 = node2.nodeType == 1; + if(isElement1 && isElement2)return node1.sourceIndex - node2.sourceIndex; + else { + var parent1 = node1.parentNode, parent2 = node2.parentNode; + if(parent1 == parent2)return goog$dom$compareSiblingOrder_(node1, node2); + if(!isElement1 && goog$dom$contains(parent1, node2))return-1 * goog$dom$compareParentsDescendantNodeIe_(node1, node2); + if(!isElement2 && goog$dom$contains(parent2, node1))return goog$dom$compareParentsDescendantNodeIe_(node2, node1); + return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex) + } + }var doc = goog$dom$getOwnerDocument(node1), range1, range2; + range1 = doc.createRange(); + range1.selectNode(node1); + range1.collapse(true); + range2 = doc.createRange(); + range2.selectNode(node2); + range2.collapse(true); + return range1.compareBoundaryPoints(goog$global.Range.START_TO_END, range2) +}, goog$dom$compareParentsDescendantNodeIe_ = function(textNode, node) { + var parent = textNode.parentNode; + if(parent == node)return-1; + for(var sibling = node;sibling.parentNode != parent;)sibling = sibling.parentNode; + return goog$dom$compareSiblingOrder_(sibling, textNode) +}, goog$dom$compareSiblingOrder_ = function(node1, node2) { + for(var s = node2;s = s.previousSibling;)if(s == node1)return-1; + return 1 +}, goog$dom$findCommonAncestor = function() { + var i, count = arguments.length; + if(count) { + if(count == 1)return arguments[0] + }else return null; + var paths = [], minLength = Infinity; + for(i = 0;i < count;i++) { + for(var ancestors = [], node = arguments[i];node;) { + ancestors.unshift(node); + node = node.parentNode + }paths.push(ancestors); + minLength = Math.min(minLength, ancestors.length) + }var output = null; + for(i = 0;i < minLength;i++) { + for(var first = paths[0][i], j = 1;j < count;j++)if(first != paths[j][i])return output; + output = first + }return output +}, goog$dom$getOwnerDocument = function(node) { + // Added 'editorDoc' as hack for browsers that don't support node.ownerDocument + return node.nodeType == 9 ? node : node.ownerDocument || node.document || editorDoc +}, goog$dom$DomHelper = function(opt_document) { + this.document_ = opt_document || goog$global.document || document +}; +goog$dom$DomHelper.prototype.getDocument = function() { + return this.document_ +}; +goog$dom$DomHelper.prototype.createElement = function(name) { + return this.document_.createElement(name) +}; +goog$dom$DomHelper.prototype.getWindow = function() { + return goog$dom$getWindow_(this.document_) +}; +goog$dom$DomHelper.prototype.appendChild = goog$dom$appendChild; +goog$dom$DomHelper.prototype.contains = goog$dom$contains;var goog$Disposable = function() { +};if("StopIteration" in goog$global)var goog$iter$StopIteration = goog$global.StopIteration; +else goog$iter$StopIteration = Error("StopIteration"); +var goog$iter$Iterator = function() { +}; +goog$iter$Iterator.prototype.next = function() { + throw goog$iter$StopIteration; +}; +goog$iter$Iterator.prototype.__iterator__ = function() { + return this +};var goog$debug$exposeException = function(err, opt_fn) { + try { + var e, JSCompiler_inline_href_34; + JSCompiler_inline_label_goog$getObjectByName_61: { + for(var JSCompiler_inline_parts = "window.location.href".split("."), JSCompiler_inline_cur = goog$global, JSCompiler_inline_part;JSCompiler_inline_part = JSCompiler_inline_parts.shift();)if(JSCompiler_inline_cur[JSCompiler_inline_part])JSCompiler_inline_cur = JSCompiler_inline_cur[JSCompiler_inline_part]; + else { + JSCompiler_inline_href_34 = null; + break JSCompiler_inline_label_goog$getObjectByName_61 + }JSCompiler_inline_href_34 = JSCompiler_inline_cur + }e = typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:JSCompiler_inline_href_34, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || JSCompiler_inline_href_34, stack:err.stack || "Not available"} : err; + var error = "Message: " + goog$string$htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog$string$htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog$string$htmlEscape(goog$debug$getStacktrace(opt_fn) + "-> "); + return error + }catch(e2) { + return"Exception trying to expose exception! You win, we lose. " + e2 + } +}, goog$debug$getStacktrace = function(opt_fn) { + return goog$debug$getStacktraceHelper_(opt_fn || arguments.callee.caller, []) +}, goog$debug$getStacktraceHelper_ = function(fn, visited) { + var sb = [], JSCompiler_inline_result_36; + JSCompiler_inline_label_goog$array$contains_41:JSCompiler_inline_result_36 = visited.contains ? visited.contains(fn) : goog$array$indexOf(visited, fn) > -1; + if(JSCompiler_inline_result_36)sb.push("[...circular reference...]"); + else if(fn && visited.length < 50) { + sb.push(goog$debug$getFunctionName(fn) + "("); + for(var args = fn.arguments, i = 0;i < args.length;i++) { + i > 0 && sb.push(", "); + var argDesc, arg = args[i]; + switch(typeof arg) { + case "object": + argDesc = arg ? "object" : "null"; + break; + case "string": + argDesc = arg; + break; + case "number": + argDesc = String(arg); + break; + case "boolean": + argDesc = arg ? "true" : "false"; + break; + case "function": + argDesc = (argDesc = goog$debug$getFunctionName(arg)) ? argDesc : "[fn]"; + break; + case "undefined": + ; + default: + argDesc = typeof arg; + break + } + if(argDesc.length > 40)argDesc = argDesc.substr(0, 40) + "..."; + sb.push(argDesc) + }visited.push(fn); + sb.push(")\n"); + try { + sb.push(goog$debug$getStacktraceHelper_(fn.caller, visited)) + }catch(e) { + sb.push("[exception trying to get caller]\n") + } + }else fn ? sb.push("[...long stack...]") : sb.push("[end]"); + return sb.join("") +}, goog$debug$getFunctionName = function(fn) { + var functionSource = String(fn); + if(!goog$debug$fnNameCache_[functionSource]) { + var matches = /function ([^\(]+)/.exec(functionSource); + if(matches) { + var method = matches[1]; + goog$debug$fnNameCache_[functionSource] = method + }else goog$debug$fnNameCache_[functionSource] = "[Anonymous]" + }return goog$debug$fnNameCache_[functionSource] +}, goog$debug$fnNameCache_ = {};var goog$debug$LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) { + this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog$debug$LogRecord$nextSequenceNumber_++; + this.time_ = opt_time || goog$now(); + this.level_ = level; + this.msg_ = msg; + this.loggerName_ = loggerName +}; +goog$debug$LogRecord.prototype.exception_ = null; +goog$debug$LogRecord.prototype.exceptionText_ = null; +var goog$debug$LogRecord$nextSequenceNumber_ = 0; +goog$debug$LogRecord.prototype.setException = function(exception) { + this.exception_ = exception +}; +goog$debug$LogRecord.prototype.setExceptionText = function(text) { + this.exceptionText_ = text +}; +goog$debug$LogRecord.prototype.setLevel = function(level) { + this.level_ = level +};var goog$debug$Logger = function(name) { + this.name_ = name; + this.parent_ = null; + this.children_ = {}; + this.handlers_ = [] +}; +goog$debug$Logger.prototype.level_ = null; +var goog$debug$Logger$Level = function(name, value) { + this.name = name; + this.value = value +}; +goog$debug$Logger$Level.prototype.toString = function() { + return this.name +}; +new goog$debug$Logger$Level("OFF", Infinity); +new goog$debug$Logger$Level("SHOUT", 1200); +var goog$debug$Logger$Level$SEVERE = new goog$debug$Logger$Level("SEVERE", 1000), goog$debug$Logger$Level$WARNING = new goog$debug$Logger$Level("WARNING", 900); +new goog$debug$Logger$Level("INFO", 800); +var goog$debug$Logger$Level$CONFIG = new goog$debug$Logger$Level("CONFIG", 700); +new goog$debug$Logger$Level("FINE", 500); +new goog$debug$Logger$Level("FINER", 400); +new goog$debug$Logger$Level("FINEST", 300); +new goog$debug$Logger$Level("ALL", 0); +goog$debug$Logger.prototype.setLevel = function(level) { + this.level_ = level +}; +goog$debug$Logger.prototype.isLoggable = function(level) { + if(this.level_)return level.value >= this.level_.value; + if(this.parent_)return this.parent_.isLoggable(level); + return false +}; +goog$debug$Logger.prototype.log = function(level, msg, opt_exception) { + this.isLoggable(level) && this.logRecord(this.getLogRecord(level, msg, opt_exception)) +}; +goog$debug$Logger.prototype.getLogRecord = function(level, msg, opt_exception) { + var logRecord = new goog$debug$LogRecord(level, String(msg), this.name_); + if(opt_exception) { + logRecord.setException(opt_exception); + logRecord.setExceptionText(goog$debug$exposeException(opt_exception, arguments.callee.caller)) + }return logRecord +}; +goog$debug$Logger.prototype.severe = function(msg, opt_exception) { + this.log(goog$debug$Logger$Level$SEVERE, msg, opt_exception) +}; +goog$debug$Logger.prototype.warning = function(msg, opt_exception) { + this.log(goog$debug$Logger$Level$WARNING, msg, opt_exception) +}; +goog$debug$Logger.prototype.logRecord = function(logRecord) { + if(this.isLoggable(logRecord.level_))for(var target = this;target;) { + target.callPublish_(logRecord); + target = target.parent_ + } +}; +goog$debug$Logger.prototype.callPublish_ = function(logRecord) { + for(var i = 0;i < this.handlers_.length;i++)this.handlers_[i](logRecord) +}; +goog$debug$Logger.prototype.setParent_ = function(parent) { + this.parent_ = parent +}; +goog$debug$Logger.prototype.addChild_ = function(name, logger) { + this.children_[name] = logger +}; +var goog$debug$LogManager$loggers_ = {}, goog$debug$LogManager$rootLogger_ = null, goog$debug$LogManager$getLogger = function(name) { + if(!goog$debug$LogManager$rootLogger_) { + goog$debug$LogManager$rootLogger_ = new goog$debug$Logger(""); + goog$debug$LogManager$loggers_[""] = goog$debug$LogManager$rootLogger_; + goog$debug$LogManager$rootLogger_.setLevel(goog$debug$Logger$Level$CONFIG) + }return name in goog$debug$LogManager$loggers_ ? goog$debug$LogManager$loggers_[name] : goog$debug$LogManager$createLogger_(name) +}, goog$debug$LogManager$createLogger_ = function(name) { + var logger = new goog$debug$Logger(name), parts = name.split("."), leafName = parts[parts.length - 1]; + parts.length = parts.length - 1; + var parentName = parts.join("."), parentLogger = goog$debug$LogManager$getLogger(parentName); + parentLogger.addChild_(leafName, logger); + logger.setParent_(parentLogger); + return goog$debug$LogManager$loggers_[name] = logger +};var goog$dom$SavedRange = function() { + goog$Disposable.call(this) +}; +goog$inherits(goog$dom$SavedRange, goog$Disposable); +goog$debug$LogManager$getLogger("goog.dom.SavedRange");var goog$dom$TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) { + this.reversed = !!opt_reversed; + opt_node && this.setPosition(opt_node, opt_tagType); + this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0; + if(this.reversed)this.depth *= -1; + this.constrained = !opt_unconstrained +}; +goog$inherits(goog$dom$TagIterator, goog$iter$Iterator); +goog$dom$TagIterator.prototype.node = null; +goog$dom$TagIterator.prototype.tagType = null; +goog$dom$TagIterator.prototype.started_ = false; +goog$dom$TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) { + if(this.node = node)this.tagType = typeof opt_tagType == "number" ? opt_tagType : this.node.nodeType != 1 ? 0 : this.reversed ? -1 : 1; + if(typeof opt_depth == "number")this.depth = opt_depth +}; +goog$dom$TagIterator.prototype.next = function() { + var node; + if(this.started_) { + if(!this.node || this.constrained && this.depth == 0)throw goog$iter$StopIteration;node = this.node; + var startType = this.reversed ? -1 : 1; + if(this.tagType == startType) { + var child = this.reversed ? node.lastChild : node.firstChild; + child ? this.setPosition(child) : this.setPosition(node, startType * -1) + }else { + var sibling = this.reversed ? node.previousSibling : node.nextSibling; + sibling ? this.setPosition(sibling) : this.setPosition(node.parentNode, startType * -1) + }this.depth += this.tagType * (this.reversed ? -1 : 1) + }else this.started_ = true; + node = this.node; + if(!this.node)throw goog$iter$StopIteration;return node +}; +goog$dom$TagIterator.prototype.isStartTag = function() { + return this.tagType == 1 +};var goog$dom$AbstractRange = function() { +}; +goog$dom$AbstractRange.prototype.getTextRanges = function() { + for(var output = [], i = 0, len = this.getTextRangeCount();i < len;i++)output.push(this.getTextRange(i)); + return output +}; +goog$dom$AbstractRange.prototype.getAnchorNode = function() { + return this.isReversed() ? this.getEndNode() : this.getStartNode() +}; +goog$dom$AbstractRange.prototype.getAnchorOffset = function() { + return this.isReversed() ? this.getEndOffset() : this.getStartOffset() +}; +goog$dom$AbstractRange.prototype.getFocusNode = function() { + return this.isReversed() ? this.getStartNode() : this.getEndNode() +}; +goog$dom$AbstractRange.prototype.getFocusOffset = function() { + return this.isReversed() ? this.getStartOffset() : this.getEndOffset() +}; +goog$dom$AbstractRange.prototype.isReversed = function() { + return false +}; +goog$dom$AbstractRange.prototype.getDocument = function() { + return goog$dom$getOwnerDocument(goog$userAgent$IE ? this.getContainer() : this.getStartNode()) +}; +goog$dom$AbstractRange.prototype.getWindow = function() { + return goog$dom$getWindow(this.getDocument()) +}; +goog$dom$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) { + return this.containsRange(goog$dom$TextRange$createFromNodeContents(node, undefined), opt_allowPartial) +}; +var goog$dom$RangeIterator = function(node, opt_reverse) { + goog$dom$TagIterator.call(this, node, opt_reverse, true) +}; +goog$inherits(goog$dom$RangeIterator, goog$dom$TagIterator);var goog$dom$AbstractMultiRange = function() { +}; +goog$inherits(goog$dom$AbstractMultiRange, goog$dom$AbstractRange); +goog$dom$AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) { + var ranges = this.getTextRanges(), otherRanges = otherRange.getTextRanges(), fn = opt_allowPartial ? goog$array$some : goog$array$every; + return fn(otherRanges, function(otherRange) { + return goog$array$some(ranges, function(range) { + return range.containsRange(otherRange, opt_allowPartial) + }) + }) +};var goog$dom$TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) { + var goNext; + if(startNode) { + this.startNode_ = startNode; + this.startOffset_ = startOffset; + this.endNode_ = endNode; + this.endOffset_ = endOffset; + if(startNode.nodeType == 1 && startNode.tagName != "BR") { + var startChildren = startNode.childNodes, candidate = startChildren[startOffset]; + if(candidate) { + this.startNode_ = candidate; + this.startOffset_ = 0 + }else { + if(startChildren.length)this.startNode_ = goog$array$peek(startChildren); + goNext = true + } + }if(endNode.nodeType == 1)if(this.endNode_ = endNode.childNodes[endOffset])this.endOffset_ = 0; + else this.endNode_ = endNode + }goog$dom$RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse); + if(goNext)try { + this.next() + }catch(e) { + if(e != goog$iter$StopIteration)throw e; + } +}; +goog$inherits(goog$dom$TextRangeIterator, goog$dom$RangeIterator); +goog$dom$TextRangeIterator.prototype.startNode_ = null; +goog$dom$TextRangeIterator.prototype.endNode_ = null; +goog$dom$TextRangeIterator.prototype.startOffset_ = 0; +goog$dom$TextRangeIterator.prototype.endOffset_ = 0; +goog$dom$TextRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog$dom$TextRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog$dom$TextRangeIterator.prototype.isLast = function() { + return this.started_ && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag()) +}; +goog$dom$TextRangeIterator.prototype.next = function() { + if(this.isLast())throw goog$iter$StopIteration;return goog$dom$TextRangeIterator.superClass_.next.call(this) +};var goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_, goog$userAgent$jscript$DETECTED_VERSION_, JSCompiler_inline_hasScriptEngine_44 = "ScriptEngine" in goog$global; +goog$userAgent$jscript$DETECTED_VERSION_ = (goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_ = JSCompiler_inline_hasScriptEngine_44 && goog$global.ScriptEngine() == "JScript") ? goog$global.ScriptEngineMajorVersion() + "." + goog$global.ScriptEngineMinorVersion() + "." + goog$global.ScriptEngineBuildVersion() : "0";var goog$dom$browserrange$AbstractRange = function() { +}; +goog$dom$browserrange$AbstractRange.prototype.containsRange = function(range, opt_allowPartial) { + return this.containsBrowserRange(range.range_, opt_allowPartial) +}; +goog$dom$browserrange$AbstractRange.prototype.containsBrowserRange = function(range, opt_allowPartial) { + try { + return opt_allowPartial ? this.compareBrowserRangeEndpoints(range, 0, 1) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 0) <= 0 : this.compareBrowserRangeEndpoints(range, 0, 0) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 1) <= 0 + }catch(e) { + if(!goog$userAgent$IE)throw e;return false + } +}; +goog$dom$browserrange$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) { + return this.containsRange(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_allowPartial) +}; +goog$dom$browserrange$AbstractRange.prototype.__iterator__ = function() { + return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +};var goog$dom$browserrange$W3cRange = function(range) { + this.range_ = range +}; +goog$inherits(goog$dom$browserrange$W3cRange, goog$dom$browserrange$AbstractRange); +var goog$dom$browserrange$W3cRange$getBrowserRangeForNode = function(node) { + var nodeRange = goog$dom$getOwnerDocument(node).createRange(); + if(node.nodeType == 3) { + nodeRange.setStart(node, 0); + nodeRange.setEnd(node, node.length) + }else { + for(var tempNode, leaf = node;tempNode = leaf.firstChild;)leaf = tempNode; + nodeRange.setStart(leaf, 0); + for(leaf = node;tempNode = leaf.lastChild;)leaf = tempNode; + nodeRange.setEnd(leaf, leaf.nodeType == 1 ? leaf.childNodes.length : leaf.length) + }return nodeRange +}, goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) { + var nodeRange = goog$dom$getOwnerDocument(startNode).createRange(); + nodeRange.setStart(startNode, startOffset); + nodeRange.setEnd(endNode, endOffset); + return nodeRange +}; +goog$dom$browserrange$W3cRange.prototype.getContainer = function() { + return this.range_.commonAncestorContainer +}; +goog$dom$browserrange$W3cRange.prototype.getStartNode = function() { + return this.range_.startContainer +}; +goog$dom$browserrange$W3cRange.prototype.getStartOffset = function() { + return this.range_.startOffset +}; +goog$dom$browserrange$W3cRange.prototype.getEndNode = function() { + return this.range_.endContainer +}; +goog$dom$browserrange$W3cRange.prototype.getEndOffset = function() { + return this.range_.endOffset +}; +goog$dom$browserrange$W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.START_TO_END : thisEndpoint == 1 ? goog$global.Range.END_TO_START : goog$global.Range.END_TO_END, range) +}; +goog$dom$browserrange$W3cRange.prototype.isCollapsed = function() { + return this.range_.collapsed +}; +goog$dom$browserrange$W3cRange.prototype.select = function(reverse) { + var win = goog$dom$getWindow(goog$dom$getOwnerDocument(this.getStartNode())); + this.selectInternal(win.getSelection(), reverse) +}; +goog$dom$browserrange$W3cRange.prototype.selectInternal = function(selection) { + selection.addRange(this.range_) +}; +goog$dom$browserrange$W3cRange.prototype.collapse = function(toStart) { + this.range_.collapse(toStart) +};var goog$dom$browserrange$GeckoRange = function(range) { + goog$dom$browserrange$W3cRange.call(this, range) +}; +goog$inherits(goog$dom$browserrange$GeckoRange, goog$dom$browserrange$W3cRange); +goog$dom$browserrange$GeckoRange.prototype.selectInternal = function(selection, reversed) { + var anchorNode = reversed ? this.getEndNode() : this.getStartNode(), anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset(), focusNode = reversed ? this.getStartNode() : this.getEndNode(), focusOffset = reversed ? this.getStartOffset() : this.getEndOffset(); + selection.collapse(anchorNode, anchorOffset); + if(anchorNode != focusNode || anchorOffset != focusOffset)selection.extend(focusNode, focusOffset) +};var goog$dom$browserrange$IeRange = function(range, doc) { + this.range_ = range; + this.doc_ = doc +}; +goog$inherits(goog$dom$browserrange$IeRange, goog$dom$browserrange$AbstractRange); +var goog$dom$browserrange$IeRange$logger_ = goog$debug$LogManager$getLogger("goog.dom.browserrange.IeRange"), goog$dom$browserrange$IeRange$getBrowserRangeForNode_ = function(node) { + var nodeRange = goog$dom$getOwnerDocument(node).body.createTextRange(); + if(node.nodeType == 1)nodeRange.moveToElementText(node); + else { + for(var offset = 0, sibling = node;sibling = sibling.previousSibling;) { + var nodeType = sibling.nodeType; + if(nodeType == 3)offset += sibling.length; + else if(nodeType == 1) { + nodeRange.moveToElementText(sibling); + break + } + }sibling || nodeRange.moveToElementText(node.parentNode); + nodeRange.collapse(!sibling); + offset && nodeRange.move("character", offset); + nodeRange.moveEnd("character", node.length) + }return nodeRange +}, goog$dom$browserrange$IeRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) { + var child, collapse = false; + if(startNode.nodeType == 1) { + startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have startOffset > startNode child count"); + child = startNode.childNodes[startOffset]; + collapse = !child; + startNode = child || startNode; + startOffset = 0 + }var leftRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(startNode); + startOffset && leftRange.move("character", startOffset); + collapse && leftRange.collapse(false); + collapse = false; + if(endNode.nodeType == 1) { + startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have endOffset > endNode child count"); + endNode = (child = endNode.childNodes[endOffset]) || endNode; + if(endNode.tagName == "BR")endOffset = 1; + else { + endOffset = 0; + collapse = !child + } + }var rightRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(endNode); + rightRange.collapse(!collapse); + endOffset && rightRange.moveEnd("character", endOffset); + leftRange.setEndPoint("EndToEnd", rightRange); + return leftRange +}, goog$dom$browserrange$IeRange$createFromNodeContents = function(node) { + var range = new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(node), goog$dom$getOwnerDocument(node)); + range.parentNode_ = node; + return range +}; +goog$dom$browserrange$IeRange.prototype.parentNode_ = null; +goog$dom$browserrange$IeRange.prototype.startNode_ = null; +goog$dom$browserrange$IeRange.prototype.endNode_ = null; +goog$dom$browserrange$IeRange.prototype.clearCachedValues_ = function() { + this.parentNode_ = this.startNode_ = this.endNode_ = null +}; +goog$dom$browserrange$IeRange.prototype.getContainer = function() { + if(!this.parentNode_) { + for(var selectText = this.range_.text, i = 1;selectText.charAt(selectText.length - i) == " ";i++)this.range_.moveEnd("character", -1); + for(var parent = this.range_.parentElement(), htmlText = this.range_.htmlText.replace(/(\r\n|\r|\n)+/g, " ");htmlText.length > parent.outerHTML.replace(/(\r\n|\r|\n)+/g, " ").length;)parent = parent.parentNode; + for(;parent.childNodes.length == 1 && parent.innerText == (parent.firstChild.nodeType == 3 ? parent.firstChild.nodeValue : parent.firstChild.innerText);) { + if(parent.firstChild.tagName == "IMG")break; + parent = parent.firstChild + }if(selectText.length == 0)parent = this.findDeepestContainer_(parent); + this.parentNode_ = parent + }return this.parentNode_ +}; +goog$dom$browserrange$IeRange.prototype.findDeepestContainer_ = function(node) { + for(var childNodes = node.childNodes, i = 0, len = childNodes.length;i < len;i++) { + var child = childNodes[i]; + if(child.nodeType == 1)if(this.range_.inRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(child)))return this.findDeepestContainer_(child) + }return node +}; +goog$dom$browserrange$IeRange.prototype.getStartNode = function() { + return this.startNode_ || (this.startNode_ = this.getEndpointNode_(1)) +}; +goog$dom$browserrange$IeRange.prototype.getStartOffset = function() { + return this.getOffset_(1) +}; +goog$dom$browserrange$IeRange.prototype.getEndNode = function() { + return this.endNode_ || (this.endNode_ = this.getEndpointNode_(0)) +}; +goog$dom$browserrange$IeRange.prototype.getEndOffset = function() { + return this.getOffset_(0) +}; +goog$dom$browserrange$IeRange.prototype.containsRange = function(range, opt_allowPartial) { + return this.containsBrowserRange(range.range_, opt_allowPartial) +}; +goog$dom$browserrange$IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + return this.range_.compareEndPoints((thisEndpoint == 1 ? "Start" : "End") + "To" + (otherEndpoint == 1 ? "Start" : "End"), range) +}; +goog$dom$browserrange$IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) { + var node = opt_node || this.getContainer(); + if(!node || !node.firstChild) { + if(endpoint == 0 && node.previousSibling && node.previousSibling.tagName == "BR" && this.getOffset_(endpoint, node) == 0)node = node.previousSibling; + return node.tagName == "BR" ? node.parentNode : node + }for(var child = endpoint == 1 ? node.firstChild : node.lastChild;child;) { + if(this.containsNode(child, true))return this.getEndpointNode_(endpoint, child); + child = endpoint == 1 ? child.nextSibling : child.previousSibling + }return node +}; +goog$dom$browserrange$IeRange.prototype.getOffset_ = function(endpoint, opt_container) { + var container = opt_container || (endpoint == 1 ? this.getStartNode() : this.getEndNode()); + if(container.nodeType == 1) { + for(var children = container.childNodes, len = children.length, i = endpoint == 1 ? 0 : len - 1;i >= 0 && i < len;) { + var child = children[i]; + if(this.containsNode(child, true)) { + endpoint == 0 && child.previousSibling && child.previousSibling.tagName == "BR" && this.getOffset_(endpoint, child) == 0 && i--; + break + }i += endpoint == 1 ? 1 : -1 + }return i == -1 ? 0 : i + }else { + var range = this.range_.duplicate(), nodeRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(container); + range.setEndPoint(endpoint == 1 ? "EndToEnd" : "StartToStart", nodeRange); + var rangeLength = range.text.length; + return endpoint == 0 ? rangeLength : container.length - rangeLength + } +}; +goog$dom$browserrange$IeRange.prototype.isCollapsed = function() { + return this.range_.text == "" +}; +goog$dom$browserrange$IeRange.prototype.select = function() { + this.range_.select() +}; +goog$dom$browserrange$IeRange.prototype.collapse = function(toStart) { + this.range_.collapse(toStart); + if(toStart)this.endNode_ = this.startNode_; + else this.startNode_ = this.endNode_ +};var goog$dom$browserrange$WebKitRange = function(range) { + goog$dom$browserrange$W3cRange.call(this, range) +}; +goog$inherits(goog$dom$browserrange$WebKitRange, goog$dom$browserrange$W3cRange); +goog$dom$browserrange$WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + if(goog$userAgent$isVersion("528"))return goog$dom$browserrange$WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint); + return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.END_TO_START : thisEndpoint == 1 ? goog$global.Range.START_TO_END : goog$global.Range.END_TO_END, range) +}; +goog$dom$browserrange$WebKitRange.prototype.selectInternal = function(selection, reversed) { + selection.removeAllRanges(); + reversed ? selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset()) : selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +};var goog$dom$browserrange$createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) { + return goog$userAgent$IE ? new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog$dom$getOwnerDocument(startNode)) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, + endNode, endOffset)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset)) +};var goog$dom$TextRange = function() { +}; +goog$inherits(goog$dom$TextRange, goog$dom$AbstractRange); +var goog$dom$TextRange$createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) { + var range = new goog$dom$TextRange; + range.browserRangeWrapper_ = browserRange; + range.isReversed_ = !!opt_isReversed; + return range +}, goog$dom$TextRange$createFromNodeContents = function(node, opt_isReversed) { + return goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_isReversed) +}, goog$dom$TextRange$createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) { + var range = new goog$dom$TextRange; + range.isReversed_ = goog$dom$Range$isReversed(anchorNode, anchorOffset, focusNode, focusOffset); + if(anchorNode.tagName == "BR") { + var parent = anchorNode.parentNode; + anchorOffset = goog$array$indexOf(parent.childNodes, anchorNode); + anchorNode = parent + }if(focusNode.tagName == "BR") { + parent = focusNode.parentNode; + focusOffset = goog$array$indexOf(parent.childNodes, focusNode); + focusNode = parent + }if(range.isReversed_) { + range.startNode_ = focusNode; + range.startOffset_ = focusOffset; + range.endNode_ = anchorNode; + range.endOffset_ = anchorOffset + }else { + range.startNode_ = anchorNode; + range.startOffset_ = anchorOffset; + range.endNode_ = focusNode; + range.endOffset_ = focusOffset + }return range +}; +goog$dom$TextRange.prototype.browserRangeWrapper_ = null; +goog$dom$TextRange.prototype.startNode_ = null; +goog$dom$TextRange.prototype.startOffset_ = null; +goog$dom$TextRange.prototype.endNode_ = null; +goog$dom$TextRange.prototype.endOffset_ = null; +goog$dom$TextRange.prototype.isReversed_ = false; +goog$dom$TextRange.prototype.getType = function() { + return"text" +}; +goog$dom$TextRange.prototype.getBrowserRangeObject = function() { + return this.getBrowserRangeWrapper_().range_ +}; +goog$dom$TextRange.prototype.clearCachedValues_ = function() { + this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null +}; +goog$dom$TextRange.prototype.getTextRangeCount = function() { + return 1 +}; +goog$dom$TextRange.prototype.getTextRange = function() { + return this +}; +goog$dom$TextRange.prototype.getBrowserRangeWrapper_ = function() { + return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog$dom$browserrange$createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())) +}; +goog$dom$TextRange.prototype.getContainer = function() { + return this.getBrowserRangeWrapper_().getContainer() +}; +goog$dom$TextRange.prototype.getStartNode = function() { + return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode()) +}; +goog$dom$TextRange.prototype.getStartOffset = function() { + return this.startOffset_ != null ? this.startOffset_ : (this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset()) +}; +goog$dom$TextRange.prototype.getEndNode = function() { + return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode()) +}; +goog$dom$TextRange.prototype.getEndOffset = function() { + return this.endOffset_ != null ? this.endOffset_ : (this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset()) +}; +goog$dom$TextRange.prototype.isReversed = function() { + return this.isReversed_ +}; +goog$dom$TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) { + var otherRangeType = otherRange.getType(); + if(otherRangeType == "text")return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial); + else if(otherRangeType == "control") { + var elements = otherRange.getElements(), fn = opt_allowPartial ? goog$array$some : goog$array$every; + return fn(elements, function(el) { + return this.containsNode(el, opt_allowPartial) + }, this) + } +}; +goog$dom$TextRange.prototype.isCollapsed = function() { + return this.getBrowserRangeWrapper_().isCollapsed() +}; +goog$dom$TextRange.prototype.__iterator__ = function() { + return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +}; +goog$dom$TextRange.prototype.select = function() { + this.getBrowserRangeWrapper_().select(this.isReversed_) +}; +goog$dom$TextRange.prototype.saveUsingDom = function() { + return new goog$dom$DomSavedTextRange_(this) +}; +goog$dom$TextRange.prototype.collapse = function(toAnchor) { + var toStart = this.isReversed() ? !toAnchor : toAnchor; + this.browserRangeWrapper_ && this.browserRangeWrapper_.collapse(toStart); + if(toStart) { + this.endNode_ = this.startNode_; + this.endOffset_ = this.startOffset_ + }else { + this.startNode_ = this.endNode_; + this.startOffset_ = this.endOffset_ + }this.isReversed_ = false +}; +var goog$dom$DomSavedTextRange_ = function(range) { + this.anchorNode_ = range.getAnchorNode(); + this.anchorOffset_ = range.getAnchorOffset(); + this.focusNode_ = range.getFocusNode(); + this.focusOffset_ = range.getFocusOffset() +}; +goog$inherits(goog$dom$DomSavedTextRange_, goog$dom$SavedRange);var goog$dom$ControlRange = function() { +}; +goog$inherits(goog$dom$ControlRange, goog$dom$AbstractMultiRange); +goog$dom$ControlRange.prototype.range_ = null; +goog$dom$ControlRange.prototype.elements_ = null; +goog$dom$ControlRange.prototype.sortedElements_ = null; +goog$dom$ControlRange.prototype.clearCachedValues_ = function() { + this.sortedElements_ = this.elements_ = null +}; +goog$dom$ControlRange.prototype.getType = function() { + return"control" +}; +goog$dom$ControlRange.prototype.getBrowserRangeObject = function() { + return this.range_ || document.body.createControlRange() +}; +goog$dom$ControlRange.prototype.getTextRangeCount = function() { + return this.range_ ? this.range_.length : 0 +}; +goog$dom$ControlRange.prototype.getTextRange = function(i) { + return goog$dom$TextRange$createFromNodeContents(this.range_.item(i)) +}; +goog$dom$ControlRange.prototype.getContainer = function() { + return goog$dom$findCommonAncestor.apply(null, this.getElements()) +}; +goog$dom$ControlRange.prototype.getStartNode = function() { + return this.getSortedElements()[0] +}; +goog$dom$ControlRange.prototype.getStartOffset = function() { + return 0 +}; +goog$dom$ControlRange.prototype.getEndNode = function() { + var sorted = this.getSortedElements(), startsLast = goog$array$peek(sorted); + return goog$array$find(sorted, function(el) { + return goog$dom$contains(el, startsLast) + }) +}; +goog$dom$ControlRange.prototype.getEndOffset = function() { + return this.getEndNode().childNodes.length +}; +goog$dom$ControlRange.prototype.getElements = function() { + if(!this.elements_) { + this.elements_ = []; + if(this.range_)for(var i = 0;i < this.range_.length;i++)this.elements_.push(this.range_.item(i)) + }return this.elements_ +}; +goog$dom$ControlRange.prototype.getSortedElements = function() { + if(!this.sortedElements_) { + this.sortedElements_ = this.getElements().concat(); + this.sortedElements_.sort(function(a, b) { + return a.sourceIndex - b.sourceIndex + }) + }return this.sortedElements_ +}; +goog$dom$ControlRange.prototype.isCollapsed = function() { + return!this.range_ || !this.range_.length +}; +goog$dom$ControlRange.prototype.__iterator__ = function() { + return new goog$dom$ControlRangeIterator(this) +}; +goog$dom$ControlRange.prototype.select = function() { + this.range_ && this.range_.select() +}; +goog$dom$ControlRange.prototype.saveUsingDom = function() { + return new goog$dom$DomSavedControlRange_(this) +}; +goog$dom$ControlRange.prototype.collapse = function() { + this.range_ = null; + this.clearCachedValues_() +}; +var goog$dom$DomSavedControlRange_ = function(range) { + this.elements_ = range.getElements() +}; +goog$inherits(goog$dom$DomSavedControlRange_, goog$dom$SavedRange); +var goog$dom$ControlRangeIterator = function(range) { + if(range) { + this.elements_ = range.getSortedElements(); + this.startNode_ = this.elements_.shift(); + this.endNode_ = goog$array$peek(this.elements_) || this.startNode_ + }goog$dom$RangeIterator.call(this, this.startNode_, false) +}; +goog$inherits(goog$dom$ControlRangeIterator, goog$dom$RangeIterator); +goog$dom$ControlRangeIterator.prototype.startNode_ = null; +goog$dom$ControlRangeIterator.prototype.endNode_ = null; +goog$dom$ControlRangeIterator.prototype.elements_ = null; +goog$dom$ControlRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog$dom$ControlRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog$dom$ControlRangeIterator.prototype.isLast = function() { + return!this.depth && !this.elements_.length +}; +goog$dom$ControlRangeIterator.prototype.next = function() { + if(this.isLast())throw goog$iter$StopIteration;else if(!this.depth) { + var el = this.elements_.shift(); + this.setPosition(el, 1, 1); + return el + }return goog$dom$ControlRangeIterator.superClass_.next.call(this) +};var goog$dom$MultiRange = function() { + this.browserRanges_ = []; + this.ranges_ = []; + this.container_ = this.sortedRanges_ = null +}; +goog$inherits(goog$dom$MultiRange, goog$dom$AbstractMultiRange); +goog$dom$MultiRange.prototype.logger_ = goog$debug$LogManager$getLogger("goog.dom.MultiRange"); +goog$dom$MultiRange.prototype.clearCachedValues_ = function() { + this.ranges_ = []; + this.container_ = this.sortedRanges_ = null +}; +goog$dom$MultiRange.prototype.getType = function() { + return"mutli" +}; +goog$dom$MultiRange.prototype.getBrowserRangeObject = function() { + this.browserRanges_.length > 1 && this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range"); + return this.browserRanges_[0] +}; +goog$dom$MultiRange.prototype.getTextRangeCount = function() { + return this.browserRanges_.length +}; +goog$dom$MultiRange.prototype.getTextRange = function(i) { + this.ranges_[i] || (this.ranges_[i] = goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? new goog$dom$browserrange$IeRange(this.browserRanges_[i], goog$dom$getOwnerDocument(this.browserRanges_[i].parentElement())) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(this.browserRanges_[i]) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(this.browserRanges_[i]) : new goog$dom$browserrange$W3cRange(this.browserRanges_[i]), undefined)); + return this.ranges_[i] +}; +goog$dom$MultiRange.prototype.getContainer = function() { + if(!this.container_) { + for(var nodes = [], i = 0, len = this.getTextRangeCount();i < len;i++)nodes.push(this.getTextRange(i).getContainer()); + this.container_ = goog$dom$findCommonAncestor.apply(null, nodes) + }return this.container_ +}; +goog$dom$MultiRange.prototype.getSortedRanges = function() { + if(!this.sortedRanges_) { + this.sortedRanges_ = this.getTextRanges(); + this.sortedRanges_.sort(function(a, b) { + var aStartNode = a.getStartNode(), aStartOffset = a.getStartOffset(), bStartNode = b.getStartNode(), bStartOffset = b.getStartOffset(); + if(aStartNode == bStartNode && aStartOffset == bStartOffset)return 0; + return goog$dom$Range$isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1 + }) + }return this.sortedRanges_ +}; +goog$dom$MultiRange.prototype.getStartNode = function() { + return this.getSortedRanges()[0].getStartNode() +}; +goog$dom$MultiRange.prototype.getStartOffset = function() { + return this.getSortedRanges()[0].getStartOffset() +}; +goog$dom$MultiRange.prototype.getEndNode = function() { + return goog$array$peek(this.getSortedRanges()).getEndNode() +}; +goog$dom$MultiRange.prototype.getEndOffset = function() { + return goog$array$peek(this.getSortedRanges()).getEndOffset() +}; +goog$dom$MultiRange.prototype.isCollapsed = function() { + return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed() +}; +goog$dom$MultiRange.prototype.__iterator__ = function() { + return new goog$dom$MultiRangeIterator(this) +}; +goog$dom$MultiRange.prototype.select = function() { + var selection; + JSCompiler_inline_label_goog$dom$AbstractRange$getBrowserSelectionForWindow_50: { + var JSCompiler_inline_win = this.getWindow(); + if(JSCompiler_inline_win.getSelection)selection = JSCompiler_inline_win.getSelection(); + else { + var JSCompiler_inline_doc = JSCompiler_inline_win.document; + selection = JSCompiler_inline_doc.selection || JSCompiler_inline_doc.getSelection && JSCompiler_inline_doc.getSelection() + } + }selection.removeAllRanges(); + for(var i = 0, len = this.getTextRangeCount();i < len;i++)selection.addRange(this.getTextRange(i).getBrowserRangeObject()) +}; +goog$dom$MultiRange.prototype.saveUsingDom = function() { + return new goog$dom$DomSavedMultiRange_(this) +}; +goog$dom$MultiRange.prototype.collapse = function(toAnchor) { + if(!this.isCollapsed()) { + var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1); + this.clearCachedValues_(); + range.collapse(toAnchor); + this.ranges_ = [range]; + this.sortedRanges_ = [range]; + this.browserRanges_ = [range.getBrowserRangeObject()] + } +}; +var goog$dom$DomSavedMultiRange_ = function(range) { + this.savedRanges_ = goog$array$map(range.getTextRanges(), function(range) { + return range.saveUsingDom() + }) +}; +goog$inherits(goog$dom$DomSavedMultiRange_, goog$dom$SavedRange); +var goog$dom$MultiRangeIterator = function(range) { + if(range) { + this.ranges_ = range.getSortedRanges(); + if(this.ranges_.length) { + this.startNode_ = this.ranges_[0].getStartNode(); + this.endNode_ = goog$array$peek(this.ranges_).getEndNode() + } + }goog$dom$RangeIterator.call(this, this.startNode_, false) +}; +goog$inherits(goog$dom$MultiRangeIterator, goog$dom$RangeIterator); +goog$dom$MultiRangeIterator.prototype.startNode_ = null; +goog$dom$MultiRangeIterator.prototype.endNode_ = null; +goog$dom$MultiRangeIterator.prototype.ranges_ = null; +goog$dom$MultiRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog$dom$MultiRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog$dom$MultiRangeIterator.prototype.isLast = function() { + return this.ranges_.length == 1 && this.ranges_[0].isLast() +}; +goog$dom$MultiRangeIterator.prototype.next = function() { + do try { + this.ranges_[0].next(); + break + }catch(ex) { + if(ex != goog$iter$StopIteration)throw ex;this.ranges_.shift() + }while(this.ranges_.length); + if(this.ranges_.length) { + var range = this.ranges_[0]; + this.setPosition(range.node, range.tagType, range.depth); + return range.node + }else throw goog$iter$StopIteration; +};var goog$dom$Range$createCaret = function(node, offset) { + return goog$dom$TextRange$createFromNodes(node, offset, node, offset) +}, goog$dom$Range$createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return goog$dom$TextRange$createFromNodes(startNode, startOffset, endNode, endOffset) +}, goog$dom$Range$isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) { + if(anchorNode == focusNode)return focusOffset < anchorOffset; + var child; + if(anchorNode.nodeType == 1 && anchorOffset)if(child = anchorNode.childNodes[anchorOffset]) { + anchorNode = child; + anchorOffset = 0 + }else if(goog$dom$contains(anchorNode, focusNode))return true; + if(focusNode.nodeType == 1 && focusOffset)if(child = focusNode.childNodes[focusOffset]) { + focusNode = child; + focusOffset = 0 + }else if(goog$dom$contains(focusNode, anchorNode))return false; + return(goog$dom$compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0 +};window.createCaret = goog$dom$Range$createCaret; +window.createFromNodes = goog$dom$Range$createFromNodes; +try { + goog$dom$Range$createCaret(document.body, 0).select() +}catch(e$$13) { +}; + +/************************************************** + Trace: + 56.427 Start Handling request + 0 56.427 Start Building cUnit + 1 56.428 Done 1 ms Building cUnit + 0 56.428 Start Checking memcacheg + 0 56.428 Start Connecting to memcacheg + 8 56.436 Done 8 ms Connecting to memcacheg + 1 56.437 Done 9 ms Checking memcacheg + 0 56.437 Done 10 ms Handling request +**************************************************/ diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html b/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html new file mode 100644 index 000000000..ef0e22f2a --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html @@ -0,0 +1,1081 @@ +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Rich Text Tests</title>
+ <script src="js/range.js"></script>
+ <script>
+ /**
+ * Color class allows cross-browser comparison of values, which can
+ * be returned from queryCommandValue in several formats:
+ * 0xff00ff
+ * rgb(255, 0, 0)
+ * Number containing the hex value
+ */
+ function Color(value) {
+ this.compare = function(other) {
+ if (!this.valid || !other.valid) {
+ return false;
+ }
+ return this.red == other.red && this.green == other.green && this.blue == other.blue;
+ }
+ this.parse = function(value) {
+ var hexMatch = String(value).match(/#([0-9a-f]{6})/i);
+ if (hexMatch) {
+ this.red = parseInt(hexMatch[1].substring(0, 2), 16);
+ this.green = parseInt(hexMatch[1].substring(2, 4), 16);
+ this.blue = parseInt(hexMatch[1].substring(4, 6), 16);
+ return true;
+ }
+ var rgbMatch = String(value).match(/rgb\(([0-9]{1,3}),\s*([0-9]{1,3}),\s*([0-9]{1,3})\)/i);
+ if (rgbMatch) {
+ this.red = Number(rgbMatch[1]);
+ this.green = Number(rgbMatch[2]);
+ this.blue = Number(rgbMatch[3]);
+ return true;
+ }
+ if (Number(value)) {
+ this.red = value & 0xFF;
+ this.green = (value & 0xFF00) >> 8;
+ this.blue = (value & 0xFF0000) >> 16;
+ return true;
+ }
+ return false;
+ }
+ this.toString = function() {
+ return this.red + ',' + this.green + ',' + this.blue;
+ }
+ this.valid = this.parse(value);
+ }
+
+ /**
+ * Utility class for converting font sizes to the size
+ * attribute in a font tag. Currently only converts px because
+ * only the sizes and px ever come from queryCommandValue.
+ */
+ function Size(value) {
+ var pxMatch = String(value).match(/([0-9]+)px/);
+ if (pxMatch) {
+ var px = Number(pxMatch[1]);
+ if (px <= 10) {
+ this.size = 1;
+ } else if (px <= 13) {
+ this.size = 2;
+ } else if (px <= 16) {
+ this.size = 3;
+ } else if (px <= 18) {
+ this.size = 4;
+ } else if (px <= 24) {
+ this.size = 5;
+ } else if (px <= 32) {
+ this.size = 6;
+ } else if (px <= 47) {
+ this.size = 7;
+ } else {
+ this.size = NaN;
+ }
+ } else if (Number(value)) {
+ this.size = Number(value);
+ } else {
+ switch (value) {
+ case 'x-small':
+ this.size = 1;
+ break;
+ case 'small':
+ this.size = 2;
+ break;
+ case 'medium':
+ this.size = 3;
+ break;
+ case 'large':
+ this.size = 4;
+ break;
+ case 'x-large':
+ this.size = 5;
+ break;
+ case 'xx-large':
+ this.size = 6;
+ break;
+ case 'xxx-large':
+ case '-webkit-xxx-large':
+ this.size = 7;
+ break;
+ default:
+ this.size = null;
+ }
+ }
+ this.compare = function(other) {
+ return this.size == other.size;
+ }
+ this.toString = function() {
+ return String(this.size);
+ }
+ }
+
+ var IMAGE_URI = '/tests/editor/libeditor/tests/green.png';
+
+ var APPLY_TESTS = {
+ 'backcolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'background-color'},
+ 'bold' : {
+ opt_arg: null,
+ styleWithCSS: 'font-weight'},
+ 'createbookmark' : {
+ opt_arg: 'bookmark_name'},
+ 'createlink' : {
+ opt_arg: 'http://www.openweb.org'},
+ 'decreasefontsize' : {
+ opt_arg: null},
+ 'fontname' : {
+ opt_arg: 'Arial',
+ styleWithCSS: 'font-family'},
+ 'fontsize' : {
+ opt_arg: 4,
+ styleWithCSS: 'font-size'},
+ 'forecolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'color'},
+ 'formatblock' : {
+ opt_arg: 'h1',
+ wholeline: true},
+ 'hilitecolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'background-color'},
+ 'indent' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'margin'},
+ 'inserthorizontalrule' : {
+ opt_arg: null,
+ collapse: true},
+ 'inserthtml': {
+ opt_arg: '<br>',
+ collapse: true},
+ 'insertimage': {
+ opt_arg: IMAGE_URI,
+ collapse: true},
+ 'insertorderedlist' : {
+ opt_arg: null,
+ wholeline: true},
+ 'insertunorderedlist' : {
+ opt_arg: null,
+ wholeline: true},
+ 'italic' : {
+ opt_arg: null,
+ styleWithCSS: 'font-style'},
+ 'justifycenter' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyfull' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyleft' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyright' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'strikethrough' : {
+ opt_arg: null,
+ styleWithCSS: 'text-decoration'},
+ 'subscript' : {
+ opt_arg: null,
+ styleWithCSS: 'vertical-align'},
+ 'superscript' : {
+ opt_arg: null,
+ styleWithCSS: 'vertical-align'},
+ 'underline' : {
+ opt_arg: null,
+ styleWithCSS: 'text-decoration'}};
+
+ var UNAPPLY_TESTS = {
+ 'bold' : {
+ tags: [
+ ['<b>', '</b>'],
+ ['<STRONG>', '</STRONG>'],
+ ['<span style="font-weight: bold;">', '</span>']]},
+ 'italic' : {
+ tags: [
+ ['<i>', '</i>'],
+ ['<EM>', '</EM>'],
+ ['<span style="font-style: italic;">', '</span>']]},
+ 'outdent' : {
+ unapply: 'indent',
+ block: true,
+ tags: [
+ ['<blockquote>', '</blockquote>'],
+ ['<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px;">', '</blockquote>'],
+ ['<ul><li>', '</li></ul>'],
+ ['<ol><li>', '</li></ol>'],
+ ['<div style="margin-left: 40px;">', '</div>']]},
+ 'removeformat' : {
+ unapply: '*',
+ block: true,
+ tags: [
+ ['<b>', '</b>'],
+ ['<a href="http://www.foo.com">', '</a>'],
+ ['<table><tr><td>', '</td></tr></table>']]},
+ 'strikethrough' : {
+ tags: [
+ ['<strike>', '</strike>'],
+ ['<s>', '</s>'],
+ ['<del>', '</del>'],
+ ['<span style="text-decoration: line-through;">', '</span>']]},
+ 'subscript' : {
+ tags: [
+ ['<sub>', '</sub>'],
+ ['<span style="vertical-align: sub;">', '</span>']]},
+ 'superscript' : {
+ tags: [
+ ['<sup>', '</sup>'],
+ ['<span style="vertical-align: super;">', '</span>']]},
+ 'unbookmark' : {
+ unapply: 'createbookmark',
+ tags: [
+ ['<a name="bookmark">', '</a>']]},
+ 'underline' : {
+ tags: [
+ ['<u>', '</u>'],
+ ['<span style="text-decoration: underline;">', '</span>']]},
+ 'unlink' : {
+ unapply: 'createbookmark',
+ tags: [
+ ['<a href="http://www.foo.com">', '</a>']]}};
+
+ var QUERY_TESTS = {
+ 'backcolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'bold' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<b>foo bar baz</b>', expected: true},
+ {html: '<STRONG>foo bar baz</STRONG>', expected: true},
+ {html: '<span style="font-weight:bold">foo bar baz</span>', expected: true},
+ {html: '<b style="font-weight:normal">foo bar baz</b>', expected: false},
+ {html: '<b><span style="font-weight:normal;">foo bar baz</span>', expected: false}
+ ]
+ },
+ 'fontname' : {
+ type: 'value',
+ tests: [
+ {html: '<font face="Arial">foo bar baz</font>', expected: 'Arial'},
+ {html: '<span style="font-family:Arial">foo bar baz</span>', expected: 'Arial'},
+ {html: '<font face="Arial" style="font-family:Courier">foo bar baz</font>', expected: 'Courier'},
+ {html: '<font face="Courier"><font face="Arial">foo bar baz</font></font>', expected: 'Arial'},
+ {html: '<span style="font-family:Courier"><font face="Arial">foo bar baz</font></span>', expected: 'Arial'}
+ ]
+ },
+ 'fontsize' : {
+ type: 'value',
+ tests: [
+ {html: '<font size=4>foo bar baz</font>', expected: new Size(4)},
+ // IE adds +1 to font size from font-size style attributes.
+ // This is hard to correct for since it does NOT add +1 to size attribute from font tag.
+ {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', expected: new Size(4)},
+ {html: '<font size=1 style="font-size:x-large;">foo bar baz</font>', expected: new Size(5)}
+ ]
+ },
+ 'forecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<font color="#ff0000">foo bar baz</font>', expected: new Color('#ff0000')},
+ {html: '<span style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'hilitecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'insertorderedlist' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<ol><li>foo bar baz</li></ol>', expected: true},
+ {html: '<ul><li>foo bar baz</li></ul>', expected: false}
+ ]
+ },
+ 'insertunorderedlist' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<ol><li>foo bar baz</li></ol>', expected: false},
+ {html: '<ul><li>foo bar baz</li></ul>', expected: true}
+ ]
+ },
+ 'italic' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<i>foo bar baz</i>', expected: true},
+ {html: '<EM>foo bar baz</EM>', expected: true},
+ {html: '<span style="font-style:italic">foo bar baz</span>', expected: true},
+ {html: '<i><span style="font-style:normal">foo bar baz</span></i>', expected: false}
+ ]
+ },
+ 'justifycenter' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="center">foo bar baz</div>', expected: true},
+ {html: '<p align="center">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: center;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyfull' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="justify">foo bar baz</div>', expected: true},
+ {html: '<p align="justify">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: justify;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyleft' : {
+ type: 'state',
+ tests: [
+ {html: '<div align="left">foo bar baz</div>', expected: true},
+ {html: '<p align="left">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: left;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyright' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="right">foo bar baz</div>', expected: true},
+ {html: '<p align="right">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: right;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'strikethrough' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<strike>foo bar baz</strike>', expected: true},
+ {html: '<strike style="text-decoration: none">foo bar baz</strike>', expected: false},
+ {html: '<s>foo bar baz</s>', expected: true},
+ {html: '<del>foo bar baz</del>', expected: true},
+ {html: '<span style="text-decoration:line-through">foo bar baz</span>', expected: true}
+ ]
+ },
+ 'subscript' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<sub>foo bar baz</sub>', expected: true}
+ ]
+ },
+ 'superscript' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<sup>foo bar baz</sup>', expected: true}
+ ]
+ },
+ 'underline' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<u>foo bar baz</u>', expected: true},
+ {html: '<a href="http://www.foo.com">foo bar baz</a>', expected: true},
+ {html: '<span style="text-decoration:underline">foo bar baz</span>', expected: true},
+ {html: '<u style="text-decoration:none">foo bar baz</u>', expected: false},
+ {html: '<a style="text-decoration:none" href="http://www.foo.com">foo bar baz</a>', expected: false}
+ ]
+ }
+ };
+
+ var CHANGE_TESTS = {
+ 'backcolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#0000ff'},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#0000ff'}
+ ]
+ },
+ 'fontname' : {
+ type: 'value',
+ tests: [
+ {html: '<font face="Arial">foo bar baz</font>', opt_arg: 'Courier'},
+ {html: '<span style="font-family:Arial">foo bar baz</span>', opt_arg: 'Courier'},
+ {html: '<font face="Arial" style="font-family:Verdana">foo bar baz</font>', opt_arg: 'Courier'},
+ {html: '<font face="Verdana"><font face="Arial">foo bar baz</font></font>', opt_arg: 'Courier'},
+ {html: '<span style="font-family:Verdana"><font face="Arial">foo bar baz</font></span>', opt_arg: 'Courier'}
+ ]
+ },
+ 'fontsize' : {
+ type: 'value',
+ tests: [
+ {html: '<font size=4>foo bar baz</font>', opt_arg: 1},
+ {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', opt_arg: 1},
+ {html: '<font size=1 style="font-size:x-small;">foo bar baz</font>', opt_arg: 5}
+ ]
+ },
+ 'forecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<font color="#ff0000">foo bar baz</font>', opt_arg: '#00ff00'},
+ {html: '<span style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'},
+ {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+ ]
+ },
+ 'hilitecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#00ff00'},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+ ]
+ }
+ };
+
+ /** The document of the editable iframe */
+ var editorDoc = null;
+ /** Dummy text to apply and unapply formatting to */
+ var TEST_CONTENT = 'foo bar baz';
+ /**
+ * Word in dummy text that should change. Formatting is sometimes applied
+ * to a single word instead of the entire text node because sometimes a
+ * style might get applied to the body node instead of wrapped around
+ * the text, and that's not what's being tested.
+ */
+ var TEST_WORD = 'bar';
+ /** Constant for indicating an action is unsupported (threw exception) */
+ var UNSUPPORTED = 'UNSUPPORTED';
+ /** <br> and <p> are acceptable HTML to be left over from block elements */
+ var BLOCK_REMOVE_TAGS = [/\s*<br>\s*/i, /\s*<p>\s*/i];
+ /** Array used to accumulate test results */
+ // Tack on the actual display tests with bogus data
+ // otherwise the beacon will fail.
+ var results = ['apply=0', 'unapply=0', 'change=0', 'query=0'];
+
+ /**
+ *
+ */
+ function resetIframe(newHtml) {
+ // These attributes can get set on the iframe by some errant execCommands
+ editorDoc.body.setAttribute('style', '');
+ editorDoc.body.setAttribute('bgcolor', '');
+ editorDoc.body.innerHTML = newHtml;
+ }
+
+ /**
+ * Finds the text node in the given node containing the given word.
+ * Returns null if not found.
+ */
+ function findTextNode(word, node) {
+ if (node.nodeType == 3) {
+ // Text node, check value.
+ if (node.data.indexOf(word) != -1) {
+ return node;
+ }
+ } else if (node.nodeType == 1) {
+ // Element node, check children.
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var result = findTextNode(word, node.childNodes[i]);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the selection to be collapsed at the start of the word,
+ * or the start of the editor if no word is passed in.
+ */
+ function selectStart(word) {
+ var textNode = findTextNode(word || '', editorDoc.body);
+ var startOffset = 0;
+ if (word) {
+ startOffset = textNode.data.indexOf(word);
+ }
+ var range = createCaret(textNode, startOffset);
+ range.select();
+ }
+
+ /**
+ * Selects the given word in the editor iframe.
+ */
+ function selectWord(word) {
+ var textNode = findTextNode(word, editorDoc.body);
+ if (!textNode) {
+ return;
+ }
+ var start = textNode.data.indexOf(word);
+ var range = createFromNodes(textNode, start, textNode, start + word.length);
+ range.select();
+ }
+
+ /**
+ * Gets the HTML before the text, so that we know how the browser
+ * applied a style
+ */
+ function getSurroundingTags(text) {
+ var html = editorDoc.body.innerHTML;
+ var tagStart = html.indexOf('<');
+ var index = editorDoc.body.innerHTML.indexOf(text);
+ if (tagStart == -1 || index == -1) {
+ return '';
+ }
+ return editorDoc.body.innerHTML.substring(tagStart, index);
+ }
+
+ /**
+ * Does the test for an apply execCommand.
+ */
+ function doApplyTest(command, styleWithCSS) {
+ try {
+ // Set styleWithCSS
+ try {
+ editorDoc.execCommand('styleWithCSS', false, styleWithCSS);
+ } catch (ex) {
+ // Ignore errors
+ }
+ resetIframe(TEST_CONTENT);
+ if (APPLY_TESTS[command].collapse) {
+ selectStart(TEST_WORD);
+ } else {
+ selectWord(TEST_WORD);
+ }
+ try {
+ editorDoc.execCommand(command, false, APPLY_TESTS[command].opt_arg);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ return getSurroundingTags(APPLY_TESTS[command].wholeline? TEST_CONTENT : TEST_WORD);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Outputs the result of the apply command to a table.
+ * @return {boolean} success
+ */
+ function outputApplyResult(command, result, styleWithCSS) {
+ // The apply command "succeeded" if HTML was generated.
+ var success = (result != UNSUPPORTED) && result;
+ // Except for styleWithCSS commands, which only succeed if the
+ // expected style was applied.
+ if (styleWithCSS) {
+ success = result && result.toLowerCase().indexOf(APPLY_TESTS[command].styleWithCSS) != -1;
+ }
+ results.push('a-' + command + '-' + (styleWithCSS ? 1 : 0) + '=' + (success ? '1' : '0'));
+
+ // Each command is displayed as a table row with 3 columns
+ var tr = document.createElement('TR');
+ tr.className = success ? 'success' : 'fail';
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: styleWithCSS
+ var td = document.createElement('TD');
+ td.innerHTML = styleWithCSS ? 'true' : 'false';
+ tr.appendChild(td);
+
+ // Column 3: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 4: generated HTML (for passing commands)
+ td = document.createElement('TD');
+ // Escape the HTML in the result for printing.
+ result = result.replace(/\</g, '<').replace(/\>/g, '>');
+ td.innerHTML = result;
+ tr.appendChild(td);
+ var table = document.getElementById('apply_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ /**
+ * Does the test for an unapply execCommand.
+ */
+ function doUnapplyTest(command, index) {
+ try {
+ var wordStart = TEST_CONTENT.indexOf(TEST_WORD);
+ resetIframe(
+ TEST_CONTENT.substring(0, wordStart) +
+ UNAPPLY_TESTS[command].tags[index][0] +
+ TEST_WORD +
+ UNAPPLY_TESTS[command].tags[index][1] +
+ TEST_CONTENT.substring(wordStart + TEST_WORD.length));
+ selectWord(TEST_WORD);
+ try {
+ editorDoc.execCommand(command, false, UNAPPLY_TESTS[command].opt_arg || null);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ return getSurroundingTags(TEST_WORD);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Check if the given unapply execCommand succeeded. It succeeded if
+ * the following conditions are true:
+ * - The execCommand did not throw an exception
+ * - One of the following:
+ * - The html was removed after the execCommand
+ * - The html was block and the html was replaced with <p> or <br>
+ */
+ function unapplyCommandSucceeded(command, result) {
+ if (result != UNSUPPORTED) {
+ if (!result) {
+ return true;
+ } else if (UNAPPLY_TESTS[command].block) {
+ for (var i = 0; i < BLOCK_REMOVE_TAGS.length; i++) {
+ if (result.match(BLOCK_REMOVE_TAGS[i])) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Outputs the result of the unapply command to a table.
+ * @return {boolean} success
+ */
+ function outputUnapplyResult(command, result, index) {
+ // The apply command "succeeded" if HTML was removed.
+ var success = unapplyCommandSucceeded(command, result);
+ results.push('u-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Each command is displayed as a table row with 5 columns
+ var tr = document.createElement('TR');
+ tr.className = success ? 'success' : 'fail';
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: command name being unapplied
+ var td = document.createElement('TD');
+ td.innerHTML = UNAPPLY_TESTS[command].unapply || command;
+ tr.appendChild(td);
+
+ // Column 3: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 4: html being removed
+ td = document.createElement('TD');
+ // Escape the html for printing.
+ var htmlToRemove = UNAPPLY_TESTS[command].tags[index][0].replace(/\</g, '<').replace(/\>/g, '>');
+ td.innerHTML = htmlToRemove;
+ tr.appendChild(td);
+
+ // Column 5: resulting html
+ td = document.createElement('TD');
+ // Escape the HTML in the result for printing.
+ result = result.replace(/\</g, '<').replace(/\>/g, '>');
+ td.innerHTML = success ? ' ' : result;
+ tr.appendChild(td);
+ var table = document.getElementById('unapply_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ /**
+ * Does a queryCommandState or queryCommandValue test for an execCommand.
+ */
+ function doQueryTest(command, index) {
+ try {
+ resetIframe(QUERY_TESTS[command].tests[index].html);
+ selectWord(TEST_WORD);
+ // Dummy val that won't match any expected vals, including false.
+ var result = UNSUPPORTED;
+ if (QUERY_TESTS[command].type == 'state') {
+ try {
+ result = editorDoc.queryCommandState(command);
+ } catch (ex) {
+ result = UNSUPPORTED;
+ }
+ } else {
+ try {
+ // A return value of false indicates the command is not supported.
+ result = editorDoc.queryCommandValue(command) || UNSUPPORTED;
+ } catch (ex) {
+ result = UNSUPPORTED;
+ }
+ }
+ return result;
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Check if the given queryCommandState or queryCommandValue succeeded.
+ */
+ function queryCommandSucceeded(command, index, result) {
+ var expected = QUERY_TESTS[command].tests[index].expected;
+ if (expected instanceof Color) {
+ return expected.compare(new Color(result));
+ } else if (expected instanceof Size) {
+ return expected.compare(new Size(result));
+ } else {
+ return (result == expected);
+ }
+ }
+
+ /**
+ * @return {boolean} success
+ */
+ function outputQueryResult(command, index, result) {
+ // Create table row for results.
+ var tr = document.createElement('TR');
+ var success = queryCommandSucceeded(command, index, result);
+ tr.className = success ? 'success' : 'fail';
+ results.push('q-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 3: test HTML
+ td = document.createElement('TD');
+ var testHtml = QUERY_TESTS[command].tests[index].html.replace(/</g, '<').replace(/>/g, '>');
+ td.innerHTML = testHtml.substring(0, testHtml.indexOf(TEST_CONTENT));
+ tr.appendChild(td);
+
+ // Column 4: Expected result
+ td = document.createElement('TD');
+ td.innerHTML = QUERY_TESTS[command].tests[index].expected;
+ tr.appendChild(td);
+
+ // Column 5: Actual result
+ td = document.createElement('TD');
+ td.innerHTML = result;
+ tr.appendChild(td);
+
+ // Append result to the state or value table, depending on what
+ // type of command this is.
+ var table = document.getElementById(
+ QUERY_TESTS[command].type == 'state' ? 'querystate_output' : 'queryvalue_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ function doChangeTest(command, index) {
+ try {
+ resetIframe(CHANGE_TESTS[command].tests[index].html);
+ selectWord(TEST_CONTENT);
+ try {
+ editorDoc.execCommand(command, false, CHANGE_TESTS[command].tests[index].opt_arg);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ function checkChangeSuccess(command, index) {
+ var textNode = findTextNode(TEST_CONTENT, editorDoc.body);
+ if (!textNode) {
+ // The text has been removed from the document, or split up for no reason.
+ return false;
+ }
+ var expected = null, attributeName = null, styleName = null;
+ switch (command) {
+ case 'backcolor':
+ case 'hilitecolor':
+ expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+ styleName = 'backgroundColor';
+ break;
+ case 'fontname':
+ expected = CHANGE_TESTS[command].tests[index].opt_arg;
+ attributeName = 'face';
+ styleName = 'fontFamily';
+ break;
+ case 'fontsize':
+ expected = new Size(CHANGE_TESTS[command].tests[index].opt_arg);
+ attributeName = 'size';
+ styleName = 'fontSize';
+ break;
+ case 'forecolor':
+ expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+ attributeName = 'color';
+ styleName = 'color';
+ }
+ var foundExpected = false;
+
+ // Loop through all the parent nodes that format the text node,
+ // checking that there is exactly one font attribute or
+ // style, and that it's set correctly.
+ var currentNode = textNode.parentNode;
+ while(currentNode && currentNode.nodeName != 'BODY') {
+ // Check font attribute.
+ if (attributeName && currentNode.nodeName == 'FONT' && currentNode.getAttribute(attributeName)) {
+ var foundAttribute = false;
+ switch(command) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ foundAttribute = new Color(currentNode.getAttribute(attributeName)).compare(expected);
+ break;
+ case 'fontsize':
+ foundAttribute = new Size(currentNode.getAttribute(attributeName)).compare(expected);
+ break;
+ case 'fontname':
+ foundAttribute = (currentNode.getAttribute(attributeName).toLowerCase() == expected.toLowerCase());
+ }
+ if (foundAttribute && foundExpected) {
+ // This is the correct attribute, but the style has been applied
+ // twice. This makes it hard for other browsers to remove the
+ // style.
+ return false;
+ } else if (!foundAttribute) {
+ // This node has an incorrect font attribute.
+ return false;
+ }
+ // The expected font attribute was found.
+ foundExpected = true;
+ }
+ // Check node style.
+ if (currentNode.style[styleName]) {
+ var foundStyle = false;
+ switch(command) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ foundStyle = new Color(currentNode.style[styleName]).compare(expected);
+ break;
+ case 'fontsize':
+ foundStyle = new Size(currentNode.style[styleName]).compare(expected);
+ break;
+ case 'fontname':
+ foundStyle = (currentNode.style[styleName].toLowerCase() == expected.toLowerCase());
+ }
+ if (foundStyle && foundExpected) {
+ // This is the correct style, but the style has been
+ // applied twice. This makes it hard for other browsers to
+ // remove the style.
+ return false;
+ } else if (!foundStyle) {
+ // This node has an incorrect font style.
+ return false;
+ }
+ foundExpected = true;
+ }
+ currentNode = currentNode.parentNode;
+ }
+ return foundExpected;
+ }
+
+ /**
+ * @return {boolean} success
+ */
+ function outputChangeResult(command, index) {
+ // Each command is displayed as a table row with 4 columns
+ var tr = document.createElement('TR');
+ var success = checkChangeSuccess(command, index);
+ tr.className = success ? 'success' : 'fail';
+ results.push('c-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: status
+ td = document.createElement('TD');
+ td.innerHTML = (success == null) ? '?' : (success == true ? 'PASS' : 'FAIL');
+ tr.appendChild(td);
+
+ // Column 3: opt_arg
+ td = document.createElement('TD');
+ td.innerHTML = CHANGE_TESTS[command].tests[index].opt_arg;
+ tr.appendChild(td);
+
+ // Column 4: original html
+ td = document.createElement('TD');
+ td.innerHTML = CHANGE_TESTS[command].tests[index].html.replace(/\</g, '<').replace(/\>/g, '>');;
+ tr.appendChild(td);
+
+ // Column 5: resulting html
+ td = document.createElement('TD');
+ td.innerHTML = editorDoc.body.innerHTML.replace(/\</g, '<').replace(/\>/g, '>');;
+ tr.appendChild(td);
+
+ var table = document.getElementById('change_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ function runTests() {
+ // Wrap initialization code in a try/catch so we can fail gracefully
+ // on older browsers.
+ try {
+ editorDoc = document.getElementById('editor').contentWindow.document;
+ // Default styleWithCSS to false, since it's not supported by IE.
+ try {
+ editorDoc.execCommand('styleWithCSS', false, false);
+ } catch (ex) {
+ // Not supported by IE.
+ }
+ } catch (ex) {}
+
+ // Apply tests
+ var apply_score = 0;
+ var apply_count = 0;
+ var unapply_score= 0;
+ var unapply_count = 0;
+ var change_score = 0;
+ var change_count = 0;
+ var query_score = 0;
+ var query_count = 0;
+ for (var command in APPLY_TESTS) {
+ try {
+ var result = doApplyTest(command, false);
+ var success = outputApplyResult(command, result, false);
+ apply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ apply_count++;
+ if (APPLY_TESTS[command].styleWithCSS) {
+ try {
+ var result = doApplyTest(command, true);
+ var success = outputApplyResult(command, result, true);
+ apply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ apply_count++;
+ }
+ }
+
+ // Unapply tests
+ for (var command in UNAPPLY_TESTS) {
+ for (var i = 0; i < UNAPPLY_TESTS[command].tags.length; i++) {
+ try {
+ var result = doUnapplyTest(command, i);
+ var success = outputUnapplyResult(command, result, i);
+ unapply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ unapply_count++;
+ }
+ }
+
+ // Query tests
+ for (var command in QUERY_TESTS) {
+ for (var i = 0; i < QUERY_TESTS[command].tests.length; i++) {
+ try {
+ var result = doQueryTest(command, i);
+ var success = outputQueryResult(command, i, result);
+ query_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ query_count++;
+ }
+ }
+
+ // Change tests
+ for (var command in CHANGE_TESTS) {
+ for (var i = 0; i < CHANGE_TESTS[command].tests.length; i++) {
+ try {
+ doChangeTest(command, i);
+ var success = outputChangeResult(command, i);
+ change_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ change_count++;
+ }
+ }
+
+ // Beacon all test results.
+ // and construct a shorter version for the results page.
+ try {
+ document.getElementById('apply-score').innerHTML =
+ apply_score + '/' + apply_count;
+ document.getElementById('unapply-score').innerHTML =
+ unapply_score + '/' + unapply_count;
+ document.getElementById('query-score').innerHTML =
+ query_score + '/' + query_count;
+ document.getElementById('change-score').innerHTML =
+ change_score + '/' + change_count;
+ } catch (ex) {}
+ var continueParams = [
+ 'apply=' + apply_score,
+ 'unapply=' + unapply_score,
+ 'query=' + query_score,
+ 'change=' + change_score
+ ];
+ parent.sendScore(results, continueParams);
+ }
+ </script>
+ <style>
+ .success {
+ background-color: #93c47d;
+ }
+ .fail {
+ background-color: #ea9999;
+ }
+ .score {
+ color: #666;
+ }
+ </style>
+</head>
+<body onload="runTests()">
+ <h1>Apply Formatting <span id="apply-score" class="score"></span></h1>
+ <table id="apply"><tbody id="apply_output"><tr><th>Command</th><th>styleWithCSS</th><th>Status</th><th>Output</th></tr></tbody></table>
+ <h1>Unapply Formatting <span id="unapply-score" class="score"></span></h1>
+ <table id="unapply">
+ <thead><tr><th>Command</th><th>Command unapplied</th><th>Status</th><th>HTML Attempted to Unapply</th><th>Resulting HTML</th></tr></thead>
+ <tbody id="unapply_output"></tbody></table>
+ <h1>Query Formatting State <span id="query-score" class="score"></span></h1>
+ <table id="querystate">
+ <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+ <tbody id="querystate_output"></tbody></table>
+ <h1>Query Formatting Value </h1>
+ <table id="queryvalue">
+ <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+ <tbody id="queryvalue_output"></tbody></table>
+ <h1>Change Formatting <span id="change-score" class="score"></span></h1>
+ <table id="change">
+ <thead><tr><th>Command</th><th>Status</th><th>Argument</th><th>Original HTML</th><th>Resulting HTML</th></tr></thead>
+ <tbody id="change_output"></tbody></table>
+ <iframe name="editor" id="editor" src="editable.html"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream b/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream new file mode 100644 index 000000000..2071454a8 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream @@ -0,0 +1,16 @@ +#!/bin/sh + +set -x + +if test -d richtext; then + rm -drf richtext; +fi + +svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext/static richtext | tail -1 | sed 's/[^0-9]//g' > current_revision + +find richtext -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null + +hg add current_revision richtext + +hg stat . + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE b/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE new file mode 100644 index 000000000..57bc88a15 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/README b/editor/libeditor/tests/browserscope/lib/richtext2/README new file mode 100644 index 000000000..a3bc3110f --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/README @@ -0,0 +1,58 @@ +README FOR BROWSERSCOPE +----------------------- + +Hey there - thanks for downloading the code. This file has instructions +for getting setup so that you can run the codebase locally. + +This project is built on Google App Engine using the +Django web application framework and written in Python. + +To get started, you'll need to first download the App Engine SDK at: +http://code.google.com/appengine/downloads.html + +For local development, just startup the server: +./pathto/google_appengine/dev_appserver.py --port=8080 browserscope + +You should then be able to access the local application at: +http://localhost:8080/ + +Note: the first time you hit the homepage it may take a little +while - that's because it's trying to read out median times for all +of the tests from a nonexistent datastore and write to memcache. +Just be a lil patient. + +You can run the unit tests at: + http://localhost:8080/test + + +CONTRIBUTING +------------------ + +Most likely you are interested in adding new tests or creating +a new test category. If you are interested in adding tests to an existing +"category" you may want to get in touch with the maintainer for that +branch of the tree. We are really looking forward to receiving your +code in patch format. Currently the category maintainers are: +Network: Steve Souders <souders@gmail.com> +Reflow: Lindsey Simon <elsigh@gmail.com> +Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com> + + +To create a completely new test category: + * Copy one of the existing directories in categories/ + * Edit your test_set.py, handlers.py + * Add your files in templates/ and static/ + * Update urls.py and settings.CATEGORIES + * Follow the examples of other tests re: + * beaconing using/testdriver_base + * your GetScoreAndDisplayValue method + * your GetRowScoreAndDisplayValue method + +References: + * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html + * App Engine Group - http://groups.google.com/group/google-appengine + * Python Docs - http://www.python.org/doc/ + * Django - http://www.djangoproject.com/ + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla b/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla new file mode 100644 index 000000000..3e667a0b7 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla @@ -0,0 +1,23 @@ +The BrowserScope project provides a set of cross-browser HTML editor tests, +which we import in our test suite in order to run them as part of our +continuous integration system. + +We pull tests occasionally from their Subversion repository using the pull +script which can be found in this directory. We also record the revision ID +which we've used in the current_revision file inside this directory. + +Using the pull script is quite easy, just switch to this directory, and say: + +sh update_from_upstream + +There are tests which we're currently failing on, and there will probably be +more of those in the future. We should maintain a list of the failing tests +manually in currentStatus.js (which can also be found in this directory), to +make sure that the suite passes entirely, with failing tests marked as todo +items. + +The current status of the test suite needs to be updated whenever an editor +bug gets fixed, which makes us pass one of the tests. When that happens, +you should set the UPDATE_TEST_RESULTS constant to true in test_richtext2.html, +run the test suite, paste the result JSON string in a JSON beautifier (such +as http://jsbeautifier.org/), and use the result to update currentStatus.js. diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js new file mode 100644 index 000000000..570853afa --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js @@ -0,0 +1,1850 @@ +/** + * The current status of the test suite. + * + * See README.Mozilla for details on how to generate this. + */ +const knownFailures = { + "value": { + "A-Proposed-FS:18px_TEXT-1_SI-dM": true, + "A-Proposed-FS:18px_TEXT-1_SI-body": true, + "A-Proposed-FS:18px_TEXT-1_SI-div": true, + "A-Proposed-FS:large_TEXT-1_SI-dM": true, + "A-Proposed-FS:large_TEXT-1_SI-body": true, + "A-Proposed-FS:large_TEXT-1_SI-div": true, + "A-Proposed-CB:name_TEXT-1_SI-dM": true, + "A-Proposed-CB:name_TEXT-1_SI-body": true, + "A-Proposed-CB:name_TEXT-1_SI-div": true, + "AC-Proposed-SUB_TEXT-1_SI-dM": true, + "AC-Proposed-SUB_TEXT-1_SI-body": true, + "AC-Proposed-SUB_TEXT-1_SI-div": true, + "AC-Proposed-SUP_TEXT-1_SI-dM": true, + "AC-Proposed-SUP_TEXT-1_SI-body": true, + "AC-Proposed-SUP_TEXT-1_SI-div": true, + "AC-Proposed-FS:2_TEXT-1_SI-dM": true, + "AC-Proposed-FS:2_TEXT-1_SI-body": true, + "AC-Proposed-FS:2_TEXT-1_SI-div": true, + "AC-Proposed-FS:18px_TEXT-1_SI-dM": true, + "AC-Proposed-FS:18px_TEXT-1_SI-body": true, + "AC-Proposed-FS:18px_TEXT-1_SI-div": true, + "AC-Proposed-FS:large_TEXT-1_SI-dM": true, + "AC-Proposed-FS:large_TEXT-1_SI-body": true, + "AC-Proposed-FS:large_TEXT-1_SI-div": true, + "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-dM": true, + "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-body": true, + "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-div": true, + "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-dM": true, + "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-body": true, + "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-div": true, + "C-Proposed-FN:c_FONTf:a-1_SI-dM": true, + "C-Proposed-FN:c_FONTf:a-1_SI-body": true, + "C-Proposed-FN:c_FONTf:a-1_SI-div": true, + "C-Proposed-FN:c_FONTf:a-2_SL-dM": true, + "C-Proposed-FN:c_FONTf:a-2_SL-body": true, + "C-Proposed-FN:c_FONTf:a-2_SL-div": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-dM": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-body": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-div": true, + "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-dM": true, + "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-body": true, + "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-div": true, + "C-Proposed-FS:larger_FONTsz:4-dM": true, + "C-Proposed-FS:larger_FONTsz:4-body": true, + "C-Proposed-FS:larger_FONTsz:4-div": true, + "C-Proposed-FS:smaller_FONTsz:4-dM": true, + "C-Proposed-FS:smaller_FONTsz:4-body": true, + "C-Proposed-FS:smaller_FONTsz:4-div": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-body": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-div": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-body": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-div": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-body": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-div": true, + "CC-Proposed-I_B-1_SW-dM": true, + "CC-Proposed-I_B-1_SW-body": true, + "CC-Proposed-I_B-1_SW-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-div": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-dM": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-body": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-div": true, + "CC-Proposed-FN:c_FONTf:a-1_SI-dM": true, + "CC-Proposed-FN:c_FONTf:a-1_SI-body": true, + "CC-Proposed-FN:c_FONTf:a-1_SI-div": true, + "CC-Proposed-FN:c_FONTf:a-2_SL-dM": true, + "CC-Proposed-FN:c_FONTf:a-2_SL-body": true, + "CC-Proposed-FN:c_FONTf:a-2_SL-div": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-dM": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-body": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-div": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-dM": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-body": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-div": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-dM": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-body": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-div": true, + "U-RFC-UNLINK_A-1_SO-dM": true, + "U-RFC-UNLINK_A-1_SO-body": true, + "U-RFC-UNLINK_A-1_SO-div": true, + "U-RFC-UNLINK_A-1_SW-dM": true, + "U-RFC-UNLINK_A-1_SW-body": true, + "U-RFC-UNLINK_A-1_SW-div": true, + "U-RFC-UNLINK_A-2_SO-dM": true, + "U-RFC-UNLINK_A-2_SO-body": true, + "U-RFC-UNLINK_A-2_SO-div": true, + "U-RFC-UNLINK_A2-1_SO-dM": true, + "U-RFC-UNLINK_A2-1_SO-body": true, + "U-RFC-UNLINK_A2-1_SO-div": true, + "U-Proposed-B_B-P-I..P-1_SO-I-dM": true, + "U-Proposed-B_B-P-I..P-1_SO-I-body": true, + "U-Proposed-B_B-P-I..P-1_SO-I-div": true, + "U-Proposed-B_B-2_SL-dM": true, + "U-Proposed-B_B-2_SL-body": true, + "U-Proposed-B_B-2_SL-div": true, + "U-Proposed-B_B-2_SR-dM": true, + "U-Proposed-B_B-2_SR-body": true, + "U-Proposed-B_B-2_SR-div": true, + "U-Proposed-U_U-S-2_SI-dM": true, + "U-Proposed-U_U-S-2_SI-body": true, + "U-Proposed-U_U-S-2_SI-div": true, + "U-Proposed-S_DEL-1_SW-dM": true, + "U-Proposed-S_DEL-1_SW-body": true, + "U-Proposed-S_DEL-1_SW-div": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-dM": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-body": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-div": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-dM": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-body": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-div": true, + "U-Proposed-UNLINK_A-1_SC-dM": true, + "U-Proposed-UNLINK_A-1_SC-body": true, + "U-Proposed-UNLINK_A-1_SC-div": true, + "U-Proposed-UNLINK_A-1_SI-dM": true, + "U-Proposed-UNLINK_A-1_SI-body": true, + "U-Proposed-UNLINK_A-1_SI-div": true, + "U-Proposed-UNLINK_A-2_SL-dM": true, + "U-Proposed-UNLINK_A-2_SL-body": true, + "U-Proposed-UNLINK_A-2_SL-div": true, + "U-Proposed-UNLINK_A-3_SR-dM": true, + "U-Proposed-UNLINK_A-3_SR-body": true, + "U-Proposed-UNLINK_A-3_SR-div": true, + "U-Proposed-OUTDENT_BQ-1_SW-dM": true, + "U-Proposed-OUTDENT_BQ-1_SW-body": true, + "U-Proposed-OUTDENT_BQ-1_SW-div": true, + "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-dM": true, + "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-body": true, + "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-div": true, + "U-Proposed-OUTDENT_OL-LI-1_SW-dM": true, + "U-Proposed-OUTDENT_OL-LI-1_SW-body": true, + "U-Proposed-OUTDENT_OL-LI-1_SW-div": true, + "U-Proposed-OUTDENT_UL-LI-1_SW-dM": true, + "U-Proposed-OUTDENT_UL-LI-1_SW-body": true, + "U-Proposed-OUTDENT_UL-LI-1_SW-div": true, + "U-Proposed-OUTDENT_DIV-1_SW-dM": true, + "U-Proposed-OUTDENT_DIV-1_SW-body": true, + "U-Proposed-OUTDENT_DIV-1_SW-div": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-dM": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-body": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-div": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-dM": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-body": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-div": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-dM": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-body": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-div": true, + "UC-Proposed-S_SPANc:s-1_SW-dM": true, + "UC-Proposed-S_SPANc:s-1_SW-body": true, + "UC-Proposed-S_SPANc:s-1_SW-div": true, + "UC-Proposed-S_SPANc:s-2_SI-dM": true, + "UC-Proposed-S_SPANc:s-2_SI-body": true, + "UC-Proposed-S_SPANc:s-2_SI-div": true, + "D-Proposed-CHAR-3_SC-dM": true, + "D-Proposed-CHAR-3_SC-body": true, + "D-Proposed-CHAR-3_SC-div": true, + "D-Proposed-CHAR-4_SC-dM": true, + "D-Proposed-CHAR-4_SC-body": true, + "D-Proposed-CHAR-4_SC-div": true, + "D-Proposed-CHAR-5_SC-dM": true, + "D-Proposed-CHAR-5_SC-body": true, + "D-Proposed-CHAR-5_SC-div": true, + "D-Proposed-CHAR-5_SI-1-dM": true, + "D-Proposed-CHAR-5_SI-1-body": true, + "D-Proposed-CHAR-5_SI-1-div": true, + "D-Proposed-CHAR-5_SI-2-dM": true, + "D-Proposed-CHAR-5_SI-2-body": true, + "D-Proposed-CHAR-5_SI-2-div": true, + "D-Proposed-CHAR-5_SR-dM": true, + "D-Proposed-CHAR-5_SR-body": true, + "D-Proposed-CHAR-5_SR-div": true, + "D-Proposed-CHAR-6_SC-dM": true, + "D-Proposed-CHAR-6_SC-body": true, + "D-Proposed-CHAR-6_SC-div": true, + "D-Proposed-CHAR-7_SC-dM": true, + "D-Proposed-CHAR-7_SC-body": true, + "D-Proposed-CHAR-7_SC-div": true, + "D-Proposed-OL-LI-1_SW-dM": true, + "D-Proposed-OL-LI-1_SW-body": true, + "D-Proposed-OL-LI-1_SW-div": true, + "D-Proposed-TR2rs:2-1_SO1-dM": true, + "D-Proposed-TR2rs:2-1_SO1-body": true, + "D-Proposed-TR2rs:2-1_SO1-div": true, + "D-Proposed-TR2rs:2-1_SO2-dM": true, + "D-Proposed-TR2rs:2-1_SO2-body": true, + "D-Proposed-TR2rs:2-1_SO2-div": true, + "D-Proposed-TR3rs:3-1_SO1-dM": true, + "D-Proposed-TR3rs:3-1_SO1-body": true, + "D-Proposed-TR3rs:3-1_SO1-div": true, + "D-Proposed-TR3rs:3-1_SO2-dM": true, + "D-Proposed-TR3rs:3-1_SO2-body": true, + "D-Proposed-TR3rs:3-1_SO2-div": true, + "D-Proposed-TR3rs:3-1_SO3-dM": true, + "D-Proposed-TR3rs:3-1_SO3-body": true, + "D-Proposed-TR3rs:3-1_SO3-div": true, + "D-Proposed-DIV:ce:false-1_SB-dM": true, + "D-Proposed-DIV:ce:false-1_SB-body": true, + "D-Proposed-DIV:ce:false-1_SB-div": true, + "D-Proposed-DIV:ce:false-1_SL-dM": true, + "D-Proposed-DIV:ce:false-1_SL-body": true, + "D-Proposed-DIV:ce:false-1_SL-div": true, + "D-Proposed-DIV:ce:false-1_SR-dM": true, + "D-Proposed-DIV:ce:false-1_SR-body": true, + "D-Proposed-DIV:ce:false-1_SR-div": true, + "D-Proposed-DIV:ce:false-1_SI-dM": true, + "FD-Proposed-OL-LI-1_SW-dM": true, + "FD-Proposed-OL-LI-1_SW-body": true, + "FD-Proposed-OL-LI-1_SW-div": true, + "FD-Proposed-TR2rs:2-1_SO1-dM": true, + "FD-Proposed-TR2rs:2-1_SO1-body": true, + "FD-Proposed-TR2rs:2-1_SO1-div": true, + "FD-Proposed-TR2rs:2-1_SO2-dM": true, + "FD-Proposed-TR2rs:2-1_SO2-body": true, + "FD-Proposed-TR2rs:2-1_SO2-div": true, + "FD-Proposed-TR3rs:3-1_SO1-dM": true, + "FD-Proposed-TR3rs:3-1_SO1-body": true, + "FD-Proposed-TR3rs:3-1_SO1-div": true, + "FD-Proposed-TR3rs:3-1_SO2-dM": true, + "FD-Proposed-TR3rs:3-1_SO2-body": true, + "FD-Proposed-TR3rs:3-1_SO2-div": true, + "FD-Proposed-TR3rs:3-1_SO3-dM": true, + "FD-Proposed-TR3rs:3-1_SO3-body": true, + "FD-Proposed-TR3rs:3-1_SO3-div": true, + "FD-Proposed-DIV:ce:false-1_SB-dM": true, + "FD-Proposed-DIV:ce:false-1_SB-body": true, + "FD-Proposed-DIV:ce:false-1_SB-div": true, + "FD-Proposed-DIV:ce:false-1_SL-dM": true, + "FD-Proposed-DIV:ce:false-1_SL-body": true, + "FD-Proposed-DIV:ce:false-1_SL-div": true, + "FD-Proposed-DIV:ce:false-1_SR-dM": true, + "FD-Proposed-DIV:ce:false-1_SR-body": true, + "FD-Proposed-DIV:ce:false-1_SR-div": true, + "FD-Proposed-DIV:ce:false-1_SI-dM": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-dM": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-body": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-div": true, + "I-Proposed-IIMG:._IMG-1_SO-dM": true, + "I-Proposed-IIMG:._IMG-1_SO-body": true, + "I-Proposed-IIMG:._IMG-1_SO-div": true, + "Q-Proposed-UNSELECT_TEXT-1-dM": true, + "Q-Proposed-UNSELECT_TEXT-1-body": true, + "Q-Proposed-UNSELECT_TEXT-1-div": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-body": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-div": true, + "Q-Proposed-PASTE_TEXT-1-dM": true, + "Q-Proposed-PASTE_TEXT-1-body": true, + "Q-Proposed-PASTE_TEXT-1-div": true, + "QE-Proposed-UNSELECT_TEXT-1-dM": true, + "QE-Proposed-UNSELECT_TEXT-1-body": true, + "QE-Proposed-UNSELECT_TEXT-1-div": true, + "QE-Proposed-REDO_TEXT-1-dM": true, + "QE-Proposed-REDO_TEXT-1-body": true, + "QE-Proposed-REDO_TEXT-1-div": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-body": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-div": true, + "QE-Proposed-COPY_TEXT-1-dM": true, + "QE-Proposed-COPY_TEXT-1-body": true, + "QE-Proposed-COPY_TEXT-1-div": true, + "QE-Proposed-CUT_TEXT-1-dM": true, + "QE-Proposed-CUT_TEXT-1-body": true, + "QE-Proposed-CUT_TEXT-1-div": true, + "QE-Proposed-PASTE_TEXT-1-dM": true, + "QE-Proposed-PASTE_TEXT-1-body": true, + "QE-Proposed-PASTE_TEXT-1-div": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-dM": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-body": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-div": true, + "QS-Proposed-SUB_MYSUB-1-SI-dM": true, + "QS-Proposed-SUB_MYSUB-1-SI-body": true, + "QS-Proposed-SUB_MYSUB-1-SI-div": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-dM": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-body": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-div": true, + "QS-Proposed-SUP_MYSUP-1-SI-dM": true, + "QS-Proposed-SUP_MYSUP-1-SI-body": true, + "QS-Proposed-SUP_MYSUP-1-SI-div": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-dM": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-body": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-div": true, + "QS-Proposed-JC_SPAN.jc-1-SI-dM": true, + "QS-Proposed-JC_SPAN.jc-1-SI-body": true, + "QS-Proposed-JC_SPAN.jc-1-SI-div": true, + "QS-Proposed-JC_MYJC-1-SI-dM": true, + "QS-Proposed-JC_MYJC-1-SI-body": true, + "QS-Proposed-JC_MYJC-1-SI-div": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-dM": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-body": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-div": true, + "QS-Proposed-JF_SPAN.jf-1-SI-dM": true, + "QS-Proposed-JF_SPAN.jf-1-SI-body": true, + "QS-Proposed-JF_SPAN.jf-1-SI-div": true, + "QS-Proposed-JF_MYJF-1-SI-dM": true, + "QS-Proposed-JF_MYJF-1-SI-body": true, + "QS-Proposed-JF_MYJF-1-SI-div": true, + "QS-Proposed-JL_TEXT_SI-dM": true, + "QS-Proposed-JL_TEXT_SI-body": true, + "QS-Proposed-JL_TEXT_SI-div": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-dM": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-body": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-div": true, + "QS-Proposed-JR_SPAN.jr-1-SI-dM": true, + "QS-Proposed-JR_SPAN.jr-1-SI-body": true, + "QS-Proposed-JR_SPAN.jr-1-SI-div": true, + "QS-Proposed-JR_MYJR-1-SI-dM": true, + "QS-Proposed-JR_MYJR-1-SI-body": true, + "QS-Proposed-JR_MYJR-1-SI-div": true, + "QV-Proposed-B_TEXT_SI-dM": true, + "QV-Proposed-B_TEXT_SI-body": true, + "QV-Proposed-B_TEXT_SI-div": true, + "QV-Proposed-B_B-1_SI-dM": true, + "QV-Proposed-B_B-1_SI-body": true, + "QV-Proposed-B_B-1_SI-div": true, + "QV-Proposed-B_STRONG-1_SI-dM": true, + "QV-Proposed-B_STRONG-1_SI-body": true, + "QV-Proposed-B_STRONG-1_SI-div": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-dM": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-body": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-div": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-dM": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-body": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-div": true, + "QV-Proposed-B_Bs:fw:n-1_SI-dM": true, + "QV-Proposed-B_Bs:fw:n-1_SI-body": true, + "QV-Proposed-B_Bs:fw:n-1_SI-div": true, + "QV-Proposed-B_SPAN.b-1_SI-dM": true, + "QV-Proposed-B_SPAN.b-1_SI-body": true, + "QV-Proposed-B_SPAN.b-1_SI-div": true, + "QV-Proposed-B_MYB-1-SI-dM": true, + "QV-Proposed-B_MYB-1-SI-body": true, + "QV-Proposed-B_MYB-1-SI-div": true, + "QV-Proposed-I_TEXT_SI-dM": true, + "QV-Proposed-I_TEXT_SI-body": true, + "QV-Proposed-I_TEXT_SI-div": true, + "QV-Proposed-I_I-1_SI-dM": true, + "QV-Proposed-I_I-1_SI-body": true, + "QV-Proposed-I_I-1_SI-div": true, + "QV-Proposed-I_EM-1_SI-dM": true, + "QV-Proposed-I_EM-1_SI-body": true, + "QV-Proposed-I_EM-1_SI-div": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-dM": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-body": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-div": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-dM": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-body": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-div": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-dM": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-body": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-div": true, + "QV-Proposed-I_SPAN.i-1_SI-dM": true, + "QV-Proposed-I_SPAN.i-1_SI-body": true, + "QV-Proposed-I_SPAN.i-1_SI-div": true, + "QV-Proposed-I_MYI-1-SI-dM": true, + "QV-Proposed-I_MYI-1-SI-body": true, + "QV-Proposed-I_MYI-1-SI-div": true, + "QV-Proposed-FB_BQ-1_SC-dM": true, + "QV-Proposed-FB_BQ-1_SC-body": true, + "QV-Proposed-FB_BQ-1_SC-div": true, + "QV-Proposed-FB_H1-H2-1_SL-dM": true, + "QV-Proposed-FB_H1-H2-1_SL-body": true, + "QV-Proposed-FB_H1-H2-1_SL-div": true, + "QV-Proposed-FB_H1-H2-1_SR-dM": true, + "QV-Proposed-FB_H1-H2-1_SR-body": true, + "QV-Proposed-FB_H1-H2-1_SR-div": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-dM": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-body": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-div": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-dM": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-body": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-div": true, + "QV-Proposed-H_P-1_SC-dM": true, + "QV-Proposed-H_P-1_SC-body": true, + "QV-Proposed-H_P-1_SC-div": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-dM": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-body": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-div": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-dM": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-body": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-div": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-dM": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-body": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-div": true, + "QV-Proposed-FS_SPAN.large-1_SI-dM": true, + "QV-Proposed-FS_SPAN.large-1_SI-body": true, + "QV-Proposed-FS_SPAN.large-1_SI-div": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-dM": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-body": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-div": true, + "QV-Proposed-FA_MYLARGE-1-SI-dM": true, + "QV-Proposed-FA_MYLARGE-1-SI-body": true, + "QV-Proposed-FA_MYLARGE-1-SI-div": true, + "QV-Proposed-FA_MYFS18PX-1-SI-dM": true, + "QV-Proposed-FA_MYFS18PX-1-SI-body": true, + "QV-Proposed-FA_MYFS18PX-1-SI-div": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-dM": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-body": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-div": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-dM": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-body": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-div": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-dM": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-body": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-div": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-dM": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-body": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-div": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-dM": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-body": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-div": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-dM": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-body": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-div": true, + "QV-Proposed-BC_MYBCRED-1-SI-dM": true, + "QV-Proposed-BC_MYBCRED-1-SI-body": true, + "QV-Proposed-BC_MYBCRED-1-SI-div": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-dM": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-body": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-div": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-dM": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-body": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-div": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-dM": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-body": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-div": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-dM": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-body": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-div": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-dM": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-body": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-div": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-dM": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-body": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-div": true, + "QV-Proposed-HC_MYBCRED-1-SI-dM": true, + "QV-Proposed-HC_MYBCRED-1-SI-body": true, + "QV-Proposed-HC_MYBCRED-1-SI-div": true + }, + "select": { + "S-Proposed-UNSEL_TEXT-1_SI-dM": true, + "S-Proposed-UNSEL_TEXT-1_SI-body": true, + "S-Proposed-UNSEL_TEXT-1_SI-div": true, + "S-Proposed-SM:m.f.c_TEXT-1_SI-1-dM": true, + "S-Proposed-SM:m.f.c_TEXT-1_SI-1-body": true, + "S-Proposed-SM:m.f.c_TEXT-1_SI-1-div": true, + "S-Proposed-SM:m.b.c_TEXT-1_SI-1-dM": true, + "S-Proposed-SM:m.b.c_TEXT-1_SI-1-body": true, + "S-Proposed-SM:m.b.c_TEXT-1_SI-1-div": true, + "S-Proposed-SM:m.b.w_TEXT-1_SI-1-dM": true, + "S-Proposed-SM:m.b.w_TEXT-1_SI-1-body": true, + "S-Proposed-SM:m.b.w_TEXT-1_SI-1-div": true, + "S-Proposed-SM:m.f.c_CHAR-5_SI-2-dM": true, + "S-Proposed-SM:m.f.c_CHAR-5_SI-2-body": true, + "S-Proposed-SM:m.f.c_CHAR-5_SI-2-div": true, + "S-Proposed-SM:m.f.c_CHAR-5_SR-dM": true, + "S-Proposed-SM:m.f.c_CHAR-5_SR-body": true, + "S-Proposed-SM:m.f.c_CHAR-5_SR-div": true, + "S-Proposed-SM:m.b.c_CHAR-5_SR-dM": true, + "S-Proposed-SM:m.b.c_CHAR-5_SR-body": true, + "S-Proposed-SM:m.b.c_CHAR-5_SR-div": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-dM": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-body": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-div": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-dM": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-body": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-div": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-dM": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-body": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-div": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-3-dM": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-3-body": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-3-div": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-4-dM": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-4-body": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-4-div": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-5-dM": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-5-body": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-5-div": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-dM": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-body": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-div": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-dM": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-body": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-div": true, + "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-dM": true, + "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-body": true, + "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-div": true, + "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-dM": true, + "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-body": true, + "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-div": true, + "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-dM": true, + "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-body": true, + "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-div": true, + "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-dM": true, + "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-body": true, + "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-div": true, + "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-dM": true, + "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-body": true, + "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-div": true, + "A-Proposed-B_TEXT-1_SI-dM": true, + "A-Proposed-B_TEXT-1_SI-body": true, + "A-Proposed-B_TEXT-1_SI-div": true, + "A-Proposed-B_TEXT-1_SIR-dM": true, + "A-Proposed-B_TEXT-1_SIR-body": true, + "A-Proposed-B_TEXT-1_SIR-div": true, + "A-Proposed-B_I-1_SL-dM": true, + "A-Proposed-B_I-1_SL-body": true, + "A-Proposed-B_I-1_SL-div": true, + "A-Proposed-I_TEXT-1_SI-dM": true, + "A-Proposed-I_TEXT-1_SI-body": true, + "A-Proposed-I_TEXT-1_SI-div": true, + "A-Proposed-U_TEXT-1_SI-dM": true, + "A-Proposed-U_TEXT-1_SI-body": true, + "A-Proposed-U_TEXT-1_SI-div": true, + "A-Proposed-S_TEXT-1_SI-dM": true, + "A-Proposed-S_TEXT-1_SI-body": true, + "A-Proposed-S_TEXT-1_SI-div": true, + "A-Proposed-SUB_TEXT-1_SI-dM": true, + "A-Proposed-SUB_TEXT-1_SI-body": true, + "A-Proposed-SUB_TEXT-1_SI-div": true, + "A-Proposed-SUP_TEXT-1_SI-dM": true, + "A-Proposed-SUP_TEXT-1_SI-body": true, + "A-Proposed-SUP_TEXT-1_SI-div": true, + "A-Proposed-CL:url_TEXT-1_SI-dM": true, + "A-Proposed-CL:url_TEXT-1_SI-body": true, + "A-Proposed-CL:url_TEXT-1_SI-div": true, + "A-Proposed-BC:blue_TEXT-1_SI-dM": true, + "A-Proposed-BC:blue_TEXT-1_SI-body": true, + "A-Proposed-BC:blue_TEXT-1_SI-div": true, + "A-Proposed-FC:blue_TEXT-1_SI-dM": true, + "A-Proposed-FC:blue_TEXT-1_SI-body": true, + "A-Proposed-FC:blue_TEXT-1_SI-div": true, + "A-Proposed-HC:blue_TEXT-1_SI-dM": true, + "A-Proposed-HC:blue_TEXT-1_SI-body": true, + "A-Proposed-HC:blue_TEXT-1_SI-div": true, + "A-Proposed-FN:a_TEXT-1_SI-dM": true, + "A-Proposed-FN:a_TEXT-1_SI-body": true, + "A-Proposed-FN:a_TEXT-1_SI-div": true, + "A-Proposed-FS:2_TEXT-1_SI-dM": true, + "A-Proposed-FS:2_TEXT-1_SI-body": true, + "A-Proposed-FS:2_TEXT-1_SI-div": true, + "A-Proposed-FS:18px_TEXT-1_SI-dM": true, + "A-Proposed-FS:18px_TEXT-1_SI-body": true, + "A-Proposed-FS:18px_TEXT-1_SI-div": true, + "A-Proposed-FS:large_TEXT-1_SI-dM": true, + "A-Proposed-FS:large_TEXT-1_SI-body": true, + "A-Proposed-FS:large_TEXT-1_SI-div": true, + "A-Proposed-INCFS:2_TEXT-1_SI-dM": true, + "A-Proposed-INCFS:2_TEXT-1_SI-body": true, + "A-Proposed-INCFS:2_TEXT-1_SI-div": true, + "A-Proposed-DECFS:2_TEXT-1_SI-dM": true, + "A-Proposed-DECFS:2_TEXT-1_SI-body": true, + "A-Proposed-DECFS:2_TEXT-1_SI-div": true, + "A-Proposed-CB:name_TEXT-1_SI-dM": true, + "A-Proposed-CB:name_TEXT-1_SI-body": true, + "A-Proposed-CB:name_TEXT-1_SI-div": true, + "AC-Proposed-B_TEXT-1_SI-dM": true, + "AC-Proposed-B_TEXT-1_SI-body": true, + "AC-Proposed-B_TEXT-1_SI-div": true, + "AC-Proposed-I_TEXT-1_SI-dM": true, + "AC-Proposed-I_TEXT-1_SI-body": true, + "AC-Proposed-I_TEXT-1_SI-div": true, + "AC-Proposed-U_TEXT-1_SI-dM": true, + "AC-Proposed-U_TEXT-1_SI-body": true, + "AC-Proposed-U_TEXT-1_SI-div": true, + "AC-Proposed-S_TEXT-1_SI-dM": true, + "AC-Proposed-S_TEXT-1_SI-body": true, + "AC-Proposed-S_TEXT-1_SI-div": true, + "AC-Proposed-SUB_TEXT-1_SI-dM": true, + "AC-Proposed-SUB_TEXT-1_SI-body": true, + "AC-Proposed-SUB_TEXT-1_SI-div": true, + "AC-Proposed-SUP_TEXT-1_SI-dM": true, + "AC-Proposed-SUP_TEXT-1_SI-body": true, + "AC-Proposed-SUP_TEXT-1_SI-div": true, + "AC-Proposed-BC:blue_TEXT-1_SI-dM": true, + "AC-Proposed-BC:blue_TEXT-1_SI-body": true, + "AC-Proposed-BC:blue_TEXT-1_SI-div": true, + "AC-Proposed-FC:blue_TEXT-1_SI-dM": true, + "AC-Proposed-FC:blue_TEXT-1_SI-body": true, + "AC-Proposed-FC:blue_TEXT-1_SI-div": true, + "AC-Proposed-HC:blue_TEXT-1_SI-dM": true, + "AC-Proposed-HC:blue_TEXT-1_SI-body": true, + "AC-Proposed-HC:blue_TEXT-1_SI-div": true, + "AC-Proposed-FN:a_TEXT-1_SI-dM": true, + "AC-Proposed-FN:a_TEXT-1_SI-body": true, + "AC-Proposed-FN:a_TEXT-1_SI-div": true, + "AC-Proposed-FS:2_TEXT-1_SI-dM": true, + "AC-Proposed-FS:2_TEXT-1_SI-body": true, + "AC-Proposed-FS:2_TEXT-1_SI-div": true, + "AC-Proposed-FS:18px_TEXT-1_SI-dM": true, + "AC-Proposed-FS:18px_TEXT-1_SI-body": true, + "AC-Proposed-FS:18px_TEXT-1_SI-div": true, + "AC-Proposed-FS:large_TEXT-1_SI-dM": true, + "AC-Proposed-FS:large_TEXT-1_SI-body": true, + "AC-Proposed-FS:large_TEXT-1_SI-div": true, + "C-Proposed-I_I-1_SL-dM": true, + "C-Proposed-I_I-1_SL-body": true, + "C-Proposed-I_I-1_SL-div": true, + "C-Proposed-I_B-I-1_SO-dM": true, + "C-Proposed-I_B-I-1_SO-body": true, + "C-Proposed-I_B-I-1_SO-div": true, + "C-Proposed-U_U-1_SO-dM": true, + "C-Proposed-U_U-1_SO-body": true, + "C-Proposed-U_U-1_SO-div": true, + "C-Proposed-U_U-1_SL-dM": true, + "C-Proposed-U_U-1_SL-body": true, + "C-Proposed-U_U-1_SL-div": true, + "C-Proposed-U_S-U-1_SO-dM": true, + "C-Proposed-U_S-U-1_SO-body": true, + "C-Proposed-U_S-U-1_SO-div": true, + "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-dM": true, + "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-body": true, + "C-Proposed-BC:ace_FONT.ass.s:bc:rgb-1_SW-div": true, + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-dM": true, + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-body": true, + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-div": true, + "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-dM": true, + "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-body": true, + "C-Proposed-HC:g_SPAN.ass.s:c:rgb-1_SW-div": true, + "C-Proposed-FN:c_FONTf:a-1_SI-dM": true, + "C-Proposed-FN:c_FONTf:a-1_SI-body": true, + "C-Proposed-FN:c_FONTf:a-1_SI-div": true, + "C-Proposed-FN:c_FONTf:a-2_SL-dM": true, + "C-Proposed-FN:c_FONTf:a-2_SL-body": true, + "C-Proposed-FN:c_FONTf:a-2_SL-div": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-dM": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-body": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-div": true, + "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-dM": true, + "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-body": true, + "C-Proposed-FS:5_FONTsz:1.s:fs:xs-1_SW-div": true, + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-dM": true, + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-body": true, + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-div": true, + "C-Proposed-FS:larger_FONTsz:4-dM": true, + "C-Proposed-FS:larger_FONTsz:4-body": true, + "C-Proposed-FS:larger_FONTsz:4-div": true, + "C-Proposed-FS:smaller_FONTsz:4-dM": true, + "C-Proposed-FS:smaller_FONTsz:4-body": true, + "C-Proposed-FS:smaller_FONTsz:4-div": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-body": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-div": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-body": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-div": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-body": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-div": true, + "CC-Proposed-I_I-1_SL-dM": true, + "CC-Proposed-I_I-1_SL-body": true, + "CC-Proposed-I_I-1_SL-div": true, + "CC-Proposed-I_B-1_SL-dM": true, + "CC-Proposed-I_B-1_SL-body": true, + "CC-Proposed-I_B-1_SL-div": true, + "CC-Proposed-I_B-1_SW-dM": true, + "CC-Proposed-I_B-1_SW-body": true, + "CC-Proposed-I_B-1_SW-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-div": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-dM": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-body": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-div": true, + "CC-Proposed-FN:c_FONTf:a-1_SI-dM": true, + "CC-Proposed-FN:c_FONTf:a-1_SI-body": true, + "CC-Proposed-FN:c_FONTf:a-1_SI-div": true, + "CC-Proposed-FN:c_FONTf:a-2_SL-dM": true, + "CC-Proposed-FN:c_FONTf:a-2_SL-body": true, + "CC-Proposed-FN:c_FONTf:a-2_SL-div": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-dM": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-body": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-div": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-dM": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-body": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-div": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-dM": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-body": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-div": true, + "U-RFC-UNLINK_A-1_SO-dM": true, + "U-RFC-UNLINK_A-1_SO-body": true, + "U-RFC-UNLINK_A-1_SO-div": true, + "U-RFC-UNLINK_A-1_SW-dM": true, + "U-RFC-UNLINK_A-1_SW-body": true, + "U-RFC-UNLINK_A-1_SW-div": true, + "U-RFC-UNLINK_A-2_SO-dM": true, + "U-RFC-UNLINK_A-2_SO-body": true, + "U-RFC-UNLINK_A-2_SO-div": true, + "U-RFC-UNLINK_A2-1_SO-dM": true, + "U-RFC-UNLINK_A2-1_SO-body": true, + "U-RFC-UNLINK_A2-1_SO-div": true, + "U-Proposed-B_B-P3-1_SO12-dM": true, + "U-Proposed-B_B-P3-1_SO12-body": true, + "U-Proposed-B_B-P3-1_SO12-div": true, + "U-Proposed-B_B-P-I..P-1_SO-I-dM": true, + "U-Proposed-B_B-P-I..P-1_SO-I-body": true, + "U-Proposed-B_B-P-I..P-1_SO-I-div": true, + "U-Proposed-B_B-2_SL-dM": true, + "U-Proposed-B_B-2_SL-body": true, + "U-Proposed-B_B-2_SL-div": true, + "U-Proposed-B_B-2_SR-dM": true, + "U-Proposed-B_B-2_SR-body": true, + "U-Proposed-B_B-2_SR-div": true, + "U-Proposed-I_I-P3-1_SO2-dM": true, + "U-Proposed-I_I-P3-1_SO2-body": true, + "U-Proposed-I_I-P3-1_SO2-div": true, + "U-Proposed-U_U-S-1_SO-dM": true, + "U-Proposed-U_U-S-1_SO-body": true, + "U-Proposed-U_U-S-1_SO-div": true, + "U-Proposed-U_U-S-2_SI-dM": true, + "U-Proposed-U_U-S-2_SI-body": true, + "U-Proposed-U_U-S-2_SI-div": true, + "U-Proposed-U_U-P3-1_SO-dM": true, + "U-Proposed-U_U-P3-1_SO-body": true, + "U-Proposed-U_U-P3-1_SO-div": true, + "U-Proposed-S_DEL-1_SW-dM": true, + "U-Proposed-S_DEL-1_SW-body": true, + "U-Proposed-S_DEL-1_SW-div": true, + "U-Proposed-S_S-U-1_SI-dM": true, + "U-Proposed-S_S-U-1_SI-body": true, + "U-Proposed-S_S-U-1_SI-div": true, + "U-Proposed-S_U-S-1_SI-dM": true, + "U-Proposed-S_U-S-1_SI-body": true, + "U-Proposed-S_U-S-1_SI-div": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-dM": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-body": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-div": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-dM": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-body": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-div": true, + "U-Proposed-UNLINK_A-1_SC-dM": true, + "U-Proposed-UNLINK_A-1_SC-body": true, + "U-Proposed-UNLINK_A-1_SC-div": true, + "U-Proposed-UNLINK_A-1_SI-dM": true, + "U-Proposed-UNLINK_A-1_SI-body": true, + "U-Proposed-UNLINK_A-1_SI-div": true, + "U-Proposed-UNLINK_A-2_SL-dM": true, + "U-Proposed-UNLINK_A-2_SL-body": true, + "U-Proposed-UNLINK_A-2_SL-div": true, + "U-Proposed-UNLINK_A-3_SR-dM": true, + "U-Proposed-UNLINK_A-3_SR-body": true, + "U-Proposed-UNLINK_A-3_SR-div": true, + "U-Proposed-OUTDENT_DIV-1_SW-dM": true, + "U-Proposed-OUTDENT_DIV-1_SW-body": true, + "U-Proposed-OUTDENT_DIV-1_SW-div": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-dM": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-body": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-div": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-dM": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-body": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-div": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-dM": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-body": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-div": true, + "UC-Proposed-S_SPANc:s-1_SW-dM": true, + "UC-Proposed-S_SPANc:s-1_SW-body": true, + "UC-Proposed-S_SPANc:s-1_SW-div": true, + "UC-Proposed-S_SPANc:s-2_SI-dM": true, + "UC-Proposed-S_SPANc:s-2_SI-body": true, + "UC-Proposed-S_SPANc:s-2_SI-div": true, + "D-Proposed-CHAR-3_SC-dM": true, + "D-Proposed-CHAR-3_SC-body": true, + "D-Proposed-CHAR-3_SC-div": true, + "D-Proposed-CHAR-4_SC-dM": true, + "D-Proposed-CHAR-4_SC-body": true, + "D-Proposed-CHAR-4_SC-div": true, + "D-Proposed-CHAR-5_SC-dM": true, + "D-Proposed-CHAR-5_SC-body": true, + "D-Proposed-CHAR-5_SC-div": true, + "D-Proposed-CHAR-5_SI-1-dM": true, + "D-Proposed-CHAR-5_SI-1-body": true, + "D-Proposed-CHAR-5_SI-1-div": true, + "D-Proposed-CHAR-5_SI-2-dM": true, + "D-Proposed-CHAR-5_SI-2-body": true, + "D-Proposed-CHAR-5_SI-2-div": true, + "D-Proposed-CHAR-5_SR-dM": true, + "D-Proposed-CHAR-5_SR-body": true, + "D-Proposed-CHAR-5_SR-div": true, + "D-Proposed-CHAR-6_SC-dM": true, + "D-Proposed-CHAR-6_SC-body": true, + "D-Proposed-CHAR-6_SC-div": true, + "D-Proposed-CHAR-7_SC-dM": true, + "D-Proposed-CHAR-7_SC-body": true, + "D-Proposed-CHAR-7_SC-div": true, + "D-Proposed-B-1_SW-div": true, + "D-Proposed-B-1_SL-dM": true, + "D-Proposed-B-1_SL-body": true, + "D-Proposed-B-1_SL-div": true, + "D-Proposed-B-1_SR-dM": true, + "D-Proposed-B-1_SR-body": true, + "D-Proposed-B-1_SR-div": true, + "D-Proposed-B.I-1_SM-dM": true, + "D-Proposed-B.I-1_SM-body": true, + "D-Proposed-B.I-1_SM-div": true, + "D-Proposed-OL-LI2-1_SO1-dM": true, + "D-Proposed-OL-LI2-1_SO1-body": true, + "D-Proposed-OL-LI2-1_SO1-div": true, + "D-Proposed-OL-LI-1_SW-dM": true, + "D-Proposed-OL-LI-1_SW-body": true, + "D-Proposed-OL-LI-1_SW-div": true, + "D-Proposed-OL-LI-1_SO-dM": true, + "D-Proposed-OL-LI-1_SO-body": true, + "D-Proposed-OL-LI-1_SO-div": true, + "D-Proposed-HR.BR-1_SM-dM": true, + "D-Proposed-HR.BR-1_SM-body": true, + "D-Proposed-HR.BR-1_SM-div": true, + "D-Proposed-TR2rs:2-1_SO1-dM": true, + "D-Proposed-TR2rs:2-1_SO1-body": true, + "D-Proposed-TR2rs:2-1_SO1-div": true, + "D-Proposed-TR2rs:2-1_SO2-dM": true, + "D-Proposed-TR2rs:2-1_SO2-body": true, + "D-Proposed-TR2rs:2-1_SO2-div": true, + "D-Proposed-TR3rs:3-1_SO1-dM": true, + "D-Proposed-TR3rs:3-1_SO1-body": true, + "D-Proposed-TR3rs:3-1_SO1-div": true, + "D-Proposed-TR3rs:3-1_SO2-dM": true, + "D-Proposed-TR3rs:3-1_SO2-body": true, + "D-Proposed-TR3rs:3-1_SO2-div": true, + "D-Proposed-TR3rs:3-1_SO3-dM": true, + "D-Proposed-TR3rs:3-1_SO3-body": true, + "D-Proposed-TR3rs:3-1_SO3-div": true, + "D-Proposed-DIV:ce:false-1_SB-dM": true, + "D-Proposed-DIV:ce:false-1_SB-body": true, + "D-Proposed-DIV:ce:false-1_SB-div": true, + "D-Proposed-DIV:ce:false-1_SL-dM": true, + "D-Proposed-DIV:ce:false-1_SL-body": true, + "D-Proposed-DIV:ce:false-1_SL-div": true, + "D-Proposed-DIV:ce:false-1_SR-dM": true, + "D-Proposed-DIV:ce:false-1_SR-body": true, + "D-Proposed-DIV:ce:false-1_SR-div": true, + "D-Proposed-DIV:ce:false-1_SI-dM": true, + "D-Proposed-SPAN:d:ib-2_SL-dM": true, + "D-Proposed-SPAN:d:ib-2_SL-body": true, + "D-Proposed-SPAN:d:ib-2_SL-div": true, + "D-Proposed-SPAN:d:ib-3_SR-dM": true, + "D-Proposed-SPAN:d:ib-3_SR-body": true, + "D-Proposed-SPAN:d:ib-3_SR-div": true, + "FD-Proposed-B-1_SW-div": true, + "FD-Proposed-OL-LI-1_SW-dM": true, + "FD-Proposed-OL-LI-1_SW-body": true, + "FD-Proposed-OL-LI-1_SW-div": true, + "FD-Proposed-OL-LI-1_SO-dM": true, + "FD-Proposed-OL-LI-1_SO-body": true, + "FD-Proposed-OL-LI-1_SO-div": true, + "FD-Proposed-TABLE-1_SB-dM": true, + "FD-Proposed-TABLE-1_SB-body": true, + "FD-Proposed-TABLE-1_SB-div": true, + "FD-Proposed-TD-1_SE-dM": true, + "FD-Proposed-TD-1_SE-body": true, + "FD-Proposed-TD-1_SE-div": true, + "FD-Proposed-TD2-1_SE1-dM": true, + "FD-Proposed-TD2-1_SE1-body": true, + "FD-Proposed-TD2-1_SE1-div": true, + "FD-Proposed-TD2-1_SM-dM": true, + "FD-Proposed-TD2-1_SM-body": true, + "FD-Proposed-TD2-1_SM-div": true, + "FD-Proposed-TR2rs:2-1_SO1-dM": true, + "FD-Proposed-TR2rs:2-1_SO1-body": true, + "FD-Proposed-TR2rs:2-1_SO1-div": true, + "FD-Proposed-TR2rs:2-1_SO2-dM": true, + "FD-Proposed-TR2rs:2-1_SO2-body": true, + "FD-Proposed-TR2rs:2-1_SO2-div": true, + "FD-Proposed-TR3rs:3-1_SO1-dM": true, + "FD-Proposed-TR3rs:3-1_SO1-body": true, + "FD-Proposed-TR3rs:3-1_SO1-div": true, + "FD-Proposed-TR3rs:3-1_SO2-dM": true, + "FD-Proposed-TR3rs:3-1_SO2-body": true, + "FD-Proposed-TR3rs:3-1_SO2-div": true, + "FD-Proposed-TR3rs:3-1_SO3-dM": true, + "FD-Proposed-TR3rs:3-1_SO3-body": true, + "FD-Proposed-TR3rs:3-1_SO3-div": true, + "FD-Proposed-DIV:ce:false-1_SB-dM": true, + "FD-Proposed-DIV:ce:false-1_SB-body": true, + "FD-Proposed-DIV:ce:false-1_SB-div": true, + "FD-Proposed-DIV:ce:false-1_SL-dM": true, + "FD-Proposed-DIV:ce:false-1_SL-body": true, + "FD-Proposed-DIV:ce:false-1_SL-div": true, + "FD-Proposed-DIV:ce:false-1_SR-dM": true, + "FD-Proposed-DIV:ce:false-1_SR-body": true, + "FD-Proposed-DIV:ce:false-1_SR-div": true, + "FD-Proposed-DIV:ce:false-1_SI-dM": true, + "I-Proposed-IHR_TEXT-1_SC-dM": true, + "I-Proposed-IHR_TEXT-1_SC-body": true, + "I-Proposed-IHR_TEXT-1_SC-div": true, + "I-Proposed-IHR_TEXT-1_SI-dM": true, + "I-Proposed-IHR_TEXT-1_SI-body": true, + "I-Proposed-IHR_TEXT-1_SI-div": true, + "I-Proposed-IHR_B-1_SC-dM": true, + "I-Proposed-IHR_B-1_SC-body": true, + "I-Proposed-IHR_B-1_SC-div": true, + "I-Proposed-IHR_B-1_SS-dM": true, + "I-Proposed-IHR_B-1_SS-body": true, + "I-Proposed-IHR_B-1_SS-div": true, + "I-Proposed-IHR_B-I-1_SMR-dM": true, + "I-Proposed-IHR_B-I-1_SMR-body": true, + "I-Proposed-IHR_B-I-1_SMR-div": true, + "I-Proposed-IBR_LI-1_SC-dM": true, + "I-Proposed-IBR_LI-1_SC-body": true, + "I-Proposed-IBR_LI-1_SC-div": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-dM": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-body": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-div": true, + "I-Proposed-IIMG:._IMG-1_SO-dM": true, + "I-Proposed-IIMG:._IMG-1_SO-body": true, + "I-Proposed-IIMG:._IMG-1_SO-div": true, + "I-Proposed-IHTML:BR_TEXT-1_SC-dM": true, + "I-Proposed-IHTML:BR_TEXT-1_SC-body": true, + "I-Proposed-IHTML:BR_TEXT-1_SC-div": true, + "I-Proposed-IHTML:S_TEXT-1_SI-dM": true, + "I-Proposed-IHTML:S_TEXT-1_SI-body": true, + "I-Proposed-IHTML:S_TEXT-1_SI-div": true, + "I-Proposed-IHTML:H1.H2_TEXT-1_SI-dM": true, + "I-Proposed-IHTML:H1.H2_TEXT-1_SI-body": true, + "I-Proposed-IHTML:H1.H2_TEXT-1_SI-div": true, + "I-Proposed-IHTML:P-B_TEXT-1_SI-dM": true, + "I-Proposed-IHTML:P-B_TEXT-1_SI-body": true, + "I-Proposed-IHTML:P-B_TEXT-1_SI-div": true, + "Q-Proposed-SELECTALL_TEXT-1-dM": true, + "Q-Proposed-SELECTALL_TEXT-1-body": true, + "Q-Proposed-SELECTALL_TEXT-1-div": true, + "Q-Proposed-UNSELECT_TEXT-1-dM": true, + "Q-Proposed-UNSELECT_TEXT-1-body": true, + "Q-Proposed-UNSELECT_TEXT-1-div": true, + "Q-Proposed-UNDO_TEXT-1-dM": true, + "Q-Proposed-UNDO_TEXT-1-body": true, + "Q-Proposed-UNDO_TEXT-1-div": true, + "Q-Proposed-REDO_TEXT-1-dM": true, + "Q-Proposed-REDO_TEXT-1-body": true, + "Q-Proposed-REDO_TEXT-1-div": true, + "Q-Proposed-BOLD_TEXT-1-dM": true, + "Q-Proposed-BOLD_TEXT-1-body": true, + "Q-Proposed-BOLD_TEXT-1-div": true, + "Q-Proposed-BOLD_B-dM": true, + "Q-Proposed-BOLD_B-body": true, + "Q-Proposed-BOLD_B-div": true, + "Q-Proposed-ITALIC_TEXT-1-dM": true, + "Q-Proposed-ITALIC_TEXT-1-body": true, + "Q-Proposed-ITALIC_TEXT-1-div": true, + "Q-Proposed-ITALIC_I-dM": true, + "Q-Proposed-ITALIC_I-body": true, + "Q-Proposed-ITALIC_I-div": true, + "Q-Proposed-UNDERLINE_TEXT-1-dM": true, + "Q-Proposed-UNDERLINE_TEXT-1-body": true, + "Q-Proposed-UNDERLINE_TEXT-1-div": true, + "Q-Proposed-STRIKETHROUGH_TEXT-1-dM": true, + "Q-Proposed-STRIKETHROUGH_TEXT-1-body": true, + "Q-Proposed-STRIKETHROUGH_TEXT-1-div": true, + "Q-Proposed-SUBSCRIPT_TEXT-1-dM": true, + "Q-Proposed-SUBSCRIPT_TEXT-1-body": true, + "Q-Proposed-SUBSCRIPT_TEXT-1-div": true, + "Q-Proposed-SUPERSCRIPT_TEXT-1-dM": true, + "Q-Proposed-SUPERSCRIPT_TEXT-1-body": true, + "Q-Proposed-SUPERSCRIPT_TEXT-1-div": true, + "Q-Proposed-FORMATBLOCK_TEXT-1-dM": true, + "Q-Proposed-FORMATBLOCK_TEXT-1-body": true, + "Q-Proposed-FORMATBLOCK_TEXT-1-div": true, + "Q-Proposed-CREATELINK_TEXT-1-dM": true, + "Q-Proposed-CREATELINK_TEXT-1-body": true, + "Q-Proposed-CREATELINK_TEXT-1-div": true, + "Q-Proposed-UNLINK_TEXT-1-dM": true, + "Q-Proposed-UNLINK_TEXT-1-body": true, + "Q-Proposed-UNLINK_TEXT-1-div": true, + "Q-Proposed-INSERTHTML_TEXT-1-dM": true, + "Q-Proposed-INSERTHTML_TEXT-1-body": true, + "Q-Proposed-INSERTHTML_TEXT-1-div": true, + "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true, + "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true, + "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true, + "Q-Proposed-INSERTIMAGE_TEXT-1-dM": true, + "Q-Proposed-INSERTIMAGE_TEXT-1-body": true, + "Q-Proposed-INSERTIMAGE_TEXT-1-div": true, + "Q-Proposed-INSERTLINEBREAK_TEXT-1-dM": true, + "Q-Proposed-INSERTLINEBREAK_TEXT-1-body": true, + "Q-Proposed-INSERTLINEBREAK_TEXT-1-div": true, + "Q-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true, + "Q-Proposed-INSERTPARAGRAPH_TEXT-1-body": true, + "Q-Proposed-INSERTPARAGRAPH_TEXT-1-div": true, + "Q-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true, + "Q-Proposed-INSERTORDEREDLIST_TEXT-1-body": true, + "Q-Proposed-INSERTORDEREDLIST_TEXT-1-div": true, + "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true, + "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true, + "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true, + "Q-Proposed-INSERTTEXT_TEXT-1-dM": true, + "Q-Proposed-INSERTTEXT_TEXT-1-body": true, + "Q-Proposed-INSERTTEXT_TEXT-1-div": true, + "Q-Proposed-DELETE_TEXT-1-dM": true, + "Q-Proposed-DELETE_TEXT-1-body": true, + "Q-Proposed-DELETE_TEXT-1-div": true, + "Q-Proposed-FORWARDDELETE_TEXT-1-dM": true, + "Q-Proposed-FORWARDDELETE_TEXT-1-body": true, + "Q-Proposed-FORWARDDELETE_TEXT-1-div": true, + "Q-Proposed-STYLEWITHCSS_TEXT-1-dM": true, + "Q-Proposed-STYLEWITHCSS_TEXT-1-body": true, + "Q-Proposed-STYLEWITHCSS_TEXT-1-div": true, + "Q-Proposed-CONTENTREADONLY_TEXT-1-dM": true, + "Q-Proposed-CONTENTREADONLY_TEXT-1-body": true, + "Q-Proposed-CONTENTREADONLY_TEXT-1-div": true, + "Q-Proposed-BACKCOLOR_TEXT-1-dM": true, + "Q-Proposed-BACKCOLOR_TEXT-1-body": true, + "Q-Proposed-BACKCOLOR_TEXT-1-div": true, + "Q-Proposed-FORECOLOR_TEXT-1-dM": true, + "Q-Proposed-FORECOLOR_TEXT-1-body": true, + "Q-Proposed-FORECOLOR_TEXT-1-div": true, + "Q-Proposed-HILITECOLOR_TEXT-1-dM": true, + "Q-Proposed-HILITECOLOR_TEXT-1-body": true, + "Q-Proposed-HILITECOLOR_TEXT-1-div": true, + "Q-Proposed-FONTNAME_TEXT-1-dM": true, + "Q-Proposed-FONTNAME_TEXT-1-body": true, + "Q-Proposed-FONTNAME_TEXT-1-div": true, + "Q-Proposed-FONTSIZE_TEXT-1-dM": true, + "Q-Proposed-FONTSIZE_TEXT-1-body": true, + "Q-Proposed-FONTSIZE_TEXT-1-div": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-body": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-div": true, + "Q-Proposed-HEADING_TEXT-1-dM": true, + "Q-Proposed-HEADING_TEXT-1-body": true, + "Q-Proposed-HEADING_TEXT-1-div": true, + "Q-Proposed-INDENT_TEXT-1-dM": true, + "Q-Proposed-INDENT_TEXT-1-body": true, + "Q-Proposed-INDENT_TEXT-1-div": true, + "Q-Proposed-OUTDENT_TEXT-1-dM": true, + "Q-Proposed-OUTDENT_TEXT-1-body": true, + "Q-Proposed-OUTDENT_TEXT-1-div": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-body": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-div": true, + "Q-Proposed-JUSTIFYCENTER_TEXT-1-dM": true, + "Q-Proposed-JUSTIFYCENTER_TEXT-1-body": true, + "Q-Proposed-JUSTIFYCENTER_TEXT-1-div": true, + "Q-Proposed-JUSTIFYFULL_TEXT-1-dM": true, + "Q-Proposed-JUSTIFYFULL_TEXT-1-body": true, + "Q-Proposed-JUSTIFYFULL_TEXT-1-div": true, + "Q-Proposed-JUSTIFYLEFT_TEXT-1-dM": true, + "Q-Proposed-JUSTIFYLEFT_TEXT-1-body": true, + "Q-Proposed-JUSTIFYLEFT_TEXT-1-div": true, + "Q-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true, + "Q-Proposed-JUSTIFYRIGHT_TEXT-1-body": true, + "Q-Proposed-JUSTIFYRIGHT_TEXT-1-div": true, + "Q-Proposed-REMOVEFORMAT_TEXT-1-dM": true, + "Q-Proposed-REMOVEFORMAT_TEXT-1-body": true, + "Q-Proposed-REMOVEFORMAT_TEXT-1-div": true, + "Q-Proposed-COPY_TEXT-1-dM": true, + "Q-Proposed-COPY_TEXT-1-body": true, + "Q-Proposed-COPY_TEXT-1-div": true, + "Q-Proposed-CUT_TEXT-1-dM": true, + "Q-Proposed-CUT_TEXT-1-body": true, + "Q-Proposed-CUT_TEXT-1-div": true, + "Q-Proposed-PASTE_TEXT-1-dM": true, + "Q-Proposed-PASTE_TEXT-1-body": true, + "Q-Proposed-PASTE_TEXT-1-div": true, + "Q-Proposed-garbage-1_TEXT-1-dM": true, + "Q-Proposed-garbage-1_TEXT-1-body": true, + "Q-Proposed-garbage-1_TEXT-1-div": true, + "QE-Proposed-SELECTALL_TEXT-1-dM": true, + "QE-Proposed-SELECTALL_TEXT-1-body": true, + "QE-Proposed-SELECTALL_TEXT-1-div": true, + "QE-Proposed-UNSELECT_TEXT-1-dM": true, + "QE-Proposed-UNSELECT_TEXT-1-body": true, + "QE-Proposed-UNSELECT_TEXT-1-div": true, + "QE-Proposed-UNDO_TEXT-1-dM": true, + "QE-Proposed-UNDO_TEXT-1-body": true, + "QE-Proposed-UNDO_TEXT-1-div": true, + "QE-Proposed-REDO_TEXT-1-dM": true, + "QE-Proposed-REDO_TEXT-1-body": true, + "QE-Proposed-REDO_TEXT-1-div": true, + "QE-Proposed-BOLD_TEXT-1-dM": true, + "QE-Proposed-BOLD_TEXT-1-body": true, + "QE-Proposed-BOLD_TEXT-1-div": true, + "QE-Proposed-ITALIC_TEXT-1-dM": true, + "QE-Proposed-ITALIC_TEXT-1-body": true, + "QE-Proposed-ITALIC_TEXT-1-div": true, + "QE-Proposed-UNDERLINE_TEXT-1-dM": true, + "QE-Proposed-UNDERLINE_TEXT-1-body": true, + "QE-Proposed-UNDERLINE_TEXT-1-div": true, + "QE-Proposed-STRIKETHROUGH_TEXT-1-dM": true, + "QE-Proposed-STRIKETHROUGH_TEXT-1-body": true, + "QE-Proposed-STRIKETHROUGH_TEXT-1-div": true, + "QE-Proposed-SUBSCRIPT_TEXT-1-dM": true, + "QE-Proposed-SUBSCRIPT_TEXT-1-body": true, + "QE-Proposed-SUBSCRIPT_TEXT-1-div": true, + "QE-Proposed-SUPERSCRIPT_TEXT-1-dM": true, + "QE-Proposed-SUPERSCRIPT_TEXT-1-body": true, + "QE-Proposed-SUPERSCRIPT_TEXT-1-div": true, + "QE-Proposed-FORMATBLOCK_TEXT-1-dM": true, + "QE-Proposed-FORMATBLOCK_TEXT-1-body": true, + "QE-Proposed-FORMATBLOCK_TEXT-1-div": true, + "QE-Proposed-CREATELINK_TEXT-1-dM": true, + "QE-Proposed-CREATELINK_TEXT-1-body": true, + "QE-Proposed-CREATELINK_TEXT-1-div": true, + "QE-Proposed-UNLINK_TEXT-1-dM": true, + "QE-Proposed-UNLINK_TEXT-1-body": true, + "QE-Proposed-UNLINK_TEXT-1-div": true, + "QE-Proposed-INSERTHTML_TEXT-1-dM": true, + "QE-Proposed-INSERTHTML_TEXT-1-body": true, + "QE-Proposed-INSERTHTML_TEXT-1-div": true, + "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true, + "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true, + "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true, + "QE-Proposed-INSERTIMAGE_TEXT-1-dM": true, + "QE-Proposed-INSERTIMAGE_TEXT-1-body": true, + "QE-Proposed-INSERTIMAGE_TEXT-1-div": true, + "QE-Proposed-INSERTLINEBREAK_TEXT-1-dM": true, + "QE-Proposed-INSERTLINEBREAK_TEXT-1-body": true, + "QE-Proposed-INSERTLINEBREAK_TEXT-1-div": true, + "QE-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true, + "QE-Proposed-INSERTPARAGRAPH_TEXT-1-body": true, + "QE-Proposed-INSERTPARAGRAPH_TEXT-1-div": true, + "QE-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true, + "QE-Proposed-INSERTORDEREDLIST_TEXT-1-body": true, + "QE-Proposed-INSERTORDEREDLIST_TEXT-1-div": true, + "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true, + "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true, + "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true, + "QE-Proposed-INSERTTEXT_TEXT-1-dM": true, + "QE-Proposed-INSERTTEXT_TEXT-1-body": true, + "QE-Proposed-INSERTTEXT_TEXT-1-div": true, + "QE-Proposed-DELETE_TEXT-1-dM": true, + "QE-Proposed-DELETE_TEXT-1-body": true, + "QE-Proposed-DELETE_TEXT-1-div": true, + "QE-Proposed-FORWARDDELETE_TEXT-1-dM": true, + "QE-Proposed-FORWARDDELETE_TEXT-1-body": true, + "QE-Proposed-FORWARDDELETE_TEXT-1-div": true, + "QE-Proposed-STYLEWITHCSS_TEXT-1-dM": true, + "QE-Proposed-STYLEWITHCSS_TEXT-1-body": true, + "QE-Proposed-STYLEWITHCSS_TEXT-1-div": true, + "QE-Proposed-CONTENTREADONLY_TEXT-1-dM": true, + "QE-Proposed-CONTENTREADONLY_TEXT-1-body": true, + "QE-Proposed-CONTENTREADONLY_TEXT-1-div": true, + "QE-Proposed-BACKCOLOR_TEXT-1-dM": true, + "QE-Proposed-BACKCOLOR_TEXT-1-body": true, + "QE-Proposed-BACKCOLOR_TEXT-1-div": true, + "QE-Proposed-FORECOLOR_TEXT-1-dM": true, + "QE-Proposed-FORECOLOR_TEXT-1-body": true, + "QE-Proposed-FORECOLOR_TEXT-1-div": true, + "QE-Proposed-HILITECOLOR_TEXT-1-dM": true, + "QE-Proposed-HILITECOLOR_TEXT-1-body": true, + "QE-Proposed-HILITECOLOR_TEXT-1-div": true, + "QE-Proposed-FONTNAME_TEXT-1-dM": true, + "QE-Proposed-FONTNAME_TEXT-1-body": true, + "QE-Proposed-FONTNAME_TEXT-1-div": true, + "QE-Proposed-FONTSIZE_TEXT-1-dM": true, + "QE-Proposed-FONTSIZE_TEXT-1-body": true, + "QE-Proposed-FONTSIZE_TEXT-1-div": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-body": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-div": true, + "QE-Proposed-HEADING_TEXT-1-dM": true, + "QE-Proposed-HEADING_TEXT-1-body": true, + "QE-Proposed-HEADING_TEXT-1-div": true, + "QE-Proposed-INDENT_TEXT-1-dM": true, + "QE-Proposed-INDENT_TEXT-1-body": true, + "QE-Proposed-INDENT_TEXT-1-div": true, + "QE-Proposed-OUTDENT_TEXT-1-dM": true, + "QE-Proposed-OUTDENT_TEXT-1-body": true, + "QE-Proposed-OUTDENT_TEXT-1-div": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-body": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-div": true, + "QE-Proposed-JUSTIFYCENTER_TEXT-1-dM": true, + "QE-Proposed-JUSTIFYCENTER_TEXT-1-body": true, + "QE-Proposed-JUSTIFYCENTER_TEXT-1-div": true, + "QE-Proposed-JUSTIFYFULL_TEXT-1-dM": true, + "QE-Proposed-JUSTIFYFULL_TEXT-1-body": true, + "QE-Proposed-JUSTIFYFULL_TEXT-1-div": true, + "QE-Proposed-JUSTIFYLEFT_TEXT-1-dM": true, + "QE-Proposed-JUSTIFYLEFT_TEXT-1-body": true, + "QE-Proposed-JUSTIFYLEFT_TEXT-1-div": true, + "QE-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true, + "QE-Proposed-JUSTIFYRIGHT_TEXT-1-body": true, + "QE-Proposed-JUSTIFYRIGHT_TEXT-1-div": true, + "QE-Proposed-REMOVEFORMAT_TEXT-1-dM": true, + "QE-Proposed-REMOVEFORMAT_TEXT-1-body": true, + "QE-Proposed-REMOVEFORMAT_TEXT-1-div": true, + "QE-Proposed-COPY_TEXT-1-dM": true, + "QE-Proposed-COPY_TEXT-1-body": true, + "QE-Proposed-COPY_TEXT-1-div": true, + "QE-Proposed-CUT_TEXT-1-dM": true, + "QE-Proposed-CUT_TEXT-1-body": true, + "QE-Proposed-CUT_TEXT-1-div": true, + "QE-Proposed-PASTE_TEXT-1-dM": true, + "QE-Proposed-PASTE_TEXT-1-body": true, + "QE-Proposed-PASTE_TEXT-1-div": true, + "QE-Proposed-garbage-1_TEXT-1-dM": true, + "QE-Proposed-garbage-1_TEXT-1-body": true, + "QE-Proposed-garbage-1_TEXT-1-div": true, + "QI-Proposed-SELECTALL_TEXT-1-dM": true, + "QI-Proposed-SELECTALL_TEXT-1-body": true, + "QI-Proposed-SELECTALL_TEXT-1-div": true, + "QI-Proposed-UNSELECT_TEXT-1-dM": true, + "QI-Proposed-UNSELECT_TEXT-1-body": true, + "QI-Proposed-UNSELECT_TEXT-1-div": true, + "QI-Proposed-UNDO_TEXT-1-dM": true, + "QI-Proposed-UNDO_TEXT-1-body": true, + "QI-Proposed-UNDO_TEXT-1-div": true, + "QI-Proposed-REDO_TEXT-1-dM": true, + "QI-Proposed-REDO_TEXT-1-body": true, + "QI-Proposed-REDO_TEXT-1-div": true, + "QI-Proposed-BOLD_TEXT-1-dM": true, + "QI-Proposed-BOLD_TEXT-1-body": true, + "QI-Proposed-BOLD_TEXT-1-div": true, + "QI-Proposed-ITALIC_TEXT-1-dM": true, + "QI-Proposed-ITALIC_TEXT-1-body": true, + "QI-Proposed-ITALIC_TEXT-1-div": true, + "QI-Proposed-UNDERLINE_TEXT-1-dM": true, + "QI-Proposed-UNDERLINE_TEXT-1-body": true, + "QI-Proposed-UNDERLINE_TEXT-1-div": true, + "QI-Proposed-STRIKETHROUGH_TEXT-1-dM": true, + "QI-Proposed-STRIKETHROUGH_TEXT-1-body": true, + "QI-Proposed-STRIKETHROUGH_TEXT-1-div": true, + "QI-Proposed-SUBSCRIPT_TEXT-1-dM": true, + "QI-Proposed-SUBSCRIPT_TEXT-1-body": true, + "QI-Proposed-SUBSCRIPT_TEXT-1-div": true, + "QI-Proposed-SUPERSCRIPT_TEXT-1-dM": true, + "QI-Proposed-SUPERSCRIPT_TEXT-1-body": true, + "QI-Proposed-SUPERSCRIPT_TEXT-1-div": true, + "QI-Proposed-FORMATBLOCK_TEXT-1-dM": true, + "QI-Proposed-FORMATBLOCK_TEXT-1-body": true, + "QI-Proposed-FORMATBLOCK_TEXT-1-div": true, + "QI-Proposed-CREATELINK_TEXT-1-dM": true, + "QI-Proposed-CREATELINK_TEXT-1-body": true, + "QI-Proposed-CREATELINK_TEXT-1-div": true, + "QI-Proposed-UNLINK_TEXT-1-dM": true, + "QI-Proposed-UNLINK_TEXT-1-body": true, + "QI-Proposed-UNLINK_TEXT-1-div": true, + "QI-Proposed-INSERTHTML_TEXT-1-dM": true, + "QI-Proposed-INSERTHTML_TEXT-1-body": true, + "QI-Proposed-INSERTHTML_TEXT-1-div": true, + "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true, + "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true, + "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true, + "QI-Proposed-INSERTIMAGE_TEXT-1-dM": true, + "QI-Proposed-INSERTIMAGE_TEXT-1-body": true, + "QI-Proposed-INSERTIMAGE_TEXT-1-div": true, + "QI-Proposed-INSERTLINEBREAK_TEXT-1-dM": true, + "QI-Proposed-INSERTLINEBREAK_TEXT-1-body": true, + "QI-Proposed-INSERTLINEBREAK_TEXT-1-div": true, + "QI-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true, + "QI-Proposed-INSERTPARAGRAPH_TEXT-1-body": true, + "QI-Proposed-INSERTPARAGRAPH_TEXT-1-div": true, + "QI-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true, + "QI-Proposed-INSERTORDEREDLIST_TEXT-1-body": true, + "QI-Proposed-INSERTORDEREDLIST_TEXT-1-div": true, + "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true, + "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true, + "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true, + "QI-Proposed-INSERTTEXT_TEXT-1-dM": true, + "QI-Proposed-INSERTTEXT_TEXT-1-body": true, + "QI-Proposed-INSERTTEXT_TEXT-1-div": true, + "QI-Proposed-DELETE_TEXT-1-dM": true, + "QI-Proposed-DELETE_TEXT-1-body": true, + "QI-Proposed-DELETE_TEXT-1-div": true, + "QI-Proposed-FORWARDDELETE_TEXT-1-dM": true, + "QI-Proposed-FORWARDDELETE_TEXT-1-body": true, + "QI-Proposed-FORWARDDELETE_TEXT-1-div": true, + "QI-Proposed-STYLEWITHCSS_TEXT-1-dM": true, + "QI-Proposed-STYLEWITHCSS_TEXT-1-body": true, + "QI-Proposed-STYLEWITHCSS_TEXT-1-div": true, + "QI-Proposed-CONTENTREADONLY_TEXT-1-dM": true, + "QI-Proposed-CONTENTREADONLY_TEXT-1-body": true, + "QI-Proposed-CONTENTREADONLY_TEXT-1-div": true, + "QI-Proposed-BACKCOLOR_TEXT-1-dM": true, + "QI-Proposed-BACKCOLOR_TEXT-1-body": true, + "QI-Proposed-BACKCOLOR_TEXT-1-div": true, + "QI-Proposed-FORECOLOR_TEXT-1-dM": true, + "QI-Proposed-FORECOLOR_TEXT-1-body": true, + "QI-Proposed-FORECOLOR_TEXT-1-div": true, + "QI-Proposed-HILITECOLOR_TEXT-1-dM": true, + "QI-Proposed-HILITECOLOR_TEXT-1-body": true, + "QI-Proposed-HILITECOLOR_TEXT-1-div": true, + "QI-Proposed-FONTNAME_TEXT-1-dM": true, + "QI-Proposed-FONTNAME_TEXT-1-body": true, + "QI-Proposed-FONTNAME_TEXT-1-div": true, + "QI-Proposed-FONTSIZE_TEXT-1-dM": true, + "QI-Proposed-FONTSIZE_TEXT-1-body": true, + "QI-Proposed-FONTSIZE_TEXT-1-div": true, + "QI-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, + "QI-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, + "QI-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, + "QI-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true, + "QI-Proposed-DECREASEFONTSIZE_TEXT-1-body": true, + "QI-Proposed-DECREASEFONTSIZE_TEXT-1-div": true, + "QI-Proposed-HEADING_TEXT-1-dM": true, + "QI-Proposed-HEADING_TEXT-1-body": true, + "QI-Proposed-HEADING_TEXT-1-div": true, + "QI-Proposed-INDENT_TEXT-1-dM": true, + "QI-Proposed-INDENT_TEXT-1-body": true, + "QI-Proposed-INDENT_TEXT-1-div": true, + "QI-Proposed-OUTDENT_TEXT-1-dM": true, + "QI-Proposed-OUTDENT_TEXT-1-body": true, + "QI-Proposed-OUTDENT_TEXT-1-div": true, + "QI-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "QI-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "QI-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "QI-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "QI-Proposed-UNBOOKMARK_TEXT-1-body": true, + "QI-Proposed-UNBOOKMARK_TEXT-1-div": true, + "QI-Proposed-JUSTIFYCENTER_TEXT-1-dM": true, + "QI-Proposed-JUSTIFYCENTER_TEXT-1-body": true, + "QI-Proposed-JUSTIFYCENTER_TEXT-1-div": true, + "QI-Proposed-JUSTIFYFULL_TEXT-1-dM": true, + "QI-Proposed-JUSTIFYFULL_TEXT-1-body": true, + "QI-Proposed-JUSTIFYFULL_TEXT-1-div": true, + "QI-Proposed-JUSTIFYLEFT_TEXT-1-dM": true, + "QI-Proposed-JUSTIFYLEFT_TEXT-1-body": true, + "QI-Proposed-JUSTIFYLEFT_TEXT-1-div": true, + "QI-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true, + "QI-Proposed-JUSTIFYRIGHT_TEXT-1-body": true, + "QI-Proposed-JUSTIFYRIGHT_TEXT-1-div": true, + "QI-Proposed-REMOVEFORMAT_TEXT-1-dM": true, + "QI-Proposed-REMOVEFORMAT_TEXT-1-body": true, + "QI-Proposed-REMOVEFORMAT_TEXT-1-div": true, + "QI-Proposed-COPY_TEXT-1-dM": true, + "QI-Proposed-COPY_TEXT-1-body": true, + "QI-Proposed-COPY_TEXT-1-div": true, + "QI-Proposed-CUT_TEXT-1-dM": true, + "QI-Proposed-CUT_TEXT-1-body": true, + "QI-Proposed-CUT_TEXT-1-div": true, + "QI-Proposed-PASTE_TEXT-1-dM": true, + "QI-Proposed-PASTE_TEXT-1-body": true, + "QI-Proposed-PASTE_TEXT-1-div": true, + "QI-Proposed-garbage-1_TEXT-1-dM": true, + "QI-Proposed-garbage-1_TEXT-1-body": true, + "QI-Proposed-garbage-1_TEXT-1-div": true, + "QS-Proposed-B_TEXT_SI-dM": true, + "QS-Proposed-B_TEXT_SI-body": true, + "QS-Proposed-B_TEXT_SI-div": true, + "QS-Proposed-B_B-1_SI-dM": true, + "QS-Proposed-B_B-1_SI-body": true, + "QS-Proposed-B_B-1_SI-div": true, + "QS-Proposed-B_STRONG-1_SI-dM": true, + "QS-Proposed-B_STRONG-1_SI-body": true, + "QS-Proposed-B_STRONG-1_SI-div": true, + "QS-Proposed-B_SPANs:fw:b-1_SI-dM": true, + "QS-Proposed-B_SPANs:fw:b-1_SI-body": true, + "QS-Proposed-B_SPANs:fw:b-1_SI-div": true, + "QS-Proposed-B_SPANs:fw:n-1_SI-dM": true, + "QS-Proposed-B_SPANs:fw:n-1_SI-body": true, + "QS-Proposed-B_SPANs:fw:n-1_SI-div": true, + "QS-Proposed-B_Bs:fw:n-1_SI-dM": true, + "QS-Proposed-B_Bs:fw:n-1_SI-body": true, + "QS-Proposed-B_Bs:fw:n-1_SI-div": true, + "QS-Proposed-B_B-SPANs:fw:n-1_SI-dM": true, + "QS-Proposed-B_B-SPANs:fw:n-1_SI-body": true, + "QS-Proposed-B_B-SPANs:fw:n-1_SI-div": true, + "QS-Proposed-B_SPAN.b-1-SI-dM": true, + "QS-Proposed-B_SPAN.b-1-SI-body": true, + "QS-Proposed-B_SPAN.b-1-SI-div": true, + "QS-Proposed-B_MYB-1-SI-dM": true, + "QS-Proposed-B_MYB-1-SI-body": true, + "QS-Proposed-B_MYB-1-SI-div": true, + "QS-Proposed-B_B-I-1_SC-dM": true, + "QS-Proposed-B_B-I-1_SC-body": true, + "QS-Proposed-B_B-I-1_SC-div": true, + "QS-Proposed-B_B-I-1_SL-dM": true, + "QS-Proposed-B_B-I-1_SL-body": true, + "QS-Proposed-B_B-I-1_SL-div": true, + "QS-Proposed-B_B-I-1_SR-dM": true, + "QS-Proposed-B_B-I-1_SR-body": true, + "QS-Proposed-B_B-I-1_SR-div": true, + "QS-Proposed-B_STRONG-I-1_SC-dM": true, + "QS-Proposed-B_STRONG-I-1_SC-body": true, + "QS-Proposed-B_STRONG-I-1_SC-div": true, + "QS-Proposed-B_B-I-U-1_SC-dM": true, + "QS-Proposed-B_B-I-U-1_SC-body": true, + "QS-Proposed-B_B-I-U-1_SC-div": true, + "QS-Proposed-B_B-I-U-1_SM-dM": true, + "QS-Proposed-B_B-I-U-1_SM-body": true, + "QS-Proposed-B_B-I-U-1_SM-div": true, + "QS-Proposed-B_TEXT-B-1_SO-1-dM": true, + "QS-Proposed-B_TEXT-B-1_SO-1-body": true, + "QS-Proposed-B_TEXT-B-1_SO-1-div": true, + "QS-Proposed-B_TEXT-B-1_SO-2-dM": true, + "QS-Proposed-B_TEXT-B-1_SO-2-body": true, + "QS-Proposed-B_TEXT-B-1_SO-2-div": true, + "QS-Proposed-B_TEXT-B-1_SL-dM": true, + "QS-Proposed-B_TEXT-B-1_SL-body": true, + "QS-Proposed-B_TEXT-B-1_SL-div": true, + "QS-Proposed-B_TEXT-B-1_SR-dM": true, + "QS-Proposed-B_TEXT-B-1_SR-body": true, + "QS-Proposed-B_TEXT-B-1_SR-div": true, + "QS-Proposed-B_TEXT-B-1_SO-3-dM": true, + "QS-Proposed-B_TEXT-B-1_SO-3-body": true, + "QS-Proposed-B_TEXT-B-1_SO-3-div": true, + "QS-Proposed-B_B.TEXT.B-1_SM-dM": true, + "QS-Proposed-B_B.TEXT.B-1_SM-body": true, + "QS-Proposed-B_B.TEXT.B-1_SM-div": true, + "QS-Proposed-B_B.B.B-1_SM-dM": true, + "QS-Proposed-B_B.B.B-1_SM-body": true, + "QS-Proposed-B_B.B.B-1_SM-div": true, + "QS-Proposed-B_B.STRONG.B-1_SM-dM": true, + "QS-Proposed-B_B.STRONG.B-1_SM-body": true, + "QS-Proposed-B_B.STRONG.B-1_SM-div": true, + "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-dM": true, + "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-body": true, + "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-div": true, + "QS-Proposed-I_TEXT_SI-dM": true, + "QS-Proposed-I_TEXT_SI-body": true, + "QS-Proposed-I_TEXT_SI-div": true, + "QS-Proposed-I_I-1_SI-dM": true, + "QS-Proposed-I_I-1_SI-body": true, + "QS-Proposed-I_I-1_SI-div": true, + "QS-Proposed-I_EM-1_SI-dM": true, + "QS-Proposed-I_EM-1_SI-body": true, + "QS-Proposed-I_EM-1_SI-div": true, + "QS-Proposed-I_SPANs:fs:i-1_SI-dM": true, + "QS-Proposed-I_SPANs:fs:i-1_SI-body": true, + "QS-Proposed-I_SPANs:fs:i-1_SI-div": true, + "QS-Proposed-I_SPANs:fs:n-1_SI-dM": true, + "QS-Proposed-I_SPANs:fs:n-1_SI-body": true, + "QS-Proposed-I_SPANs:fs:n-1_SI-div": true, + "QS-Proposed-I_I-SPANs:fs:n-1_SI-dM": true, + "QS-Proposed-I_I-SPANs:fs:n-1_SI-body": true, + "QS-Proposed-I_I-SPANs:fs:n-1_SI-div": true, + "QS-Proposed-I_SPAN.i-1-SI-dM": true, + "QS-Proposed-I_SPAN.i-1-SI-body": true, + "QS-Proposed-I_SPAN.i-1-SI-div": true, + "QS-Proposed-I_MYI-1-SI-dM": true, + "QS-Proposed-I_MYI-1-SI-body": true, + "QS-Proposed-I_MYI-1-SI-div": true, + "QS-Proposed-U_TEXT_SI-dM": true, + "QS-Proposed-U_TEXT_SI-body": true, + "QS-Proposed-U_TEXT_SI-div": true, + "QS-Proposed-U_U-1_SI-dM": true, + "QS-Proposed-U_U-1_SI-body": true, + "QS-Proposed-U_U-1_SI-div": true, + "QS-Proposed-U_Us:td:n-1_SI-dM": true, + "QS-Proposed-U_Us:td:n-1_SI-body": true, + "QS-Proposed-U_Us:td:n-1_SI-div": true, + "QS-Proposed-U_Ah:url-1_SI-dM": true, + "QS-Proposed-U_Ah:url-1_SI-body": true, + "QS-Proposed-U_Ah:url-1_SI-div": true, + "QS-Proposed-U_Ah:url.s:td:n-1_SI-dM": true, + "QS-Proposed-U_Ah:url.s:td:n-1_SI-body": true, + "QS-Proposed-U_Ah:url.s:td:n-1_SI-div": true, + "QS-Proposed-U_SPANs:td:u-1_SI-dM": true, + "QS-Proposed-U_SPANs:td:u-1_SI-body": true, + "QS-Proposed-U_SPANs:td:u-1_SI-div": true, + "QS-Proposed-U_SPAN.u-1-SI-dM": true, + "QS-Proposed-U_SPAN.u-1-SI-body": true, + "QS-Proposed-U_SPAN.u-1-SI-div": true, + "QS-Proposed-U_MYU-1-SI-dM": true, + "QS-Proposed-U_MYU-1-SI-body": true, + "QS-Proposed-U_MYU-1-SI-div": true, + "QS-Proposed-S_TEXT_SI-dM": true, + "QS-Proposed-S_TEXT_SI-body": true, + "QS-Proposed-S_TEXT_SI-div": true, + "QS-Proposed-S_S-1_SI-dM": true, + "QS-Proposed-S_S-1_SI-body": true, + "QS-Proposed-S_S-1_SI-div": true, + "QS-Proposed-S_STRIKE-1_SI-dM": true, + "QS-Proposed-S_STRIKE-1_SI-body": true, + "QS-Proposed-S_STRIKE-1_SI-div": true, + "QS-Proposed-S_STRIKEs:td:n-1_SI-dM": true, + "QS-Proposed-S_STRIKEs:td:n-1_SI-body": true, + "QS-Proposed-S_STRIKEs:td:n-1_SI-div": true, + "QS-Proposed-S_DEL-1_SI-dM": true, + "QS-Proposed-S_DEL-1_SI-body": true, + "QS-Proposed-S_DEL-1_SI-div": true, + "QS-Proposed-S_SPANs:td:lt-1_SI-dM": true, + "QS-Proposed-S_SPANs:td:lt-1_SI-body": true, + "QS-Proposed-S_SPANs:td:lt-1_SI-div": true, + "QS-Proposed-S_SPAN.s-1-SI-dM": true, + "QS-Proposed-S_SPAN.s-1-SI-body": true, + "QS-Proposed-S_SPAN.s-1-SI-div": true, + "QS-Proposed-S_MYS-1-SI-dM": true, + "QS-Proposed-S_MYS-1-SI-body": true, + "QS-Proposed-S_MYS-1-SI-div": true, + "QS-Proposed-S_S.STRIKE.DEL-1_SM-dM": true, + "QS-Proposed-S_S.STRIKE.DEL-1_SM-body": true, + "QS-Proposed-S_S.STRIKE.DEL-1_SM-div": true, + "QS-Proposed-SUB_TEXT_SI-dM": true, + "QS-Proposed-SUB_TEXT_SI-body": true, + "QS-Proposed-SUB_TEXT_SI-div": true, + "QS-Proposed-SUB_SUB-1_SI-dM": true, + "QS-Proposed-SUB_SUB-1_SI-body": true, + "QS-Proposed-SUB_SUB-1_SI-div": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-dM": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-body": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-div": true, + "QS-Proposed-SUB_MYSUB-1-SI-dM": true, + "QS-Proposed-SUB_MYSUB-1-SI-body": true, + "QS-Proposed-SUB_MYSUB-1-SI-div": true, + "QS-Proposed-SUP_TEXT_SI-dM": true, + "QS-Proposed-SUP_TEXT_SI-body": true, + "QS-Proposed-SUP_TEXT_SI-div": true, + "QS-Proposed-SUP_SUP-1_SI-dM": true, + "QS-Proposed-SUP_SUP-1_SI-body": true, + "QS-Proposed-SUP_SUP-1_SI-div": true, + "QS-Proposed-IOL_TEXT_SI-dM": true, + "QS-Proposed-IOL_TEXT_SI-body": true, + "QS-Proposed-IOL_TEXT_SI-div": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-dM": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-body": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-div": true, + "QS-Proposed-SUP_MYSUP-1-SI-dM": true, + "QS-Proposed-SUP_MYSUP-1-SI-body": true, + "QS-Proposed-SUP_MYSUP-1-SI-div": true, + "QS-Proposed-IOL_TEXT-1_SI-dM": true, + "QS-Proposed-IOL_TEXT-1_SI-body": true, + "QS-Proposed-IOL_TEXT-1_SI-div": true, + "QS-Proposed-IOL_OL-LI-1_SI-dM": true, + "QS-Proposed-IOL_OL-LI-1_SI-body": true, + "QS-Proposed-IOL_OL-LI-1_SI-div": true, + "QS-Proposed-IOL_UL_LI-1_SI-dM": true, + "QS-Proposed-IOL_UL_LI-1_SI-body": true, + "QS-Proposed-IOL_UL_LI-1_SI-div": true, + "QS-Proposed-IUL_TEXT_SI-dM": true, + "QS-Proposed-IUL_TEXT_SI-body": true, + "QS-Proposed-IUL_TEXT_SI-div": true, + "QS-Proposed-IUL_OL-LI-1_SI-dM": true, + "QS-Proposed-IUL_OL-LI-1_SI-body": true, + "QS-Proposed-IUL_OL-LI-1_SI-div": true, + "QS-Proposed-IUL_UL-LI-1_SI-dM": true, + "QS-Proposed-IUL_UL-LI-1_SI-body": true, + "QS-Proposed-IUL_UL-LI-1_SI-div": true, + "QS-Proposed-JC_TEXT_SI-dM": true, + "QS-Proposed-JC_TEXT_SI-body": true, + "QS-Proposed-JC_TEXT_SI-div": true, + "QS-Proposed-JC_DIVa:c-1_SI-dM": true, + "QS-Proposed-JC_DIVa:c-1_SI-body": true, + "QS-Proposed-JC_DIVa:c-1_SI-div": true, + "QS-Proposed-JC_Pa:c-1_SI-dM": true, + "QS-Proposed-JC_Pa:c-1_SI-body": true, + "QS-Proposed-JC_Pa:c-1_SI-div": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-dM": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-body": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-div": true, + "QS-Proposed-JC_SPAN.jc-1-SI-dM": true, + "QS-Proposed-JC_SPAN.jc-1-SI-body": true, + "QS-Proposed-JC_SPAN.jc-1-SI-div": true, + "QS-Proposed-JC_MYJC-1-SI-dM": true, + "QS-Proposed-JC_MYJC-1-SI-body": true, + "QS-Proposed-JC_MYJC-1-SI-div": true, + "QS-Proposed-JF_TEXT_SI-dM": true, + "QS-Proposed-JF_TEXT_SI-body": true, + "QS-Proposed-JF_TEXT_SI-div": true, + "QS-Proposed-JF_DIVa:j-1_SI-dM": true, + "QS-Proposed-JF_DIVa:j-1_SI-body": true, + "QS-Proposed-JF_DIVa:j-1_SI-div": true, + "QS-Proposed-JF_Pa:j-1_SI-dM": true, + "QS-Proposed-JF_Pa:j-1_SI-body": true, + "QS-Proposed-JF_Pa:j-1_SI-div": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-dM": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-body": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-div": true, + "QS-Proposed-JF_SPAN.jf-1-SI-dM": true, + "QS-Proposed-JF_SPAN.jf-1-SI-body": true, + "QS-Proposed-JF_SPAN.jf-1-SI-div": true, + "QS-Proposed-JF_MYJF-1-SI-dM": true, + "QS-Proposed-JF_MYJF-1-SI-body": true, + "QS-Proposed-JF_MYJF-1-SI-div": true, + "QS-Proposed-JL_TEXT_SI-dM": true, + "QS-Proposed-JL_TEXT_SI-body": true, + "QS-Proposed-JL_TEXT_SI-div": true, + "QS-Proposed-JL_DIVa:l-1_SI-dM": true, + "QS-Proposed-JL_DIVa:l-1_SI-body": true, + "QS-Proposed-JL_DIVa:l-1_SI-div": true, + "QS-Proposed-JL_Pa:l-1_SI-dM": true, + "QS-Proposed-JL_Pa:l-1_SI-body": true, + "QS-Proposed-JL_Pa:l-1_SI-div": true, + "QS-Proposed-JL_SPANs:ta:l-1_SI-dM": true, + "QS-Proposed-JL_SPANs:ta:l-1_SI-body": true, + "QS-Proposed-JL_SPANs:ta:l-1_SI-div": true, + "QS-Proposed-JL_SPAN.jl-1-SI-dM": true, + "QS-Proposed-JL_SPAN.jl-1-SI-body": true, + "QS-Proposed-JL_SPAN.jl-1-SI-div": true, + "QS-Proposed-JL_MYJL-1-SI-dM": true, + "QS-Proposed-JL_MYJL-1-SI-body": true, + "QS-Proposed-JL_MYJL-1-SI-div": true, + "QS-Proposed-JR_TEXT_SI-dM": true, + "QS-Proposed-JR_TEXT_SI-body": true, + "QS-Proposed-JR_TEXT_SI-div": true, + "QS-Proposed-JR_DIVa:r-1_SI-dM": true, + "QS-Proposed-JR_DIVa:r-1_SI-body": true, + "QS-Proposed-JR_DIVa:r-1_SI-div": true, + "QS-Proposed-JR_Pa:r-1_SI-dM": true, + "QS-Proposed-JR_Pa:r-1_SI-body": true, + "QS-Proposed-JR_Pa:r-1_SI-div": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-dM": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-body": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-div": true, + "QS-Proposed-JR_SPAN.jr-1-SI-dM": true, + "QS-Proposed-JR_SPAN.jr-1-SI-body": true, + "QS-Proposed-JR_SPAN.jr-1-SI-div": true, + "QS-Proposed-JR_MYJR-1-SI-dM": true, + "QS-Proposed-JR_MYJR-1-SI-body": true, + "QS-Proposed-JR_MYJR-1-SI-div": true, + "QV-Proposed-B_TEXT_SI-dM": true, + "QV-Proposed-B_TEXT_SI-body": true, + "QV-Proposed-B_TEXT_SI-div": true, + "QV-Proposed-B_B-1_SI-dM": true, + "QV-Proposed-B_B-1_SI-body": true, + "QV-Proposed-B_B-1_SI-div": true, + "QV-Proposed-B_STRONG-1_SI-dM": true, + "QV-Proposed-B_STRONG-1_SI-body": true, + "QV-Proposed-B_STRONG-1_SI-div": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-dM": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-body": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-div": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-dM": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-body": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-div": true, + "QV-Proposed-B_Bs:fw:n-1_SI-dM": true, + "QV-Proposed-B_Bs:fw:n-1_SI-body": true, + "QV-Proposed-B_Bs:fw:n-1_SI-div": true, + "QV-Proposed-B_SPAN.b-1_SI-dM": true, + "QV-Proposed-B_SPAN.b-1_SI-body": true, + "QV-Proposed-B_SPAN.b-1_SI-div": true, + "QV-Proposed-B_MYB-1-SI-dM": true, + "QV-Proposed-B_MYB-1-SI-body": true, + "QV-Proposed-B_MYB-1-SI-div": true, + "QV-Proposed-I_TEXT_SI-dM": true, + "QV-Proposed-I_TEXT_SI-body": true, + "QV-Proposed-I_TEXT_SI-div": true, + "QV-Proposed-I_I-1_SI-dM": true, + "QV-Proposed-I_I-1_SI-body": true, + "QV-Proposed-I_I-1_SI-div": true, + "QV-Proposed-I_EM-1_SI-dM": true, + "QV-Proposed-I_EM-1_SI-body": true, + "QV-Proposed-I_EM-1_SI-div": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-dM": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-body": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-div": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-dM": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-body": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-div": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-dM": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-body": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-div": true, + "QV-Proposed-I_SPAN.i-1_SI-dM": true, + "QV-Proposed-I_SPAN.i-1_SI-body": true, + "QV-Proposed-I_SPAN.i-1_SI-div": true, + "QV-Proposed-I_MYI-1-SI-dM": true, + "QV-Proposed-I_MYI-1-SI-body": true, + "QV-Proposed-I_MYI-1-SI-div": true, + "QV-Proposed-FB_TEXT-1_SC-dM": true, + "QV-Proposed-FB_TEXT-1_SC-body": true, + "QV-Proposed-FB_TEXT-1_SC-div": true, + "QV-Proposed-FB_H1-1_SC-dM": true, + "QV-Proposed-FB_H1-1_SC-body": true, + "QV-Proposed-FB_H1-1_SC-div": true, + "QV-Proposed-FB_PRE-1_SC-dM": true, + "QV-Proposed-FB_PRE-1_SC-body": true, + "QV-Proposed-FB_PRE-1_SC-div": true, + "QV-Proposed-FB_BQ-1_SC-dM": true, + "QV-Proposed-FB_BQ-1_SC-body": true, + "QV-Proposed-FB_BQ-1_SC-div": true, + "QV-Proposed-FB_ADDRESS-1_SC-dM": true, + "QV-Proposed-FB_ADDRESS-1_SC-body": true, + "QV-Proposed-FB_ADDRESS-1_SC-div": true, + "QV-Proposed-FB_H1-H2-1_SC-dM": true, + "QV-Proposed-FB_H1-H2-1_SC-body": true, + "QV-Proposed-FB_H1-H2-1_SC-div": true, + "QV-Proposed-FB_H1-H2-1_SL-dM": true, + "QV-Proposed-FB_H1-H2-1_SL-body": true, + "QV-Proposed-FB_H1-H2-1_SL-div": true, + "QV-Proposed-FB_H1-H2-1_SR-dM": true, + "QV-Proposed-FB_H1-H2-1_SR-body": true, + "QV-Proposed-FB_H1-H2-1_SR-div": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-dM": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-body": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-div": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SR-dM": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SR-body": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SR-div": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-dM": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-body": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-div": true, + "QV-Proposed-H_H1-1_SC-dM": true, + "QV-Proposed-H_H1-1_SC-body": true, + "QV-Proposed-H_H1-1_SC-div": true, + "QV-Proposed-H_H3-1_SC-dM": true, + "QV-Proposed-H_H3-1_SC-body": true, + "QV-Proposed-H_H3-1_SC-div": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-dM": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-body": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-div": true, + "QV-Proposed-H_P-1_SC-dM": true, + "QV-Proposed-H_P-1_SC-body": true, + "QV-Proposed-H_P-1_SC-div": true, + "QV-Proposed-FN_FONTf:a-1_SI-dM": true, + "QV-Proposed-FN_FONTf:a-1_SI-body": true, + "QV-Proposed-FN_FONTf:a-1_SI-div": true, + "QV-Proposed-FN_SPANs:ff:a-1_SI-dM": true, + "QV-Proposed-FN_SPANs:ff:a-1_SI-body": true, + "QV-Proposed-FN_SPANs:ff:a-1_SI-div": true, + "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-dM": true, + "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-body": true, + "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-div": true, + "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-dM": true, + "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-body": true, + "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-div": true, + "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-dM": true, + "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-body": true, + "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-div": true, + "QV-Proposed-FN_SPAN.fs18px-1_SI-dM": true, + "QV-Proposed-FN_SPAN.fs18px-1_SI-body": true, + "QV-Proposed-FN_SPAN.fs18px-1_SI-div": true, + "QV-Proposed-FN_MYCOURIER-1-SI-dM": true, + "QV-Proposed-FN_MYCOURIER-1-SI-body": true, + "QV-Proposed-FN_MYCOURIER-1-SI-div": true, + "QV-Proposed-FS_FONTsz:4-1_SI-dM": true, + "QV-Proposed-FS_FONTsz:4-1_SI-body": true, + "QV-Proposed-FS_FONTsz:4-1_SI-div": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-dM": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-body": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-div": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-dM": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-body": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-div": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-dM": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-body": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-div": true, + "QV-Proposed-FS_SPAN.large-1_SI-dM": true, + "QV-Proposed-FS_SPAN.large-1_SI-body": true, + "QV-Proposed-FS_SPAN.large-1_SI-div": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-dM": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-body": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-div": true, + "QV-Proposed-FA_MYLARGE-1-SI-dM": true, + "QV-Proposed-FA_MYLARGE-1-SI-body": true, + "QV-Proposed-FA_MYLARGE-1-SI-div": true, + "QV-Proposed-FA_MYFS18PX-1-SI-dM": true, + "QV-Proposed-FA_MYFS18PX-1-SI-body": true, + "QV-Proposed-FA_MYFS18PX-1-SI-div": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-dM": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-body": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-div": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-dM": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-body": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-div": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-dM": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-body": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-div": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-dM": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-body": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-div": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-dM": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-body": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-div": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-dM": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-body": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-div": true, + "QV-Proposed-BC_MYBCRED-1-SI-dM": true, + "QV-Proposed-BC_MYBCRED-1-SI-body": true, + "QV-Proposed-BC_MYBCRED-1-SI-div": true, + "QV-Proposed-FC_FONTc:f00-1_SI-dM": true, + "QV-Proposed-FC_FONTc:f00-1_SI-body": true, + "QV-Proposed-FC_FONTc:f00-1_SI-div": true, + "QV-Proposed-FC_SPANs:c:0f0-1_SI-dM": true, + "QV-Proposed-FC_SPANs:c:0f0-1_SI-body": true, + "QV-Proposed-FC_SPANs:c:0f0-1_SI-div": true, + "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-dM": true, + "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-body": true, + "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-div": true, + "QV-Proposed-FC_FONTc:641-SPAN-1_SI-dM": true, + "QV-Proposed-FC_FONTc:641-SPAN-1_SI-body": true, + "QV-Proposed-FC_FONTc:641-SPAN-1_SI-div": true, + "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-dM": true, + "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-body": true, + "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-div": true, + "QV-Proposed-FC_SPAN.red-1_SI-dM": true, + "QV-Proposed-FC_SPAN.red-1_SI-body": true, + "QV-Proposed-FC_SPAN.red-1_SI-div": true, + "QV-Proposed-FC_MYRED-1-SI-dM": true, + "QV-Proposed-FC_MYRED-1-SI-body": true, + "QV-Proposed-FC_MYRED-1-SI-div": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-dM": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-body": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-div": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-dM": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-body": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-div": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-dM": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-body": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-div": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-dM": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-body": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-div": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-dM": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-body": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-div": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-dM": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-body": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-div": true, + "QV-Proposed-HC_MYBCRED-1-SI-dM": true, + "QV-Proposed-HC_MYBCRED-1-SI-body": true, + "QV-Proposed-HC_MYBCRED-1-SI-div": true + } +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/current_revision b/editor/libeditor/tests/browserscope/lib/richtext2/current_revision new file mode 100644 index 000000000..cc34bb397 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/current_revision @@ -0,0 +1 @@ +805 diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py new file mode 100644 index 000000000..345f9bbb0 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py @@ -0,0 +1,25 @@ +#!/usr/bin/python2.5 +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License') +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common constants""" + +__author__ = 'rolandsteiner@google.com (Roland Steiner)' + +CATEGORY = 'richtext2' + +TEST_ID_PREFIX = 'RTE2' + +CLASSES = ['Finalized', 'RFC', 'Proposed']
\ No newline at end of file diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py new file mode 100644 index 000000000..2ee1e79ad --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py @@ -0,0 +1,107 @@ +#!/usr/bin/python2.5 +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License') +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Handlers for New Rich Text Tests""" + +__author__ = 'rolandsteiner@google.com (Roland Steiner)' + +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.api import memcache +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template + +import django +from django import http +from django import shortcuts + +from django.template import add_to_builtins +add_to_builtins('base.custom_filters') + +# Shared stuff +from categories import all_test_sets +from base import decorators +from base import util + +# common to the RichText2 suite +from categories.richtext2 import common + +# tests +from categories.richtext2.tests.apply import APPLY_TESTS +from categories.richtext2.tests.applyCSS import APPLY_TESTS_CSS +from categories.richtext2.tests.change import CHANGE_TESTS +from categories.richtext2.tests.changeCSS import CHANGE_TESTS_CSS +from categories.richtext2.tests.delete import DELETE_TESTS +from categories.richtext2.tests.forwarddelete import FORWARDDELETE_TESTS +from categories.richtext2.tests.insert import INSERT_TESTS +from categories.richtext2.tests.selection import SELECTION_TESTS +from categories.richtext2.tests.unapply import UNAPPLY_TESTS +from categories.richtext2.tests.unapplyCSS import UNAPPLY_TESTS_CSS + +from categories.richtext2.tests.querySupported import QUERYSUPPORTED_TESTS +from categories.richtext2.tests.queryEnabled import QUERYENABLED_TESTS +from categories.richtext2.tests.queryIndeterm import QUERYINDETERM_TESTS +from categories.richtext2.tests.queryState import QUERYSTATE_TESTS, QUERYSTATE_TESTS_CSS +from categories.richtext2.tests.queryValue import QUERYVALUE_TESTS, QUERYVALUE_TESTS_CSS + + +def About(request): + """About page.""" + overview = """These tests cover browers' implementations of + <a href="http://blog.whatwg.org/the-road-to-html-5-contenteditable">contenteditable</a> + for basic rich text formatting commands. Most browser implementations do very + well at editing the HTML which is generated by their own execCommands. But a + big problem happens when developers try to make cross-browser web + applications using contenteditable - most browsers are not able to correctly + change formatting generated by other browsers. On top of that, most browsers + allow users to to paste arbitrary HTML from other webpages into a + contenteditable region, which is even harder for browsers to properly + format. These tests check how well the execCommand, queryCommandState, + and queryCommandValue functions work with different types of HTML.""" + return util.About(request, common.CATEGORY, category_title='Rich Text', + overview=overview, show_hidden=False) + + +def RunRichText2Tests(request): + params = { + 'classes': common.CLASSES, + 'commonIDPrefix': common.TEST_ID_PREFIX, + 'strict': False, + 'suites': [ + SELECTION_TESTS, + APPLY_TESTS, + APPLY_TESTS_CSS, + CHANGE_TESTS, + CHANGE_TESTS_CSS, + UNAPPLY_TESTS, + UNAPPLY_TESTS_CSS, + DELETE_TESTS, + FORWARDDELETE_TESTS, + INSERT_TESTS, + + QUERYSUPPORTED_TESTS, + QUERYENABLED_TESTS, + QUERYINDETERM_TESTS, + QUERYSTATE_TESTS, + QUERYSTATE_TESTS_CSS, + QUERYVALUE_TESTS, + QUERYVALUE_TESTS_CSS + ] + } + return shortcuts.render_to_response('%s/templates/richtext2.html' % common.CATEGORY, params) + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css new file mode 100644 index 000000000..77c6bb872 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css @@ -0,0 +1,116 @@ +.framed { + vertical-align: top; + margin: 8px; + border: 1px solid black; +} + +.legend { + padding: 12px; + background-color: #f8f8ff; +} + +.legendHdr { + font-size: large; + text-decoration: underline; +} + +table.legend { + display: inline-table; +} + +.suite-thead { + text-align: left; +} + +.lo { + background-color: #dddddd; +} +.hi { + background-color: #eeeeee; +} + +.lo .grey { + background-color: #dddddd; +} +.lo .na { + background-color: #dddddd; +} +.lo .pass { + background-color: #d4ffc0; +} +.lo .canary { + background-color: #ffcccc; +} +.lo .fail { + background-color: #ffcccc; +} +.lo .accept { + background-color: #ffffc0; +} +.lo .exception { + background-color: #f0d0f4; +} +.lo .unsupported { + background-color: #f0d0f4; +} + +.hi .grey { + background-color: #eeeeee; +} +.hi .na { + background-color: #eeeeee; +} +.hi .pass { + background-color: #e0ffdc; +} +.hi .canary { + background-color: #ffd8d8; +} +.hi .fail { + background-color: #ffd8d8; +} +.hi .accept { + background-color: #ffffd8; +} +.hi .exception { + background-color: #f4dcf8; +} +.hi .unsupported { + background-color: #f4dcf8; +} + + +.sel { + color: blue; +} + +.txt { + padding: 1px; + margin: 1px; + border: 1px solid #b0b0b0; +} + +.idLabel { + font-size: small; +} + +.fade { + color: grey; +} +.accexp { + color: #606070; +} +.comment { + color: grey; +} + +.score { + color: #666666; +} + +.fatalerror { + color: red; + font-size: large; + font-weight: bold; +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html new file mode 100644 index 000000000..a254adc03 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <link rel="stylesheet" href="editable.css" type="text/css"> +</head> +<body contentEditable="true"> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html new file mode 100644 index 000000000..e16de3ab9 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <link rel="stylesheet" href="editable.css" type="text/css"> + + <script> + function setDesignMode() { + window.document.designMode = "On"; + } + </script> +</head> +<body onload="setDesignMode()"> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html new file mode 100644 index 000000000..7dd600dbd --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <link rel="stylesheet" href="editable.css" type="text/css"> +</head> +<body> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css new file mode 100644 index 000000000..99fec4950 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css @@ -0,0 +1,66 @@ +.b, myb { + font-weight: bold; +} + +.i, myi { + font-style: italic; +} + +.s, mys { + text-decoration: line-through; +} + +.u, myu { + text-decoration: underline; +} + +.sub, mysub { + vertical-align: sub; +} + +.sup, mysup { + vertical-align: super; +} + +.jc, myjc { + text-align: center; +} + +.jf, myjf { + text-align: justify; +} + +.jl, myjl { + text-align: left; +} + +.jr, myjr { + text-align: right; +} + +.red, myred { + color: red; +} + +.bcred, mybcred { + background-color: red; +} + +.large, mylarge { + font-size: large; +} + +.fs18px, myfs18px { + font-size: 18px; +} + +.courier, mycourier { + font-family: courier; +} + +gen::before { + content: "[BEFORE]"; +} +gen::after { + content: "[AFTER]"; +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js new file mode 100644 index 000000000..2236d9dfc --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js @@ -0,0 +1,436 @@ +/** + * @fileoverview + * Canonicalization functions used in the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * Canonicalize HTML entities to their actual character + * + * @param str {String} the HTML string to be canonicalized + * @return {String} the canonicalized string + */ + +function canonicalizeEntities(str) { + // TODO(rolandsteiner): this function is very much not optimized, but that shouldn't + // theoretically matter too much - look into it at some point. + var match; + while (match = str.match(/&#x([0-9A-F]+);/i)) { + str = str.replace('&#x' + match[1] + ';', String.fromCharCode(parseInt(match[1], 16))); + } + while (match = str.match(/&#([0-9]+);/)) { + str = str.replace('&#' + match[1] + ';', String.fromCharCode(Number(match[1]))); + } + return str; +} + +/** + * Canonicalize the contents of the HTML 'style' attribute. + * I.e. sorts the CSS attributes alphabetically and canonicalizes the values + * CSS attributes where necessary. + * + * If this would return an empty string, return null instead to suppress the + * whole 'style' attribute. + * + * Avoid tests that contain {, } or : within CSS values! + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * FIXME: does not canonicalize the contents of compound attributes + * (e.g., 'border'). + * + * @param str {String} contents of the 'style' attribute + * @param emitFlags {Object} flags used for this output + * @return {String/null} canonicalized string, null instead of the empty string + */ +function canonicalizeStyle(str, emitFlags) { + // Remove any enclosing curly brackets + str = str.replace(/ ?[\{\}] ?/g, ''); + + var attributes = str.split(';'); + var count = attributes.length; + var resultArr = []; + + for (var a = 0; a < count; ++a) { + // Retrieve "name: value" pair + // Note: may expectedly fail if the last pair was terminated with ';' + var avPair = attributes[a].match(/ ?([^ :]+) ?: ?(.+)/); + if (!avPair) + continue; + + var name = avPair[1]; + var value = avPair[2].replace(/ $/, ''); // Remove any trailing space. + + switch (name) { + case 'color': + case 'background-color': + case 'border-color': + if (emitFlags.canonicalizeUnits) { + resultArr.push(name + ': #' + new Color(value).toHexString()); + } else { + resultArr.push(name + ': ' + value); + } + break; + + case 'font-family': + if (emitFlags.canonicalizeUnits) { + resultArr.push(name + ': ' + new FontName(value).toString()); + } else { + resultArr.push(name + ': ' + value); + } + break; + + case 'font-size': + if (emitFlags.canonicalizeUnits) { + resultArr.push(name + ': ' + new FontSize(value).toString()); + } else { + resultArr.push(name + ': ' + value); + } + break; + + default: + resultArr.push(name + ': ' + value); + } + } + + // Sort by name, assuming no duplicate CSS attribute names. + resultArr.sort(); + + return resultArr.join('; ') || null; +} + +/** + * Canonicalize a single attribute value. + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * @param elemName {String} the name of the element + * @param attrName {String} the name of the attribute + * @param attrValue {String} the value of the attribute + * @param emitFlags {Object} flags used for this output + * @return {String/null} the canonicalized value, or null if the attribute should be skipped. + */ +function canonicalizeSingleAttribute(elemName, attrName, attrValue, emitFlags) { + // We emit attributes as name="value", so change any contained apostrophes + // to quote marks. + attrValue = attrValue.replace(/\x22/, '\x27'); + + switch (attrName) { + case 'class': + return emitFlags.emitClass ? attrValue : null; + + case 'id': + if (!emitFlags.emitID) { + return null; + } + if (attrValue && attrValue.substr(0, 7) == 'editor-') { + return null; + } + return attrValue; + + // Remove empty style attributes, canonicalize the contents otherwise, + // provided the test cares for styles. + case 'style': + return (emitFlags.emitStyle && attrValue) + ? canonicalizeStyle(attrValue, emitFlags) + : null; + + // Never output onload handlers as they are set by the test environment. + case 'onload': + return null; + + // Canonicalize colors. + case 'bgcolor': + case 'color': + if (!attrValue) { + return null; + } + return emitFlags.canonicalizeUnits ? new Color(attrValue).toString() : attrValue; + + // Canonicalize font names. + case 'face': + return emitFlags.canonicalizeUnits ? new FontName(attrValue).toString() : attrValue; + + // Canonicalize font sizes (leave other 'size' attributes as-is). + case 'size': + if (!attrValue) { + return null; + } + switch (elemName) { + case 'basefont': + case 'font': + return emitFlags.canonicalizeUnits ? new FontSize(attrValue).toString() : attrValue; + } + return attrValue; + + // Remove spans with value 1. Retain spans with other values, even if + // empty or with a value 0, since those indicate a flawed implementation. + case 'colspan': + case 'rowspan': + case 'span': + return (attrValue == '1' || attrValue === '') ? null : attrValue; + + // Boolean attributes: presence equals true. If present, the value must be + // the empty string or the attribute's canonical name. + // (http://www.whatwg.org/specs/web-apps/current-work/#boolean-attributes) + // Below we only normalize empty string to the canonical name for + // comparison purposes. All other values are not touched and will therefore + // in all likelihood result in a failed test (even if they may be accepted + // by the UA). + case 'async': + case 'autofocus': + case 'checked': + case 'compact': + case 'declare': + case 'defer': + case 'disabled': + case 'formnovalidate': + case 'frameborder': + case 'ismap': + case 'loop': + case 'multiple': + case 'nohref': + case 'nosize': + case 'noshade': + case 'novalidate': + case 'nowrap': + case 'open': + case 'readonly': + case 'required': + case 'reversed': + case 'seamless': + case 'selected': + return attrValue ? attrValue : attrName; + + default: + return attrValue; + } +} + +/** + * Canonicalize the contents of an element tag. + * + * I.e. sorts the attributes alphabetically and canonicalizes their + * values where necessary. Also removes attributes we're not interested in. + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * @param str {String} the contens of the element tag, excluding < and >. + * @param emitFlags {Object} flags used for this output + * @return {String} the canonicalized contents. + */ +function canonicalizeElementTag(str, emitFlags) { + // FIXME: lowercase only if emitFlags.lowercase is set + str = str.toLowerCase(); + + var pos = str.search(' '); + + // element name only + if (pos == -1) { + return str; + } + + var elemName = str.substr(0, pos); + str = str.substr(pos + 1); + + // Even if emitFlags.emitAttrs is not set, we must iterate over the + // attributes to catch the special selection attribute and/or selection + // markers. :( + + // Iterate over attributes, add them to an array, canonicalize their + // contents, and finally output the (remaining) attributes in sorted order. + // Note: We can't do a simple split on space here, because the value of, + // e.g., 'style' attributes may also contain spaces. + var attrs = []; + var selStartInTag = false; + var selEndInTag = false; + + while (str) { + var attrName; + var attrValue = ''; + + pos = str.search(/[ =]/); + if (pos >= 0) { + attrName = str.substr(0, pos); + if (str.charAt(pos) == ' ') { + ++pos; + } + if (str.charAt(pos) == '=') { + ++pos; + if (str.charAt(pos) == ' ') { + ++pos; + } + str = str.substr(pos); + switch (str.charAt(0)) { + case '"': + case "'": + pos = str.indexOf(str.charAt(0), 1); + pos = (pos < 0) ? str.length : pos; + attrValue = str.substring(1, pos); + ++pos; + break; + + default: + pos = str.indexOf(' ', 0); + pos = (pos < 0) ? str.length : pos; + attrValue = (pos == -1) ? str : str.substr(0, pos); + break; + } + attrValue = attrValue.replace(/^ /, ''); + attrValue = attrValue.replace(/ $/, ''); + } + } else { + attrName = str; + } + str = (pos == -1 || pos >= str.length) ? '' : str.substr(pos + 1); + + // Remove special selection attributes. + switch (attrName) { + case ATTRNAME_SEL_START: + selStartInTag = true; + continue; + + case ATTRNAME_SEL_END: + selEndInTag = true; + continue; + } + + switch (attrName) { + case '': + case 'onload': + case 'xmlns': + break; + + default: + if (!emitFlags.emitAttrs) { + break; + } + // >>> fall through >>> + + case 'contenteditable': + attrValue = canonicalizeEntities(attrValue); + attrValue = canonicalizeSingleAttribute(elemName, attrName, attrValue, emitFlags); + if (attrValue !== null) { + attrs.push(attrName + '="' + attrValue + '"'); + } + } + } + + var result = elemName; + + // Sort alphabetically (on full string rather than just attribute value for + // simplicity. Also, attribute names will differ when encountering the '='). + if (attrs.length > 0) { + attrs.sort(); + result += ' ' + attrs.join(' '); + } + + // Add intra-tag selection marker(s) or attribute(s), if any, at the end. + if (selStartInTag && selEndInTag) { + result += ' |'; + } else if (selStartInTag) { + result += ' {'; + } else if (selEndInTag) { + result += ' }'; + } + + return result; +} + +/** + * Canonicalize elements and attributes to facilitate comparison to the + * expectation string: sort attributes, canonicalize values and remove chaff. + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * @param str {String} the HTML string to be canonicalized + * @param emitFlags {Object} flags used for this output + * @return {String} the canonicalized string + */ +function canonicalizeElementsAndAttributes(str, emitFlags) { + var tagStart = str.indexOf('<'); + var tagEnd = 0; + var result = ''; + + while (tagStart >= 0) { + ++tagStart; + if (str.charAt(tagStart) == '/') { + ++tagStart; + } + result = result + canonicalizeEntities(str.substring(tagEnd, tagStart)); + tagEnd = str.indexOf('>', tagStart); + if (tagEnd < 0) { + tagEnd = str.length - 1; + } + if (str.charAt(tagEnd - 1) == '/') { + --tagEnd; + } + var elemStr = str.substring(tagStart, tagEnd); + elemStr = canonicalizeElementTag(elemStr, emitFlags); + result = result + elemStr; + tagStart = str.indexOf('<', tagEnd); + } + return result + canonicalizeEntities(str.substring(tagEnd)); +} + +/** + * Canonicalize an innerHTML string to uniform single whitespaces. + * + * FIXME: running this prevents testing for pre-formatted content + * and the CSS 'white-space' attribute. + * + * @param str {String} the HTML string to be canonicalized + * @return {String} the canonicalized string + */ +function canonicalizeSpaces(str) { + // Collapse sequential whitespace. + str = str.replace(/\s+/g, ' '); + + // Remove spaces immediately inside angle brackets <, >, </ and />. + // While doing this also canonicalize <.../> to <...>. + str = str.replace(/\< ?/g, '<'); + str = str.replace(/\<\/ ?/g, '</'); + str = str.replace(/ ?\/?\>/g, '>'); + + return str; +} + +/** + * Canonicalize an innerHTML string to uniform single whitespaces. + * Also remove comments to retain only embedded selection markers, and + * remove </br> and </hr> if present. + * + * FIXME: running this prevents testing for pre-formatted content + * and the CSS 'white-space' attribute. + * + * @param str {String} the HTML string to be canonicalized + * @return {String} the canonicalized string + */ +function initialCanonicalizationOf(str) { + str = canonicalizeSpaces(str); + str = str.replace(/ ?<!-- ?/g, ''); + str = str.replace(/ ?--> ?/g, ''); + str = str.replace(/<\/[bh]r>/g, ''); + + return str; +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js new file mode 100644 index 000000000..be059cfc8 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js @@ -0,0 +1,489 @@ +/** + * @fileoverview + * Comparison functions used in the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * constants used only in the compare functions. + */ +var RESULT_DIFF = 0; // actual result doesn't match expectation +var RESULT_SEL = 1; // actual result matches expectation in HTML only +var RESULT_EQUAL = 2; // actual result matches expectation in both HTML and selection + +/** + * Gets the test expectations as an array from the passed-in field. + * + * @param {Array|String} the test expectation(s) as string or array. + * @return {Array} test expectations as an array. + */ +function getExpectationArray(expected) { + if (expected === undefined) { + return []; + } + if (expected === null) { + return [null]; + } + switch (typeof expected) { + case 'string': + case 'boolean': + case 'number': + return [expected]; + } + // Assume it's already an array. + return expected; +} + +/** + * Compare a test result to a single expectation string. + * + * FIXME: add support for optional elements/attributes. + * + * @param expected {String} the already canonicalized (with the exception of selection marks) expectation string + * @param actual {String} the already canonicalized (with the exception of selection marks) actual result + * @return {Integer} one of the RESULT_... return values + * @see variables.js for return values + */ +function compareHTMLToSingleExpectation(expected, actual) { + // If the test checks the selection, then the actual string must match the + // expectation exactly. + if (expected == actual) { + return RESULT_EQUAL; + } + + // Remove selection markers and see if strings match then. + expected = expected.replace(/ [{}\|]>/g, '>'); // intra-tag + expected = expected.replace(/[\[\]\^{}\|]/g, ''); // outside tag + actual = actual.replace(/ [{}\|]>/g, '>'); // intra-tag + actual = actual.replace(/[\[\]\^{}\|]/g, ''); // outside tag + + return (expected == actual) ? RESULT_SEL : RESULT_DIFF; +} + +/** + * Compare the current HTMLtest result to the expectation string(s). + * + * @param actual {String/Boolean} actual value + * @param expected {String/Array} expectation(s) + * @param emitFlags {Object} flags to use for canonicalization + * @return {Integer} one of the RESULT_... return values + * @see variables.js for return values + */ +function compareHTMLToExpectation(actual, expected, emitFlags) { + // Find the most favorable result among the possible expectation strings. + var expectedArr = getExpectationArray(expected); + var count = expectedArr ? expectedArr.length : 0; + var best = RESULT_DIFF; + + for (var idx = 0; idx < count && best < RESULT_EQUAL; ++idx) { + var expected = expectedArr[idx]; + expected = canonicalizeSpaces(expected); + expected = canonicalizeElementsAndAttributes(expected, emitFlags); + + var singleResult = compareHTMLToSingleExpectation(expected, actual); + + best = Math.max(best, singleResult); + } + return best; +} + +/** + * Compare the current HTMLtest result to expected and acceptable results + * + * @param expected {String/Array} expected result(s) + * @param accepted {String/Array} accepted result(s) + * @param actual {String} actual result + * @param emitFlags {Object} how to canonicalize the HTML strings + * @param result {Object} [out] object recieving the result of the comparison. + */ +function compareHTMLTestResultTo(expected, accepted, actual, emitFlags, result) { + actual = actual.replace(/[\x60\xb4]/g, ''); + actual = canonicalizeElementsAndAttributes(actual, emitFlags); + + var bestExpected = compareHTMLToExpectation(actual, expected, emitFlags); + + if (bestExpected == RESULT_EQUAL) { + // Shortcut - it doesn't get any better + result.valresult = VALRESULT_EQUAL; + result.selresult = SELRESULT_EQUAL; + return; + } + + var bestAccepted = compareHTMLToExpectation(actual, accepted, emitFlags); + + switch (bestExpected) { + case RESULT_SEL: + switch (bestAccepted) { + case RESULT_EQUAL: + // The HTML was equal to the/an expected HTML result as well + // (just not the selection there), therefore the difference + // between expected and accepted can only lie in the selection. + result.valresult = VALRESULT_EQUAL; + result.selresult = SELRESULT_ACCEPT; + return; + + case RESULT_SEL: + case RESULT_DIFF: + // The acceptable expectations did not yield a better result + // -> stay with the original (i.e., comparison to 'expected') result. + result.valresult = VALRESULT_EQUAL; + result.selresult = SELRESULT_DIFF; + return; + } + break; + + case RESULT_DIFF: + switch (bestAccepted) { + case RESULT_EQUAL: + result.valresult = VALRESULT_ACCEPT; + result.selresult = SELRESULT_EQUAL; + return; + + case RESULT_SEL: + result.valresult = VALRESULT_ACCEPT; + result.selresult = SELRESULT_DIFF; + return; + + case RESULT_DIFF: + result.valresult = VALRESULT_DIFF; + result.selresult = SELRESULT_NA; + return; + } + break; + } + + throw INTERNAL_ERR + HTML_COMPARISON; +} + +/** + * Verify that the canaries are unviolated. + * + * @param container {Object} the test container descriptor as object reference + * @param result {Object} object reference that contains the result data + * @return {Boolean} whether the canaries' HTML is OK (selection flagged, but not fatal) + */ +function verifyCanaries(container, result) { + if (!container.canary) { + return true; + } + + var str = canonicalizeElementsAndAttributes(result.bodyInnerHTML, emitFlagsForCanary); + + if (str.length < 2 * container.canary.length) { + result.valresult = VALRESULT_CANARY; + result.selresult = SELRESULT_NA; + result.output = result.bodyOuterHTML; + return false; + } + + var strBefore = str.substr(0, container.canary.length); + var strAfter = str.substr(str.length - container.canary.length); + + // Verify that the canary stretch doesn't contain any selection markers + if (SELECTION_MARKERS.test(strBefore) || SELECTION_MARKERS.test(strAfter)) { + str = str.replace(SELECTION_MARKERS, ''); + if (str.length < 2 * container.canary.length) { + result.valresult = VALRESULT_CANARY; + result.selresult = SELRESULT_NA; + result.output = result.bodyOuterHTML; + return false; + } + + // Selection escaped contentEditable element, but HTML may still be ok. + result.selresult = SELRESULT_CANARY; + strBefore = str.substr(0, container.canary.length); + strAfter = str.substr(str.length - container.canary.length); + } + + if (strBefore !== container.canary || strAfter !== container.canary) { + result.valresult = VALRESULT_CANARY; + result.selresult = SELRESULT_NA; + result.output = result.bodyOuterHTML; + return false; + } + + return true; +} + +/** + * Compare the current HTMLtest result to the expectation string(s). + * Sets the global result variables. + * + * @param suite {Object} the test suite as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test as object reference + * @param container {Object} the test container description + * @param result {Object} [in/out] the result description, incl. HTML strings + * @see variables.js for result values + */ +function compareHTMLTestResult(suite, group, test, container, result) { + if (!verifyCanaries(container, result)) { + return; + } + + var emitFlags = { + emitAttrs: getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES), + emitStyle: getTestParameter(suite, group, test, PARAM_CHECK_STYLE), + emitClass: getTestParameter(suite, group, test, PARAM_CHECK_CLASS), + emitID: getTestParameter(suite, group, test, PARAM_CHECK_ID), + lowercase: true, + canonicalizeUnits: true + }; + + // 2a.) Compare opening tag - + // decide whether to compare vs. outer or inner HTML based on this. + var openingTagEnd = result.outerHTML.indexOf('>') + 1; + var openingTag = result.outerHTML.substr(0, openingTagEnd); + + openingTag = canonicalizeElementsAndAttributes(openingTag, emitFlags); + var tagCmp = compareHTMLToExpectation(openingTag, container.tagOpen, emitFlags); + + if (tagCmp == RESULT_EQUAL) { + result.output = result.innerHTML; + compareHTMLTestResultTo( + getTestParameter(suite, group, test, PARAM_EXPECTED), + getTestParameter(suite, group, test, PARAM_ACCEPT), + result.innerHTML, + emitFlags, + result) + } else { + result.output = result.outerHTML; + compareHTMLTestResultTo( + getContainerParameter(suite, group, test, container, PARAM_EXPECTED_OUTER), + getContainerParameter(suite, group, test, container, PARAM_ACCEPT_OUTER), + result.outerHTML, + emitFlags, + result) + } +} + +/** + * Insert a selection position indicator. + * + * @param node {DOMNode} the node where to insert the selection indicator + * @param offs {Integer} the offset of the selection indicator + * @param textInd {String} the indicator to use if the node is a text node + * @param elemInd {String} the indicator to use if the node is an element node + */ +function insertSelectionIndicator(node, offs, textInd, elemInd) { + switch (node.nodeType) { + case DOM_NODE_TYPE_TEXT: + // Insert selection marker for text node into text content. + var text = node.data; + node.data = text.substring(0, offs) + textInd + text.substring(offs); + break; + + case DOM_NODE_TYPE_ELEMENT: + var child = node.firstChild; + try { + // node has other children: insert marker as comment node + var comment = document.createComment(elemInd); + while (child && offs) { + --offs; + child = child.nextSibling; + } + if (child) { + node.insertBefore(comment, child); + } else { + node.appendChild(comment); + } + } catch (ex) { + // can't append child comment -> insert as special attribute(s) + switch (elemInd) { + case '|': + node.setAttribute(ATTRNAME_SEL_START, '1'); + node.setAttribute(ATTRNAME_SEL_END, '1'); + break; + + case '{': + node.setAttribute(ATTRNAME_SEL_START, '1'); + break; + + case '}': + node.setAttribute(ATTRNAME_SEL_END, '1'); + break; + } + } + break; + } +} + +/** + * Adds quotes around all text nodes to show cases with non-normalized + * text nodes. Those are not a bug, but may still be usefil in helping to + * debug erroneous cases. + * + * @param node {DOMNode} root node from which to descend + */ +function encloseTextNodesWithQuotes(node) { + switch (node.nodeType) { + case DOM_NODE_TYPE_ELEMENT: + for (var i = 0; i < node.childNodes.length; ++i) { + encloseTextNodesWithQuotes(node.childNodes[i]); + } + break; + + case DOM_NODE_TYPE_TEXT: + node.data = '\x60' + node.data + '\xb4'; + break; + } +} + +/** + * Retrieve the result of a test run and do some preliminary canonicalization. + * + * @param container {Object} the container where to retrieve the result from as object reference + * @param result {Object} object reference that contains the result data + * @return {String} a preliminarily canonicalized innerHTML with selection markers + */ +function prepareHTMLTestResult(container, result) { + // Start with empty strings in case any of the below throws. + result.innerHTML = ''; + result.outerHTML = ''; + + // 1.) insert selection markers + var selRange = createFromWindow(container.win); + if (selRange) { + // save values, since range object gets auto-modified + var node1 = selRange.getAnchorNode(); + var offs1 = selRange.getAnchorOffset(); + var node2 = selRange.getFocusNode(); + var offs2 = selRange.getFocusOffset(); + + // add markers + if (node1 && node1 == node2 && offs1 == offs2) { + // collapsed selection + insertSelectionIndicator(node1, offs1, '^', '|'); + } else { + // Start point and end point are different + if (node1) { + insertSelectionIndicator(node1, offs1, '[', '{'); + } + + if (node2) { + if (node1 == node2 && offs1 < offs2) { + // Anchor indicator was inserted under the same node, so we need + // to shift the offset by 1 + ++offs2; + } + insertSelectionIndicator(node2, offs2, ']', '}'); + } + } + } + + // 2.) insert markers for text node boundaries; + encloseTextNodesWithQuotes(container.editor); + + // 3.) retrieve inner and outer HTML + result.innerHTML = initialCanonicalizationOf(container.editor.innerHTML); + result.bodyInnerHTML = initialCanonicalizationOf(container.body.innerHTML); + if (goog.userAgent.IE) { + result.outerHTML = initialCanonicalizationOf(container.editor.outerHTML); + result.bodyOuterHTML = initialCanonicalizationOf(container.body.outerHTML); + result.outerHTML = result.outerHTML.replace(/^\s+/, ''); + result.outerHTML = result.outerHTML.replace(/\s+$/, ''); + result.bodyOuterHTML = result.bodyOuterHTML.replace(/^\s+/, ''); + result.bodyOuterHTML = result.bodyOuterHTML.replace(/\s+$/, ''); + } else { + result.outerHTML = initialCanonicalizationOf(new XMLSerializer().serializeToString(container.editor)); + result.bodyOuterHTML = initialCanonicalizationOf(new XMLSerializer().serializeToString(container.body)); + } +} + +/** + * Compare a text test result to the expectation string(s). + * + * @param suite {Object} the test suite as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test as object reference + * @param actual {String/Boolean} actual value + * @param expected {String/Array} expectation(s) + * @return {Boolean} whether we found a match + */ +function compareTextTestResultWith(suite, group, test, actual, expected) { + var expectedArr = getExpectationArray(expected); + // Find the most favorable result among the possible expectation strings. + var count = expectedArr.length; + + // If the value matches the expectation exactly, then we're fine. + for (var idx = 0; idx < count; ++idx) { + if (actual === expectedArr[idx]) + return true; + } + + // Otherwise see if we should canonicalize specific value types. + // + // We only need to look at font name, color and size units if the originating + // test was both a) queryCommandValue and b) querying a font name/color/size + // specific criterion. + // + // TODO(rolandsteiner): This is ugly! Refactor! + switch (getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) { + case 'backcolor': + case 'forecolor': + case 'hilitecolor': + for (var idx = 0; idx < count; ++idx) { + if (new Color(actual).compare(new Color(expectedArr[idx]))) + return true; + } + return false; + + case 'fontname': + for (var idx = 0; idx < count; ++idx) { + if (new FontName(actual).compare(new FontName(expectedArr[idx]))) + return true; + } + return false; + + case 'fontsize': + for (var idx = 0; idx < count; ++idx) { + if (new FontSize(actual).compare(new FontSize(expectedArr[idx]))) + return true; + } + return false; + } + + return false; +} + +/** + * Compare the passed-in text test result to the expectation string(s). + * Sets the global result variables. + * + * @param suite {Object} the test suite as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test as object reference + * @param actual {String/Boolean} actual value + * @return {Integer} a RESUTLHTML... result value + * @see variables.js for result values + */ +function compareTextTestResult(suite, group, test, result) { + var expected = getTestParameter(suite, group, test, PARAM_EXPECTED); + if (compareTextTestResultWith(suite, group, test, result.output, expected)) { + result.valresult = VALRESULT_EQUAL; + return; + } + var accepted = getTestParameter(suite, group, test, PARAM_ACCEPT); + if (accepted && compareTextTestResultWith(suite, group, test, result.output, accepted)) { + result.valresult = VALRESULT_ACCEPT; + return; + } + result.valresult = VALRESULT_DIFF; +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js new file mode 100644 index 000000000..897efa011 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js @@ -0,0 +1,456 @@ +/** + * @fileoverview + * Functions used to format the test result output. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * Writes a fatal error to the output (replaces alert box) + * + * @param text {String} text to output + */ +function writeFatalError(text) { + var errorsStart = document.getElementById('errors'); + var divider = document.getElementById('divider'); + if (!errorsStart) { + errorsStart = document.createElement('hr'); + errorsStart.id = 'errors'; + divider.parentNode.insertBefore(errorsStart, divider); + } + var error = document.createElement('div'); + error.className = 'fatalerror'; + error.innerHTML = 'FATAL ERROR: ' + escapeOutput(text); + errorsStart.parentNode.insertBefore(error, divider); +} + +/** + * Generates a unique ID for a given single test out of the suite ID and + * test ID. + * + * @param suiteID {string} ID string of the suite + * @param testID {string} ID string of the individual tests + * @return {string} globally unique ID + */ +function generateOutputID(suiteID, testID) { + return commonIDPrefix + '-' + suiteID + '_' + testID; +} + +/** + * Function to highlight the selection markers + * + * @param str {String} a HTML string containing selection markers + * @return {String} the HTML string with highlighting tags around the markers + */ +function highlightSelectionMarkers(str) { + str = str.replace(/\[/g, '<span class="sel">[</span>'); + str = str.replace(/\]/g, '<span class="sel">]</span>'); + str = str.replace(/\^/g, '<span class="sel">^</span>'); + str = str.replace(/{/g, '<span class="sel">{</span>'); + str = str.replace(/}/g, '<span class="sel">}</span>'); + str = str.replace(/\|/g, '<b class="sel">|</b>'); + return str; +} + +/** + * Function to highlight the selection markers + * + * @param str {String} a HTML string containing selection markers + * @return {String} the HTML string with highlighting tags around the markers + */ +function highlightSelectionMarkersAndTextNodes(str) { + str = highlightSelectionMarkers(str); + str = str.replace(/\x60/g, '<span class="txt">'); + str = str.replace(/\xb4/g, '</span>'); + return str; +} + +/** + * Function to format output according to type + * + * @param value {String/Boolean} string or value to format + * @return {String} HTML-formatted string + */ +function formatValueOrString(value) { + if (value === undefined) + return '<i>undefined</i>'; + if (value === null) + return '<i>null</i>'; + + switch (typeof value) { + case 'boolean': + return '<i>' + value.toString() + '</i>'; + + case 'number': + return value.toString(); + + case 'string': + return "'" + escapeOutput(value) + "'"; + + default: + return '<i>(' + escapeOutput(value.toString()) + ')</i>'; + } +} + +/** + * Function to highlight text nodes + * + * @param suite {Object} the suite the test belongs to + * @param group {Object} the group within the suite the test belongs to + * @param test {Object} the test description as object reference + * @param actual {String} a HTML string containing text nodes with markers + * @return {String} string with highlighting tags around the text node parts + */ +function formatActualResult(suite, group, test, actual) { + if (typeof actual != 'string') + return formatValueOrString(actual); + + actual = escapeOutput(actual); + + // Fade attributes (or just style) if not actually tested for + if (!getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES)) { + actual = actual.replace(/([^ =]+)=\x22([^\x22]*)\x22/g, '<span class="fade">$1="$2"</span>'); + } else { + // NOTE: convert 'class="..."' first, before adding other <span class="fade">...</span> !!! + if (!getTestParameter(suite, group, test, PARAM_CHECK_CLASS)) { + actual = actual.replace(/class=\x22([^\x22]*)\x22/g, '<span class="fade">class="$1"</span>'); + } + if (!getTestParameter(suite, group, test, PARAM_CHECK_STYLE)) { + actual = actual.replace(/style=\x22([^\x22]*)\x22/g, '<span class="fade">style="$1"</span>'); + } + if (!getTestParameter(suite, group, test, PARAM_CHECK_ID)) { + actual = actual.replace(/id=\x22([^\x22]*)\x22/g, '<span class="fade">id="$1"</span>'); + } else { + // fade out contenteditable host element's 'editor-<xyz>' ID. + actual = actual.replace(/id=\x22editor-([^\x22]*)\x22/g, '<span class="fade">id="editor-$1"</span>'); + } + // grey out 'xmlns' + actual = actual.replace(/xmlns=\x22([^\x22]*)\x22/g, '<span class="fade">xmlns="$1"</span>'); + // remove 'onload' + actual = actual.replace(/onload=\x22[^\x22]*\x22 ?/g, ''); + } + // Highlight selection markers and text nodes. + actual = highlightSelectionMarkersAndTextNodes(actual); + + return actual; +} + +/** + * Escape text content for use with .innerHTML. + * + * @param str {String} HTML text to displayed + * @return {String} the escaped HTML + */ +function escapeOutput(str) { + return str ? str.replace(/\</g, '<').replace(/\>/g, '>') : ''; +} + +/** + * Fills in a single output table cell + * + * @param id {String} ID of the table cell + * @param val {String} inner HTML to set + * @param ttl {String, optional} value of the 'title' attribute + * @param cls {String, optional} class name for the cell + */ +function setTD(id, val, ttl, cls) { + var td = document.getElementById(id); + if (td) { + td.innerHTML = val; + if (ttl) { + td.title = ttl; + } + if (cls) { + td.className = cls; + } + } +} + +/** + * Outputs the results of a single test suite + * + * @param suite {Object} test suite as object reference + * @param clsID {String} test class ID ('Proposed', 'RFC', 'Final') + * @param group {Object} the group of tests within the suite the test belongs to + * @param testIdx {Object} the test as object reference + */ +function outputTestResults(suite, clsID, group, test) { + var suiteID = suite.id; + var cls = suite[clsID]; + var trID = generateOutputID(suiteID, test.id); + var testResult = results[suiteID][clsID][test.id]; + var testValOut = VALOUTPUT[testResult.valresult]; + var testSelOut = SELOUTPUT[testResult.selresult]; + + var suiteChecksSelOnly = !suiteChecksHTMLOrText(suite); + var testUsesHTML = !!getTestParameter(suite, group, test, PARAM_EXECCOMMAND) || + !!getTestParameter(suite, group, test, PARAM_FUNCTION); + + // Set background color for test ID + var td = document.getElementById(trID + IDOUT_TESTID); + if (td) { + td.className = (suiteChecksSelOnly && testResult.selresult != SELRESULT_NA) ? testSelOut.css : testValOut.css; + } + + // Fill in "Command" and "Value" cells + var cmd; + var cmdOutput = ' '; + var valOutput = ' '; + + if (cmd = getTestParameter(suite, group, test, PARAM_EXECCOMMAND)) { + cmdOutput = escapeOutput(cmd); + var val = getTestParameter(suite, group, test, PARAM_VALUE); + if (val !== undefined) { + valOutput = formatValueOrString(val); + } + } else if (cmd = getTestParameter(suite, group, test, PARAM_FUNCTION)) { + cmdOutput = '<i>' + escapeOutput(cmd) + '</i>'; + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSUPPORTED)) { + cmdOutput = '<i>queryCommandSupported</i>'; + valOutput = escapeOutput(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDENABLED)) { + cmdOutput = '<i>queryCommandEnabled</i>'; + valOutput = escapeOutput(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDINDETERM)) { + cmdOutput = '<i>queryCommandIndeterm</i>'; + valOutput = escapeOutput(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSTATE)) { + cmdOutput = '<i>queryCommandState</i>'; + valOutput = escapeOutput(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) { + cmdOutput = '<i>queryCommandValue</i>'; + valOutput = escapeOutput(cmd); + } else { + cmdOutput = '<i>(none)</i>'; + } + setTD(trID + IDOUT_COMMAND, cmdOutput); + setTD(trID + IDOUT_VALUE, valOutput); + + // Fill in "Attribute checked?" and "Style checked?" cells + if (testUsesHTML) { + var checkAttrs = getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES); + var checkStyle = getTestParameter(suite, group, test, PARAM_CHECK_STYLE); + + setTD(trID + IDOUT_CHECKATTRS, + checkAttrs ? OUTSTR_YES : OUTSTR_NO, + checkAttrs ? 'attributes must match' : 'attributes are ignored'); + + if (checkAttrs && checkStyle) { + setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_YES, 'style attribute contents must match'); + } else if (checkAttrs) { + setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NO, 'style attribute contents is ignored'); + } else { + setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NO, 'all attributes (incl. style) are ignored'); + } + } else { + setTD(trID + IDOUT_CHECKATTRS, OUTSTR_NA, 'attributes not applicable'); + setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NA, 'style not applicable'); + } + + // Fill in test pad specification cell (initial HTML + selection markers) + setTD(trID + IDOUT_PAD, highlightSelectionMarkers(escapeOutput(getTestParameter(suite, group, test, PARAM_PAD)))); + + // Fill in expected result(s) cell + var expectedOutput = ''; + var expectedArr = getExpectationArray(getTestParameter(suite, group, test, PARAM_EXPECTED)); + for (var idx = 0; idx < expectedArr.length; ++idx) { + if (expectedOutput) { + expectedOutput += '\xA0\xA0\xA0<i>or</i><br>'; + } + expectedOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(expectedArr[idx])) + : formatValueOrString(expectedArr[idx]); + } + var acceptedArr = getExpectationArray(getTestParameter(suite, group, test, PARAM_ACCEPT)); + for (var idx = 0; idx < acceptedArr.length; ++idx) { + expectedOutput += '<span class="accexp">\xA0\xA0\xA0<i>or</i></span><br><span class="accexp">'; + expectedOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(acceptedArr[idx])) + : formatValueOrString(acceptedArr[idx]); + expectedOutput += '</span>'; + } + // TODO(rolandsteiner): THIS IS UGLY, relying on 'div' container being index 2, + // AND not allowing other containers to have 'outer' results - change!!! + var outerOutput = ''; + expectedArr = getExpectationArray(getContainerParameter(suite, group, test, containers[2], PARAM_EXPECTED_OUTER)); + for (var idx = 0; idx < expectedArr.length; ++idx) { + if (outerOutput) { + outerOutput += '\xA0\xA0\xA0<i>or</i><br>'; + } + outerOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(expectedArr[idx])) + : formatValueOrString(expectedArr[idx]); + } + acceptedArr = getExpectationArray(getContainerParameter(suite, group, test, containers[2], PARAM_ACCEPT_OUTER)); + for (var idx = 0; idx < acceptedArr.length; ++idx) { + if (outerOutput) { + outerOutput += '<span class="accexp">\xA0\xA0\xA0<i>or</i></span><br>'; + } + outerOutput += '<span class="accexp">'; + outerOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(acceptedArr[idx])) + : formatValueOrString(acceptedArr[idx]); + outerOutput += '</span>'; + } + if (outerOutput) { + expectedOutput += '<hr>' + outerOutput; + } + setTD(trID + IDOUT_EXPECTED, expectedOutput); + + // Iterate over the individual container results + for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) { + var cntID = containers[cntIdx].id; + var cntTD = document.getElementById(trID + IDOUT_CONTAINER + cntID); + var cntResult = testResult[cntID]; + var cntValOut = VALOUTPUT[cntResult.valresult]; + var cntSelOut = SELOUTPUT[cntResult.selresult]; + var cssVal = cntValOut.css; + var cssSel = (!suiteChecksSelOnly || cntResult.selresult != SELRESULT_NA) ? cntSelOut.css : cssVal; + var cssCnt = cssVal; + + // Fill in result status cell ("PASS", "ACC.", "FAIL", "EXC.", etc.) + setTD(trID + IDOUT_STATUSVAL + cntID, cntValOut.output, cntValOut.title, cssVal); + + // Fill in selection status cell ("PASS", "ACC.", "FAIL", "N/A") + setTD(trID + IDOUT_STATUSSEL + cntID, cntSelOut.output, cntSelOut.title, cssSel); + + // Fill in actual result + switch (cntResult.valresult) { + case VALRESULT_SETUP_EXCEPTION: + setTD(trID + IDOUT_ACTUAL + cntID, + SETUP_EXCEPTION + '(mouseover)', + escapeOutput(cntResult.output), + cssVal); + break; + + case VALRESULT_EXECUTION_EXCEPTION: + setTD(trID + IDOUT_ACTUAL + cntID, + EXECUTION_EXCEPTION + '(mouseover)', + escapeOutput(cntResult.output.toString()), + cssVal); + break; + + case VALRESULT_VERIFICATION_EXCEPTION: + setTD(trID + IDOUT_ACTUAL + cntID, + VERIFICATION_EXCEPTION + '(mouseover)', + escapeOutput(cntResult.output.toString()), + cssVal); + break; + + case VALRESULT_UNSUPPORTED: + setTD(trID + IDOUT_ACTUAL + cntID, + escapeOutput(cntResult.output), + '', + cssVal); + break; + + case VALRESULT_CANARY: + setTD(trID + IDOUT_ACTUAL + cntID, + highlightSelectionMarkersAndTextNodes(escapeOutput(cntResult.output)), + '', + cssVal); + break; + + case VALRESULT_DIFF: + case VALRESULT_ACCEPT: + case VALRESULT_EQUAL: + if (!testUsesHTML) { + setTD(trID + IDOUT_ACTUAL + cntID, + formatValueOrString(cntResult.output), + '', + cssVal); + } else if (cntResult.selresult == SELRESULT_CANARY) { + cssCnt = suiteChecksSelOnly ? cssSel : cssVal; + setTD(trID + IDOUT_ACTUAL + cntID, + highlightSelectionMarkersAndTextNodes(escapeOutput(cntResult.output)), + '', + cssCnt); + } else { + cssCnt = suiteChecksSelOnly ? cssSel : cssVal; + setTD(trID + IDOUT_ACTUAL + cntID, + formatActualResult(suite, group, test, cntResult.output), + '', + cssCnt); + } + break; + + default: + cssCnt = 'exception'; + setTD(trID + IDOUT_ACTUAL + cntID, + INTERNAL_ERR + 'UNKNOWN RESULT VALUE', + '', + cssCnt); + } + + if (cntTD) { + cntTD.className = cssCnt; + } + } +} + +/** + * Outputs the results of a single test suite + * + * @param {Object} suite as object reference + */ +function outputTestSuiteResults(suite) { + var suiteID = suite.id; + var span; + + span = document.getElementById(suiteID + '-score'); + if (span) { + span.innerHTML = results[suiteID].valscore + '/' + results[suiteID].count; + } + span = document.getElementById(suiteID + '-selscore'); + if (span) { + span.innerHTML = results[suiteID].selscore + '/' + results[suiteID].count; + } + span = document.getElementById(suiteID + '-time'); + if (span) { + span.innerHTML = results[suiteID].time; + } + span = document.getElementById(suiteID + '-progress'); + if (span) { + span.style.color = 'green'; + } + + for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) { + var clsID = testClassIDs[clsIdx]; + var cls = suite[clsID]; + if (!cls) + continue; + + span = document.getElementById(suiteID + '-' + clsID + '-score'); + if (span) { + span.innerHTML = results[suiteID][clsID].valscore + '/' + results[suiteID][clsID].count; + } + span = document.getElementById(suiteID + '-' + clsID + '-selscore'); + if (span) { + span.innerHTML = results[suiteID][clsID].selscore + '/' + results[suiteID][clsID].count; + } + + var groupCount = cls.length; + + for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) { + var group = cls[groupIdx]; + var testCount = group.tests.length; + + for (var testIdx = 0; testIdx < testCount; ++testIdx) { + var test = group.tests[testIdx]; + + outputTestResults(suite, clsID, group, test); + } + } + } +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js new file mode 100644 index 000000000..282f0d907 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js @@ -0,0 +1,269 @@ +/** + * @fileoverview + * Functions used to handle test and expectation strings. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * Normalize text selection indicators and convert inter-element selection + * indicators to comments. + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * @param pad {String} HTML string that includes selection marker characters + * @return {String} the HTML string with the selection markers converted + */ +function convertSelectionIndicators(pad) { + // Sanity check: Markers { } | only directly before or after an element, + // or just before a closing > (i.e., not within a text node). + // Note that intra-tag selection markers have already been converted to the + // special selection attribute(s) above. + if (/[^>][{}\|][^<>]/.test(pad) || + /^[{}\|][^<]/.test(pad) || + /[^>][{}\|]$/.test(pad) || + /^[{}\|]*$/.test(pad)) { + throw SETUP_BAD_SELECTION_SPEC; + } + + // Convert intra-tag selection markers to special attributes. + pad = pad.replace(/\{\>/g, ATTRNAME_SEL_START + '="1">'); + pad = pad.replace(/\}\>/g, ATTRNAME_SEL_END + '="1">'); + pad = pad.replace(/\|\>/g, ATTRNAME_SEL_START + '="1" ' + + ATTRNAME_SEL_END + '="1">'); + + // Convert remaining {, }, | to comments with '[' and ']' data. + pad = pad.replace('{', '<!--[-->'); + pad = pad.replace('}', '<!--]-->'); + pad = pad.replace('|', '<!--[--><!--]-->'); + + // Convert caret indicator ^ to empty selection indicator [] + // (this simplifies further processing). + pad = pad.replace(/\^/, '[]'); + + return pad; +} + +/** + * Derives one point of the selection from the indicators with the HTML tree: + * '[' or ']' within a text or comment node, or the special selection + * attributes within an element node. + * + * @param root {DOMNode} root node of the recursive search + * @param marker {String} which marker to look for: '[' or ']' + * @return {Object} a pair object: {node: {DOMNode}/null, offset: {Integer}} + */ +function deriveSelectionPoint(root, marker) { + switch (root.nodeType) { + case DOM_NODE_TYPE_ELEMENT: + if (root.attributes) { + // Note: getAttribute() is necessary for this to work on all browsers! + if (marker == '[' && root.getAttribute(ATTRNAME_SEL_START)) { + root.removeAttribute(ATTRNAME_SEL_START); + return {node: root, offs: 0}; + } + if (marker == ']' && root.getAttribute(ATTRNAME_SEL_END)) { + root.removeAttribute(ATTRNAME_SEL_END); + return {node: root, offs: 0}; + } + } + for (var i = 0; i < root.childNodes.length; ++i) { + var pair = deriveSelectionPoint(root.childNodes[i], marker); + if (pair.node) { + return pair; + } + } + break; + + case DOM_NODE_TYPE_TEXT: + var pos = root.data.indexOf(marker); + if (pos != -1) { + // Remove selection marker from text. + var nodeText = root.data; + root.data = nodeText.substr(0, pos) + nodeText.substr(pos + 1); + return {node: root, offs: pos }; + } + break; + + case DOM_NODE_TYPE_COMMENT: + var pos = root.data.indexOf(marker); + if (pos != -1) { + // Remove comment node from parent. + var helper = root.previousSibling; + + for (pos = 0; helper; ++pos ) { + helper = helper.previousSibling; + } + helper = root; + root = root.parentNode; + root.removeChild(helper); + return {node: root, offs: pos }; + } + break; + } + + return {node: null, offs: 0 }; +} + +/** + * Initialize the test HTML with the starting state specified in the test. + * + * The selection is specified "inline", using special characters: + * ^ a collapsed text caret selection (same as []) + * [ the selection start within a text node + * ] the selection end within a text node + * | collapsed selection between elements (same as {}) + * { selection starting with the following element + * } selection ending with the preceding element + * {, } and | can also be used within an element tag, just before the closing + * angle bracket > to specify a selection [element, 0] where the element + * doesn't otherwise have any children. Ex.: <hr {>foobarbaz<hr }> + * + * Explicit and implicit specification can also be mixed between the 2 points. + * + * A pad string must only contain at most ONE of the above that is suitable for + * that start or end point, respectively, and must contain either both or none. + * + * @param suite {Object} suite that test originates in as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} test to be run as object reference + * @param container {Object} container descriptor as object reference + */ +function initContainer(suite, group, test, container) { + var pad = getTestParameter(suite, group, test, PARAM_PAD); + pad = canonicalizeSpaces(pad); + pad = convertSelectionIndicators(pad); + + if (container.editorID) { + container.body.innerHTML = container.canary + container.tagOpen + pad + container.tagClose + container.canary; + container.editor = container.doc.getElementById(container.editorID); + } else { + container.body.innerHTML = pad; + container.editor = container.body; + } + + win = container.win; + doc = container.doc; + body = container.body; + editor = container.editor; + sel = null; + + if (!editor) { + throw SETUP_CONTAINER; + } + + if (getTestParameter(suite, group, test, PARAM_STYLE_WITH_CSS)) { + try { + container.doc.execCommand('styleWithCSS', false, true); + } catch (ex) { + // ignore exception if unsupported + } + } + + var selAnchor = deriveSelectionPoint(editor, '['); + var selFocus = deriveSelectionPoint(editor, ']'); + + // sanity check + if (!selAnchor || !selFocus) { + throw SETUP_SELECTION; + } + + if (!selAnchor.node || !selFocus.node) { + if (selAnchor.node || selFocus.node) { + // Broken test: only one selection point was specified + throw SETUP_BAD_SELECTION_SPEC; + } + sel = null; + return; + } + + if (selAnchor.node === selFocus.node) { + if (selAnchor.offs > selFocus.offs) { + // Both selection points are within the same node, the selection was + // specified inline (thus the end indicator element or character was + // removed), and the end point is before the start (reversed selection). + // Start offset that was derived is now off by 1 and needs adjustment. + --selAnchor.offs; + } + + if (selAnchor.offs === selFocus.offs) { + createCaret(selAnchor.node, selAnchor.offs).select(); + try { + sel = win.getSelection(); + } catch (ex) { + sel = undefined; + } + return; + } + } + + createFromNodes(selAnchor.node, selAnchor.offs, selFocus.node, selFocus.offs).select(); + + try { + sel = win.getSelection(); + } catch (ex) { + sel = undefined; + } +} + +/** + * Reset the editor element after a test is run. + * + * @param container {Object} container descriptor as object reference + */ +function resetContainer(container) { + // Remove errant styles and attributes that may have been set on the <body>. + container.body.removeAttribute('style'); + container.body.removeAttribute('color'); + container.body.removeAttribute('bgcolor'); + + try { + container.doc.execCommand('styleWithCSS', false, false); + } catch (ex) { + // Ignore exception if unsupported. + } +} + +/** + * Initialize the editor document. + */ +function initEditorDocs() { + for (var c = 0; c < containers.length; ++c) { + var container = containers[c]; + + container.iframe = document.getElementById('iframe-' + container.id); + container.win = container.iframe.contentWindow; + container.doc = container.win.document; + container.body = container.doc.body; + // container.editor is set per test (changes on embedded editor elements). + + // Some browsers require a selection to go with their 'styleWithCSS'. + try { + container.win.getSelection().selectAllChildren(editor); + } catch (ex) { + // ignore exception if unsupported + } + // Default styleWithCSS to false. + try { + container.doc.execCommand('styleWithCSS', false, false); + } catch (ex) { + // ignore exception if unsupported + } + } +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js new file mode 100644 index 000000000..24aef7ae9 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js @@ -0,0 +1,5 @@ +goog.require('goog.dom.Range'); + +window.createFromWindow = goog.dom.Range.createFromWindow; +window.createFromNodes = goog.dom.Range.createFromNodes; +window.createCaret = goog.dom.Range.createCaret; diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js new file mode 100644 index 000000000..f323cf9b6 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js @@ -0,0 +1,6184 @@ +var COMPILED = false; +var goog = goog || {}; +goog.global = this; +goog.DEBUG = true; +goog.LOCALE = "en"; +goog.evalWorksForGlobals_ = null; +goog.provide = function(name) { + if(!COMPILED) { + if(goog.getObjectByName(name) && !goog.implicitNamespaces_[name]) { + throw Error('Namespace "' + name + '" already declared.'); + } + var namespace = name; + while(namespace = namespace.substring(0, namespace.lastIndexOf("."))) { + goog.implicitNamespaces_[namespace] = true + } + } + goog.exportPath_(name) +}; +if(!COMPILED) { + goog.implicitNamespaces_ = {} +} +goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) { + var parts = name.split("."); + var cur = opt_objectToExportTo || goog.global; + if(!(parts[0] in cur) && cur.execScript) { + cur.execScript("var " + parts[0]) + } + for(var part;parts.length && (part = parts.shift());) { + if(!parts.length && goog.isDef(opt_object)) { + cur[part] = opt_object + }else { + if(cur[part]) { + cur = cur[part] + }else { + cur = cur[part] = {} + } + } + } +}; +goog.getObjectByName = function(name, opt_obj) { + var parts = name.split("."); + var cur = opt_obj || goog.global; + for(var part;part = parts.shift();) { + if(cur[part]) { + cur = cur[part] + }else { + return null + } + } + return cur +}; +goog.globalize = function(obj, opt_global) { + var global = opt_global || goog.global; + for(var x in obj) { + global[x] = obj[x] + } +}; +goog.addDependency = function(relPath, provides, requires) { + if(!COMPILED) { + var provide, require; + var path = relPath.replace(/\\/g, "/"); + var deps = goog.dependencies_; + for(var i = 0;provide = provides[i];i++) { + deps.nameToPath[provide] = path; + if(!(path in deps.pathToNames)) { + deps.pathToNames[path] = {} + } + deps.pathToNames[path][provide] = true + } + for(var j = 0;require = requires[j];j++) { + if(!(path in deps.requires)) { + deps.requires[path] = {} + } + deps.requires[path][require] = true + } + } +}; +goog.require = function(rule) { + if(!COMPILED) { + if(goog.getObjectByName(rule)) { + return + } + var path = goog.getPathFromDeps_(rule); + if(path) { + goog.included_[path] = true; + goog.writeScripts_() + }else { + var errorMessage = "goog.require could not find: " + rule; + if(goog.global.console) { + goog.global.console["error"](errorMessage) + } + throw Error(errorMessage); + } + } +}; +goog.basePath = ""; +goog.global.CLOSURE_BASE_PATH; +goog.nullFunction = function() { +}; +goog.identityFunction = function(var_args) { + return arguments[0] +}; +goog.abstractMethod = function() { + throw Error("unimplemented abstract method"); +}; +goog.addSingletonGetter = function(ctor) { + ctor.getInstance = function() { + return ctor.instance_ || (ctor.instance_ = new ctor) + } +}; +if(!COMPILED) { + goog.included_ = {}; + goog.dependencies_ = {pathToNames:{}, nameToPath:{}, requires:{}, visited:{}, written:{}}; + goog.inHtmlDocument_ = function() { + var doc = goog.global.document; + return typeof doc != "undefined" && "write" in doc + }; + goog.findBasePath_ = function() { + if(!goog.inHtmlDocument_()) { + return + } + var doc = goog.global.document; + if(goog.global.CLOSURE_BASE_PATH) { + goog.basePath = goog.global.CLOSURE_BASE_PATH; + return + } + var scripts = doc.getElementsByTagName("script"); + for(var i = scripts.length - 1;i >= 0;--i) { + var src = scripts[i].src; + var l = src.length; + if(src.substr(l - 7) == "base.js") { + goog.basePath = src.substr(0, l - 7); + return + } + } + }; + goog.writeScriptTag_ = function(src) { + if(goog.inHtmlDocument_() && !goog.dependencies_.written[src]) { + goog.dependencies_.written[src] = true; + var doc = goog.global.document; + doc.write('<script type="text/javascript" src="' + src + '"></' + "script>") + } + }; + goog.writeScripts_ = function() { + var scripts = []; + var seenScript = {}; + var deps = goog.dependencies_; + function visitNode(path) { + if(path in deps.written) { + return + } + if(path in deps.visited) { + if(!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path) + } + return + } + deps.visited[path] = true; + if(path in deps.requires) { + for(var requireName in deps.requires[path]) { + if(requireName in deps.nameToPath) { + visitNode(deps.nameToPath[requireName]) + }else { + if(!goog.getObjectByName(requireName)) { + throw Error("Undefined nameToPath for " + requireName); + } + } + } + } + if(!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path) + } + } + for(var path in goog.included_) { + if(!deps.written[path]) { + visitNode(path) + } + } + for(var i = 0;i < scripts.length;i++) { + if(scripts[i]) { + goog.writeScriptTag_(goog.basePath + scripts[i]) + }else { + throw Error("Undefined script input"); + } + } + }; + goog.getPathFromDeps_ = function(rule) { + if(rule in goog.dependencies_.nameToPath) { + return goog.dependencies_.nameToPath[rule] + }else { + return null + } + }; + goog.findBasePath_(); +} +goog.typeOf = function(value) { + var s = typeof value; + if(s == "object") { + if(value) { + if(value instanceof Array || !(value instanceof Object) && Object.prototype.toString.call(value) == "[object Array]" || typeof value.length == "number" && typeof value.splice != "undefined" && typeof value.propertyIsEnumerable != "undefined" && !value.propertyIsEnumerable("splice")) { + return"array" + } + if(!(value instanceof Object) && (Object.prototype.toString.call(value) == "[object Function]" || typeof value.call != "undefined" && typeof value.propertyIsEnumerable != "undefined" && !value.propertyIsEnumerable("call"))) { + return"function" + } + }else { + return"null" + } + }else { + if(s == "function" && typeof value.call == "undefined") { + return"object" + } + } + return s +}; +goog.propertyIsEnumerableCustom_ = function(object, propName) { + if(propName in object) { + for(var key in object) { + if(key == propName && Object.prototype.hasOwnProperty.call(object, propName)) { + return true + } + } + } + return false +}; +goog.propertyIsEnumerable_ = function(object, propName) { + if(object instanceof Object) { + return Object.prototype.propertyIsEnumerable.call(object, propName) + }else { + return goog.propertyIsEnumerableCustom_(object, propName) + } +}; +goog.isDef = function(val) { + return val !== undefined +}; +goog.isNull = function(val) { + return val === null +}; +goog.isDefAndNotNull = function(val) { + return val != null +}; +goog.isArray = function(val) { + return goog.typeOf(val) == "array" +}; +goog.isArrayLike = function(val) { + var type = goog.typeOf(val); + return type == "array" || type == "object" && typeof val.length == "number" +}; +goog.isDateLike = function(val) { + return goog.isObject(val) && typeof val.getFullYear == "function" +}; +goog.isString = function(val) { + return typeof val == "string" +}; +goog.isBoolean = function(val) { + return typeof val == "boolean" +}; +goog.isNumber = function(val) { + return typeof val == "number" +}; +goog.isFunction = function(val) { + return goog.typeOf(val) == "function" +}; +goog.isObject = function(val) { + var type = goog.typeOf(val); + return type == "object" || type == "array" || type == "function" +}; +goog.getUid = function(obj) { + return obj[goog.UID_PROPERTY_] || (obj[goog.UID_PROPERTY_] = ++goog.uidCounter_) +}; +goog.removeUid = function(obj) { + if("removeAttribute" in obj) { + obj.removeAttribute(goog.UID_PROPERTY_) + } + try { + delete obj[goog.UID_PROPERTY_] + }catch(ex) { + } +}; +goog.UID_PROPERTY_ = "closure_uid_" + Math.floor(Math.random() * 2147483648).toString(36); +goog.uidCounter_ = 0; +goog.getHashCode = goog.getUid; +goog.removeHashCode = goog.removeUid; +goog.cloneObject = function(obj) { + var type = goog.typeOf(obj); + if(type == "object" || type == "array") { + if(obj.clone) { + return obj.clone() + } + var clone = type == "array" ? [] : {}; + for(var key in obj) { + clone[key] = goog.cloneObject(obj[key]) + } + return clone + } + return obj +}; +Object.prototype.clone; +goog.bind = function(fn, selfObj, var_args) { + var context = selfObj || goog.global; + if(arguments.length > 2) { + var boundArgs = Array.prototype.slice.call(arguments, 2); + return function() { + var newArgs = Array.prototype.slice.call(arguments); + Array.prototype.unshift.apply(newArgs, boundArgs); + return fn.apply(context, newArgs) + } + }else { + return function() { + return fn.apply(context, arguments) + } + } +}; +goog.partial = function(fn, var_args) { + var args = Array.prototype.slice.call(arguments, 1); + return function() { + var newArgs = Array.prototype.slice.call(arguments); + newArgs.unshift.apply(newArgs, args); + return fn.apply(this, newArgs) + } +}; +goog.mixin = function(target, source) { + for(var x in source) { + target[x] = source[x] + } +}; +goog.now = Date.now || function() { + return+new Date +}; +goog.globalEval = function(script) { + if(goog.global.execScript) { + goog.global.execScript(script, "JavaScript") + }else { + if(goog.global.eval) { + if(goog.evalWorksForGlobals_ == null) { + goog.global.eval("var _et_ = 1;"); + if(typeof goog.global["_et_"] != "undefined") { + delete goog.global["_et_"]; + goog.evalWorksForGlobals_ = true + }else { + goog.evalWorksForGlobals_ = false + } + } + if(goog.evalWorksForGlobals_) { + goog.global.eval(script) + }else { + var doc = goog.global.document; + var scriptElt = doc.createElement("script"); + scriptElt.type = "text/javascript"; + scriptElt.defer = false; + scriptElt.appendChild(doc.createTextNode(script)); + doc.body.appendChild(scriptElt); + doc.body.removeChild(scriptElt) + } + }else { + throw Error("goog.globalEval not available"); + } + } +}; +goog.typedef = true; +goog.cssNameMapping_; +goog.getCssName = function(className, opt_modifier) { + var cssName = className + (opt_modifier ? "-" + opt_modifier : ""); + return goog.cssNameMapping_ && cssName in goog.cssNameMapping_ ? goog.cssNameMapping_[cssName] : cssName +}; +goog.setCssNameMapping = function(mapping) { + goog.cssNameMapping_ = mapping +}; +goog.getMsg = function(str, opt_values) { + var values = opt_values || {}; + for(var key in values) { + var value = ("" + values[key]).replace(/\$/g, "$$$$"); + str = str.replace(new RegExp("\\{\\$" + key + "\\}", "gi"), value) + } + return str +}; +goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) { + goog.exportPath_(publicPath, object, opt_objectToExportTo) +}; +goog.exportProperty = function(object, publicName, symbol) { + object[publicName] = symbol +}; +goog.inherits = function(childCtor, parentCtor) { + function tempCtor() { + } + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor; + childCtor.prototype.constructor = childCtor +}; +goog.base = function(me, opt_methodName, var_args) { + var caller = arguments.callee.caller; + if(caller.superClass_) { + return caller.superClass_.constructor.apply(me, Array.prototype.slice.call(arguments, 1)) + } + var args = Array.prototype.slice.call(arguments, 2); + var foundCaller = false; + for(var ctor = me.constructor;ctor;ctor = ctor.superClass_ && ctor.superClass_.constructor) { + if(ctor.prototype[opt_methodName] === caller) { + foundCaller = true + }else { + if(foundCaller) { + return ctor.prototype[opt_methodName].apply(me, args) + } + } + } + if(me[opt_methodName] === caller) { + return me.constructor.prototype[opt_methodName].apply(me, args) + }else { + throw Error("goog.base called from a method of one name " + "to a method of a different name"); + } +}; +goog.scope = function(fn) { + fn.call(goog.global) +}; +goog.provide("goog.debug.Error"); +goog.debug.Error = function(opt_msg) { + this.stack = (new Error).stack || ""; + if(opt_msg) { + this.message = String(opt_msg) + } +}; +goog.inherits(goog.debug.Error, Error); +goog.debug.Error.prototype.name = "CustomError"; +goog.provide("goog.string"); +goog.provide("goog.string.Unicode"); +goog.string.Unicode = {NBSP:"\u00a0"}; +goog.string.startsWith = function(str, prefix) { + return str.lastIndexOf(prefix, 0) == 0 +}; +goog.string.endsWith = function(str, suffix) { + var l = str.length - suffix.length; + return l >= 0 && str.indexOf(suffix, l) == l +}; +goog.string.caseInsensitiveStartsWith = function(str, prefix) { + return goog.string.caseInsensitiveCompare(prefix, str.substr(0, prefix.length)) == 0 +}; +goog.string.caseInsensitiveEndsWith = function(str, suffix) { + return goog.string.caseInsensitiveCompare(suffix, str.substr(str.length - suffix.length, suffix.length)) == 0 +}; +goog.string.subs = function(str, var_args) { + for(var i = 1;i < arguments.length;i++) { + var replacement = String(arguments[i]).replace(/\$/g, "$$$$"); + str = str.replace(/\%s/, replacement) + } + return str +}; +goog.string.collapseWhitespace = function(str) { + return str.replace(/[\s\xa0]+/g, " ").replace(/^\s+|\s+$/g, "") +}; +goog.string.isEmpty = function(str) { + return/^[\s\xa0]*$/.test(str) +}; +goog.string.isEmptySafe = function(str) { + return goog.string.isEmpty(goog.string.makeSafe(str)) +}; +goog.string.isBreakingWhitespace = function(str) { + return!/[^\t\n\r ]/.test(str) +}; +goog.string.isAlpha = function(str) { + return!/[^a-zA-Z]/.test(str) +}; +goog.string.isNumeric = function(str) { + return!/[^0-9]/.test(str) +}; +goog.string.isAlphaNumeric = function(str) { + return!/[^a-zA-Z0-9]/.test(str) +}; +goog.string.isSpace = function(ch) { + return ch == " " +}; +goog.string.isUnicodeChar = function(ch) { + return ch.length == 1 && ch >= " " && ch <= "~" || ch >= "\u0080" && ch <= "\ufffd" +}; +goog.string.stripNewlines = function(str) { + return str.replace(/(\r\n|\r|\n)+/g, " ") +}; +goog.string.canonicalizeNewlines = function(str) { + return str.replace(/(\r\n|\r|\n)/g, "\n") +}; +goog.string.normalizeWhitespace = function(str) { + return str.replace(/\xa0|\s/g, " ") +}; +goog.string.normalizeSpaces = function(str) { + return str.replace(/\xa0|[ \t]+/g, " ") +}; +goog.string.trim = function(str) { + return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "") +}; +goog.string.trimLeft = function(str) { + return str.replace(/^[\s\xa0]+/, "") +}; +goog.string.trimRight = function(str) { + return str.replace(/[\s\xa0]+$/, "") +}; +goog.string.caseInsensitiveCompare = function(str1, str2) { + var test1 = String(str1).toLowerCase(); + var test2 = String(str2).toLowerCase(); + if(test1 < test2) { + return-1 + }else { + if(test1 == test2) { + return 0 + }else { + return 1 + } + } +}; +goog.string.numerateCompareRegExp_ = /(\.\d+)|(\d+)|(\D+)/g; +goog.string.numerateCompare = function(str1, str2) { + if(str1 == str2) { + return 0 + } + if(!str1) { + return-1 + } + if(!str2) { + return 1 + } + var tokens1 = str1.toLowerCase().match(goog.string.numerateCompareRegExp_); + var tokens2 = str2.toLowerCase().match(goog.string.numerateCompareRegExp_); + var count = Math.min(tokens1.length, tokens2.length); + for(var i = 0;i < count;i++) { + var a = tokens1[i]; + var b = tokens2[i]; + if(a != b) { + var num1 = parseInt(a, 10); + if(!isNaN(num1)) { + var num2 = parseInt(b, 10); + if(!isNaN(num2) && num1 - num2) { + return num1 - num2 + } + } + return a < b ? -1 : 1 + } + } + if(tokens1.length != tokens2.length) { + return tokens1.length - tokens2.length + } + return str1 < str2 ? -1 : 1 +}; +goog.string.encodeUriRegExp_ = /^[a-zA-Z0-9\-_.!~*'()]*$/; +goog.string.urlEncode = function(str) { + str = String(str); + if(!goog.string.encodeUriRegExp_.test(str)) { + return encodeURIComponent(str) + } + return str +}; +goog.string.urlDecode = function(str) { + return decodeURIComponent(str.replace(/\+/g, " ")) +}; +goog.string.newLineToBr = function(str, opt_xml) { + return str.replace(/(\r\n|\r|\n)/g, opt_xml ? "<br />" : "<br>") +}; +goog.string.htmlEscape = function(str, opt_isLikelyToContainHtmlChars) { + if(opt_isLikelyToContainHtmlChars) { + return str.replace(goog.string.amperRe_, "&").replace(goog.string.ltRe_, "<").replace(goog.string.gtRe_, ">").replace(goog.string.quotRe_, """) + }else { + if(!goog.string.allRe_.test(str)) { + return str + } + if(str.indexOf("&") != -1) { + str = str.replace(goog.string.amperRe_, "&") + } + if(str.indexOf("<") != -1) { + str = str.replace(goog.string.ltRe_, "<") + } + if(str.indexOf(">") != -1) { + str = str.replace(goog.string.gtRe_, ">") + } + if(str.indexOf('"') != -1) { + str = str.replace(goog.string.quotRe_, """) + } + return str + } +}; +goog.string.amperRe_ = /&/g; +goog.string.ltRe_ = /</g; +goog.string.gtRe_ = />/g; +goog.string.quotRe_ = /\"/g; +goog.string.allRe_ = /[&<>\"]/; +goog.string.unescapeEntities = function(str) { + if(goog.string.contains(str, "&")) { + if("document" in goog.global && !goog.string.contains(str, "<")) { + return goog.string.unescapeEntitiesUsingDom_(str) + }else { + return goog.string.unescapePureXmlEntities_(str) + } + } + return str +}; +goog.string.unescapeEntitiesUsingDom_ = function(str) { + var el = goog.global["document"]["createElement"]("a"); + el["innerHTML"] = str; + if(el[goog.string.NORMALIZE_FN_]) { + el[goog.string.NORMALIZE_FN_]() + } + str = el["firstChild"]["nodeValue"]; + el["innerHTML"] = ""; + return str +}; +goog.string.unescapePureXmlEntities_ = function(str) { + return str.replace(/&([^;]+);/g, function(s, entity) { + switch(entity) { + case "amp": + return"&"; + case "lt": + return"<"; + case "gt": + return">"; + case "quot": + return'"'; + default: + if(entity.charAt(0) == "#") { + var n = Number("0" + entity.substr(1)); + if(!isNaN(n)) { + return String.fromCharCode(n) + } + } + return s + } + }) +}; +goog.string.NORMALIZE_FN_ = "normalize"; +goog.string.whitespaceEscape = function(str, opt_xml) { + return goog.string.newLineToBr(str.replace(/ /g, "  "), opt_xml) +}; +goog.string.stripQuotes = function(str, quoteChars) { + var length = quoteChars.length; + for(var i = 0;i < length;i++) { + var quoteChar = length == 1 ? quoteChars : quoteChars.charAt(i); + if(str.charAt(0) == quoteChar && str.charAt(str.length - 1) == quoteChar) { + return str.substring(1, str.length - 1) + } + } + return str +}; +goog.string.truncate = function(str, chars, opt_protectEscapedCharacters) { + if(opt_protectEscapedCharacters) { + str = goog.string.unescapeEntities(str) + } + if(str.length > chars) { + str = str.substring(0, chars - 3) + "..." + } + if(opt_protectEscapedCharacters) { + str = goog.string.htmlEscape(str) + } + return str +}; +goog.string.truncateMiddle = function(str, chars, opt_protectEscapedCharacters) { + if(opt_protectEscapedCharacters) { + str = goog.string.unescapeEntities(str) + } + if(str.length > chars) { + var half = Math.floor(chars / 2); + var endPos = str.length - half; + half += chars % 2; + str = str.substring(0, half) + "..." + str.substring(endPos) + } + if(opt_protectEscapedCharacters) { + str = goog.string.htmlEscape(str) + } + return str +}; +goog.string.specialEscapeChars_ = {"\u0000":"\\0", "\u0008":"\\b", "\u000c":"\\f", "\n":"\\n", "\r":"\\r", "\t":"\\t", "\u000b":"\\x0B", '"':'\\"', "\\":"\\\\"}; +goog.string.jsEscapeCache_ = {"'":"\\'"}; +goog.string.quote = function(s) { + s = String(s); + if(s.quote) { + return s.quote() + }else { + var sb = ['"']; + for(var i = 0;i < s.length;i++) { + var ch = s.charAt(i); + var cc = ch.charCodeAt(0); + sb[i + 1] = goog.string.specialEscapeChars_[ch] || (cc > 31 && cc < 127 ? ch : goog.string.escapeChar(ch)) + } + sb.push('"'); + return sb.join("") + } +}; +goog.string.escapeString = function(str) { + var sb = []; + for(var i = 0;i < str.length;i++) { + sb[i] = goog.string.escapeChar(str.charAt(i)) + } + return sb.join("") +}; +goog.string.escapeChar = function(c) { + if(c in goog.string.jsEscapeCache_) { + return goog.string.jsEscapeCache_[c] + } + if(c in goog.string.specialEscapeChars_) { + return goog.string.jsEscapeCache_[c] = goog.string.specialEscapeChars_[c] + } + var rv = c; + var cc = c.charCodeAt(0); + if(cc > 31 && cc < 127) { + rv = c + }else { + if(cc < 256) { + rv = "\\x"; + if(cc < 16 || cc > 256) { + rv += "0" + } + }else { + rv = "\\u"; + if(cc < 4096) { + rv += "0" + } + } + rv += cc.toString(16).toUpperCase() + } + return goog.string.jsEscapeCache_[c] = rv +}; +goog.string.toMap = function(s) { + var rv = {}; + for(var i = 0;i < s.length;i++) { + rv[s.charAt(i)] = true + } + return rv +}; +goog.string.contains = function(s, ss) { + return s.indexOf(ss) != -1 +}; +goog.string.removeAt = function(s, index, stringLength) { + var resultStr = s; + if(index >= 0 && index < s.length && stringLength > 0) { + resultStr = s.substr(0, index) + s.substr(index + stringLength, s.length - index - stringLength) + } + return resultStr +}; +goog.string.remove = function(s, ss) { + var re = new RegExp(goog.string.regExpEscape(ss), ""); + return s.replace(re, "") +}; +goog.string.removeAll = function(s, ss) { + var re = new RegExp(goog.string.regExpEscape(ss), "g"); + return s.replace(re, "") +}; +goog.string.regExpEscape = function(s) { + return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, "\\$1").replace(/\x08/g, "\\x08") +}; +goog.string.repeat = function(string, length) { + return(new Array(length + 1)).join(string) +}; +goog.string.padNumber = function(num, length, opt_precision) { + var s = goog.isDef(opt_precision) ? num.toFixed(opt_precision) : String(num); + var index = s.indexOf("."); + if(index == -1) { + index = s.length + } + return goog.string.repeat("0", Math.max(0, length - index)) + s +}; +goog.string.makeSafe = function(obj) { + return obj == null ? "" : String(obj) +}; +goog.string.buildString = function(var_args) { + return Array.prototype.join.call(arguments, "") +}; +goog.string.getRandomString = function() { + return Math.floor(Math.random() * 2147483648).toString(36) + (Math.floor(Math.random() * 2147483648) ^ goog.now()).toString(36) +}; +goog.string.compareVersions = function(version1, version2) { + var order = 0; + var v1Subs = goog.string.trim(String(version1)).split("."); + var v2Subs = goog.string.trim(String(version2)).split("."); + var subCount = Math.max(v1Subs.length, v2Subs.length); + for(var subIdx = 0;order == 0 && subIdx < subCount;subIdx++) { + var v1Sub = v1Subs[subIdx] || ""; + var v2Sub = v2Subs[subIdx] || ""; + var v1CompParser = new RegExp("(\\d*)(\\D*)", "g"); + var v2CompParser = new RegExp("(\\d*)(\\D*)", "g"); + do { + var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""]; + var v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""]; + if(v1Comp[0].length == 0 && v2Comp[0].length == 0) { + break + } + var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10); + var v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10); + order = goog.string.compareElements_(v1CompNum, v2CompNum) || goog.string.compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog.string.compareElements_(v1Comp[2], v2Comp[2]) + }while(order == 0) + } + return order +}; +goog.string.compareElements_ = function(left, right) { + if(left < right) { + return-1 + }else { + if(left > right) { + return 1 + } + } + return 0 +}; +goog.string.HASHCODE_MAX_ = 4294967296; +goog.string.hashCode = function(str) { + var result = 0; + for(var i = 0;i < str.length;++i) { + result = 31 * result + str.charCodeAt(i); + result %= goog.string.HASHCODE_MAX_ + } + return result +}; +goog.string.uniqueStringCounter_ = Math.random() * 2147483648 | 0; +goog.string.createUniqueString = function() { + return"goog_" + goog.string.uniqueStringCounter_++ +}; +goog.string.toNumber = function(str) { + var num = Number(str); + if(num == 0 && goog.string.isEmpty(str)) { + return NaN + } + return num +}; +goog.provide("goog.asserts"); +goog.provide("goog.asserts.AssertionError"); +goog.require("goog.debug.Error"); +goog.require("goog.string"); +goog.asserts.ENABLE_ASSERTS = goog.DEBUG; +goog.asserts.AssertionError = function(messagePattern, messageArgs) { + messageArgs.unshift(messagePattern); + goog.debug.Error.call(this, goog.string.subs.apply(null, messageArgs)); + messageArgs.shift(); + this.messagePattern = messagePattern +}; +goog.inherits(goog.asserts.AssertionError, goog.debug.Error); +goog.asserts.AssertionError.prototype.name = "AssertionError"; +goog.asserts.doAssertFailure_ = function(defaultMessage, defaultArgs, givenMessage, givenArgs) { + var message = "Assertion failed"; + if(givenMessage) { + message += ": " + givenMessage; + var args = givenArgs + }else { + if(defaultMessage) { + message += ": " + defaultMessage; + args = defaultArgs + } + } + throw new goog.asserts.AssertionError("" + message, args || []); +}; +goog.asserts.assert = function(condition, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !condition) { + goog.asserts.doAssertFailure_("", null, opt_message, Array.prototype.slice.call(arguments, 2)) + } + return condition +}; +goog.asserts.fail = function(opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS) { + throw new goog.asserts.AssertionError("Failure" + (opt_message ? ": " + opt_message : ""), Array.prototype.slice.call(arguments, 1)); + } +}; +goog.asserts.assertNumber = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isNumber(value)) { + goog.asserts.doAssertFailure_("Expected number but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertString = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isString(value)) { + goog.asserts.doAssertFailure_("Expected string but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertFunction = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isFunction(value)) { + goog.asserts.doAssertFailure_("Expected function but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertObject = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isObject(value)) { + goog.asserts.doAssertFailure_("Expected object but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertArray = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isArray(value)) { + goog.asserts.doAssertFailure_("Expected array but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertBoolean = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isBoolean(value)) { + goog.asserts.doAssertFailure_("Expected boolean but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertInstanceof = function(value, type, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !(value instanceof type)) { + goog.asserts.doAssertFailure_("instanceof check failed.", null, opt_message, Array.prototype.slice.call(arguments, 3)) + } +}; +goog.provide("goog.array"); +goog.require("goog.asserts"); +goog.array.ArrayLike; +goog.array.peek = function(array) { + return array[array.length - 1] +}; +goog.array.ARRAY_PROTOTYPE_ = Array.prototype; +goog.array.indexOf = goog.array.ARRAY_PROTOTYPE_.indexOf ? function(arr, obj, opt_fromIndex) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.indexOf.call(arr, obj, opt_fromIndex) +} : function(arr, obj, opt_fromIndex) { + var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex; + if(goog.isString(arr)) { + if(!goog.isString(obj) || obj.length != 1) { + return-1 + } + return arr.indexOf(obj, fromIndex) + } + for(var i = fromIndex;i < arr.length;i++) { + if(i in arr && arr[i] === obj) { + return i + } + } + return-1 +}; +goog.array.lastIndexOf = goog.array.ARRAY_PROTOTYPE_.lastIndexOf ? function(arr, obj, opt_fromIndex) { + goog.asserts.assert(arr.length != null); + var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex; + return goog.array.ARRAY_PROTOTYPE_.lastIndexOf.call(arr, obj, fromIndex) +} : function(arr, obj, opt_fromIndex) { + var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex; + if(fromIndex < 0) { + fromIndex = Math.max(0, arr.length + fromIndex) + } + if(goog.isString(arr)) { + if(!goog.isString(obj) || obj.length != 1) { + return-1 + } + return arr.lastIndexOf(obj, fromIndex) + } + for(var i = fromIndex;i >= 0;i--) { + if(i in arr && arr[i] === obj) { + return i + } + } + return-1 +}; +goog.array.forEach = goog.array.ARRAY_PROTOTYPE_.forEach ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + goog.array.ARRAY_PROTOTYPE_.forEach.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2) { + f.call(opt_obj, arr2[i], i, arr) + } + } +}; +goog.array.forEachRight = function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = l - 1;i >= 0;--i) { + if(i in arr2) { + f.call(opt_obj, arr2[i], i, arr) + } + } +}; +goog.array.filter = goog.array.ARRAY_PROTOTYPE_.filter ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.filter.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var res = []; + var resLength = 0; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2) { + var val = arr2[i]; + if(f.call(opt_obj, val, i, arr)) { + res[resLength++] = val + } + } + } + return res +}; +goog.array.map = goog.array.ARRAY_PROTOTYPE_.map ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.map.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var res = new Array(l); + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2) { + res[i] = f.call(opt_obj, arr2[i], i, arr) + } + } + return res +}; +goog.array.reduce = function(arr, f, val, opt_obj) { + if(arr.reduce) { + if(opt_obj) { + return arr.reduce(goog.bind(f, opt_obj), val) + }else { + return arr.reduce(f, val) + } + } + var rval = val; + goog.array.forEach(arr, function(val, index) { + rval = f.call(opt_obj, rval, val, index, arr) + }); + return rval +}; +goog.array.reduceRight = function(arr, f, val, opt_obj) { + if(arr.reduceRight) { + if(opt_obj) { + return arr.reduceRight(goog.bind(f, opt_obj), val) + }else { + return arr.reduceRight(f, val) + } + } + var rval = val; + goog.array.forEachRight(arr, function(val, index) { + rval = f.call(opt_obj, rval, val, index, arr) + }); + return rval +}; +goog.array.some = goog.array.ARRAY_PROTOTYPE_.some ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.some.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return true + } + } + return false +}; +goog.array.every = goog.array.ARRAY_PROTOTYPE_.every ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.every.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr)) { + return false + } + } + return true +}; +goog.array.find = function(arr, f, opt_obj) { + var i = goog.array.findIndex(arr, f, opt_obj); + return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i] +}; +goog.array.findIndex = function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return i + } + } + return-1 +}; +goog.array.findRight = function(arr, f, opt_obj) { + var i = goog.array.findIndexRight(arr, f, opt_obj); + return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i] +}; +goog.array.findIndexRight = function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = l - 1;i >= 0;i--) { + if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return i + } + } + return-1 +}; +goog.array.contains = function(arr, obj) { + return goog.array.indexOf(arr, obj) >= 0 +}; +goog.array.isEmpty = function(arr) { + return arr.length == 0 +}; +goog.array.clear = function(arr) { + if(!goog.isArray(arr)) { + for(var i = arr.length - 1;i >= 0;i--) { + delete arr[i] + } + } + arr.length = 0 +}; +goog.array.insert = function(arr, obj) { + if(!goog.array.contains(arr, obj)) { + arr.push(obj) + } +}; +goog.array.insertAt = function(arr, obj, opt_i) { + goog.array.splice(arr, opt_i, 0, obj) +}; +goog.array.insertArrayAt = function(arr, elementsToAdd, opt_i) { + goog.partial(goog.array.splice, arr, opt_i, 0).apply(null, elementsToAdd) +}; +goog.array.insertBefore = function(arr, obj, opt_obj2) { + var i; + if(arguments.length == 2 || (i = goog.array.indexOf(arr, opt_obj2)) < 0) { + arr.push(obj) + }else { + goog.array.insertAt(arr, obj, i) + } +}; +goog.array.remove = function(arr, obj) { + var i = goog.array.indexOf(arr, obj); + var rv; + if(rv = i >= 0) { + goog.array.removeAt(arr, i) + } + return rv +}; +goog.array.removeAt = function(arr, i) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.splice.call(arr, i, 1).length == 1 +}; +goog.array.removeIf = function(arr, f, opt_obj) { + var i = goog.array.findIndex(arr, f, opt_obj); + if(i >= 0) { + goog.array.removeAt(arr, i); + return true + } + return false +}; +goog.array.concat = function(var_args) { + return goog.array.ARRAY_PROTOTYPE_.concat.apply(goog.array.ARRAY_PROTOTYPE_, arguments) +}; +goog.array.clone = function(arr) { + if(goog.isArray(arr)) { + return goog.array.concat(arr) + }else { + var rv = []; + for(var i = 0, len = arr.length;i < len;i++) { + rv[i] = arr[i] + } + return rv + } +}; +goog.array.toArray = function(object) { + if(goog.isArray(object)) { + return goog.array.concat(object) + } + return goog.array.clone(object) +}; +goog.array.extend = function(arr1, var_args) { + for(var i = 1;i < arguments.length;i++) { + var arr2 = arguments[i]; + var isArrayLike; + if(goog.isArray(arr2) || (isArrayLike = goog.isArrayLike(arr2)) && arr2.hasOwnProperty("callee")) { + arr1.push.apply(arr1, arr2) + }else { + if(isArrayLike) { + var len1 = arr1.length; + var len2 = arr2.length; + for(var j = 0;j < len2;j++) { + arr1[len1 + j] = arr2[j] + } + }else { + arr1.push(arr2) + } + } + } +}; +goog.array.splice = function(arr, index, howMany, var_args) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.splice.apply(arr, goog.array.slice(arguments, 1)) +}; +goog.array.slice = function(arr, start, opt_end) { + goog.asserts.assert(arr.length != null); + if(arguments.length <= 2) { + return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start) + }else { + return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start, opt_end) + } +}; +goog.array.removeDuplicates = function(arr, opt_rv) { + var rv = opt_rv || arr; + var seen = {}, cursorInsert = 0, cursorRead = 0; + while(cursorRead < arr.length) { + var current = arr[cursorRead++]; + var uid = goog.isObject(current) ? goog.getUid(current) : current; + if(!Object.prototype.hasOwnProperty.call(seen, uid)) { + seen[uid] = true; + rv[cursorInsert++] = current + } + } + rv.length = cursorInsert +}; +goog.array.binarySearch = function(arr, target, opt_compareFn) { + return goog.array.binarySearch_(arr, opt_compareFn || goog.array.defaultCompare, false, target) +}; +goog.array.binarySelect = function(arr, evaluator, opt_obj) { + return goog.array.binarySearch_(arr, evaluator, true, undefined, opt_obj) +}; +goog.array.binarySearch_ = function(arr, compareFn, isEvaluator, opt_target, opt_selfObj) { + var left = 0; + var right = arr.length; + var found; + while(left < right) { + var middle = left + right >> 1; + var compareResult; + if(isEvaluator) { + compareResult = compareFn.call(opt_selfObj, arr[middle], middle, arr) + }else { + compareResult = compareFn(opt_target, arr[middle]) + } + if(compareResult > 0) { + left = middle + 1 + }else { + right = middle; + found = !compareResult + } + } + return found ? left : ~left +}; +goog.array.sort = function(arr, opt_compareFn) { + goog.asserts.assert(arr.length != null); + goog.array.ARRAY_PROTOTYPE_.sort.call(arr, opt_compareFn || goog.array.defaultCompare) +}; +goog.array.stableSort = function(arr, opt_compareFn) { + for(var i = 0;i < arr.length;i++) { + arr[i] = {index:i, value:arr[i]} + } + var valueCompareFn = opt_compareFn || goog.array.defaultCompare; + function stableCompareFn(obj1, obj2) { + return valueCompareFn(obj1.value, obj2.value) || obj1.index - obj2.index + } + goog.array.sort(arr, stableCompareFn); + for(var i = 0;i < arr.length;i++) { + arr[i] = arr[i].value + } +}; +goog.array.sortObjectsByKey = function(arr, key, opt_compareFn) { + var compare = opt_compareFn || goog.array.defaultCompare; + goog.array.sort(arr, function(a, b) { + return compare(a[key], b[key]) + }) +}; +goog.array.equals = function(arr1, arr2, opt_equalsFn) { + if(!goog.isArrayLike(arr1) || !goog.isArrayLike(arr2) || arr1.length != arr2.length) { + return false + } + var l = arr1.length; + var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality; + for(var i = 0;i < l;i++) { + if(!equalsFn(arr1[i], arr2[i])) { + return false + } + } + return true +}; +goog.array.compare = function(arr1, arr2, opt_equalsFn) { + return goog.array.equals(arr1, arr2, opt_equalsFn) +}; +goog.array.defaultCompare = function(a, b) { + return a > b ? 1 : a < b ? -1 : 0 +}; +goog.array.defaultCompareEquality = function(a, b) { + return a === b +}; +goog.array.binaryInsert = function(array, value, opt_compareFn) { + var index = goog.array.binarySearch(array, value, opt_compareFn); + if(index < 0) { + goog.array.insertAt(array, value, -(index + 1)); + return true + } + return false +}; +goog.array.binaryRemove = function(array, value, opt_compareFn) { + var index = goog.array.binarySearch(array, value, opt_compareFn); + return index >= 0 ? goog.array.removeAt(array, index) : false +}; +goog.array.bucket = function(array, sorter) { + var buckets = {}; + for(var i = 0;i < array.length;i++) { + var value = array[i]; + var key = sorter(value, i, array); + if(goog.isDef(key)) { + var bucket = buckets[key] || (buckets[key] = []); + bucket.push(value) + } + } + return buckets +}; +goog.array.repeat = function(value, n) { + var array = []; + for(var i = 0;i < n;i++) { + array[i] = value + } + return array +}; +goog.array.flatten = function(var_args) { + var result = []; + for(var i = 0;i < arguments.length;i++) { + var element = arguments[i]; + if(goog.isArray(element)) { + result.push.apply(result, goog.array.flatten.apply(null, element)) + }else { + result.push(element) + } + } + return result +}; +goog.array.rotate = function(array, n) { + goog.asserts.assert(array.length != null); + if(array.length) { + n %= array.length; + if(n > 0) { + goog.array.ARRAY_PROTOTYPE_.unshift.apply(array, array.splice(-n, n)) + }else { + if(n < 0) { + goog.array.ARRAY_PROTOTYPE_.push.apply(array, array.splice(0, -n)) + } + } + } + return array +}; +goog.array.zip = function(var_args) { + if(!arguments.length) { + return[] + } + var result = []; + for(var i = 0;true;i++) { + var value = []; + for(var j = 0;j < arguments.length;j++) { + var arr = arguments[j]; + if(i >= arr.length) { + return result + } + value.push(arr[i]) + } + result.push(value) + } +}; +goog.provide("goog.userAgent"); +goog.require("goog.string"); +goog.userAgent.ASSUME_IE = false; +goog.userAgent.ASSUME_GECKO = false; +goog.userAgent.ASSUME_WEBKIT = false; +goog.userAgent.ASSUME_MOBILE_WEBKIT = false; +goog.userAgent.ASSUME_OPERA = false; +goog.userAgent.BROWSER_KNOWN_ = goog.userAgent.ASSUME_IE || goog.userAgent.ASSUME_GECKO || goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_OPERA; +goog.userAgent.getUserAgentString = function() { + return goog.global["navigator"] ? goog.global["navigator"].userAgent : null +}; +goog.userAgent.getNavigator = function() { + return goog.global["navigator"] +}; +goog.userAgent.init_ = function() { + goog.userAgent.detectedOpera_ = false; + goog.userAgent.detectedIe_ = false; + goog.userAgent.detectedWebkit_ = false; + goog.userAgent.detectedMobile_ = false; + goog.userAgent.detectedGecko_ = false; + var ua; + if(!goog.userAgent.BROWSER_KNOWN_ && (ua = goog.userAgent.getUserAgentString())) { + var navigator = goog.userAgent.getNavigator(); + goog.userAgent.detectedOpera_ = ua.indexOf("Opera") == 0; + goog.userAgent.detectedIe_ = !goog.userAgent.detectedOpera_ && ua.indexOf("MSIE") != -1; + goog.userAgent.detectedWebkit_ = !goog.userAgent.detectedOpera_ && ua.indexOf("WebKit") != -1; + goog.userAgent.detectedMobile_ = goog.userAgent.detectedWebkit_ && ua.indexOf("Mobile") != -1; + goog.userAgent.detectedGecko_ = !goog.userAgent.detectedOpera_ && !goog.userAgent.detectedWebkit_ && navigator.product == "Gecko" + } +}; +if(!goog.userAgent.BROWSER_KNOWN_) { + goog.userAgent.init_() +} +goog.userAgent.OPERA = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_OPERA : goog.userAgent.detectedOpera_; +goog.userAgent.IE = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_IE : goog.userAgent.detectedIe_; +goog.userAgent.GECKO = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_GECKO : goog.userAgent.detectedGecko_; +goog.userAgent.WEBKIT = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_MOBILE_WEBKIT : goog.userAgent.detectedWebkit_; +goog.userAgent.MOBILE = goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.detectedMobile_; +goog.userAgent.SAFARI = goog.userAgent.WEBKIT; +goog.userAgent.determinePlatform_ = function() { + var navigator = goog.userAgent.getNavigator(); + return navigator && navigator.platform || "" +}; +goog.userAgent.PLATFORM = goog.userAgent.determinePlatform_(); +goog.userAgent.ASSUME_MAC = false; +goog.userAgent.ASSUME_WINDOWS = false; +goog.userAgent.ASSUME_LINUX = false; +goog.userAgent.ASSUME_X11 = false; +goog.userAgent.PLATFORM_KNOWN_ = goog.userAgent.ASSUME_MAC || goog.userAgent.ASSUME_WINDOWS || goog.userAgent.ASSUME_LINUX || goog.userAgent.ASSUME_X11; +goog.userAgent.initPlatform_ = function() { + goog.userAgent.detectedMac_ = goog.string.contains(goog.userAgent.PLATFORM, "Mac"); + goog.userAgent.detectedWindows_ = goog.string.contains(goog.userAgent.PLATFORM, "Win"); + goog.userAgent.detectedLinux_ = goog.string.contains(goog.userAgent.PLATFORM, "Linux"); + goog.userAgent.detectedX11_ = !!goog.userAgent.getNavigator() && goog.string.contains(goog.userAgent.getNavigator()["appVersion"] || "", "X11") +}; +if(!goog.userAgent.PLATFORM_KNOWN_) { + goog.userAgent.initPlatform_() +} +goog.userAgent.MAC = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_MAC : goog.userAgent.detectedMac_; +goog.userAgent.WINDOWS = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_WINDOWS : goog.userAgent.detectedWindows_; +goog.userAgent.LINUX = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_LINUX : goog.userAgent.detectedLinux_; +goog.userAgent.X11 = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_X11 : goog.userAgent.detectedX11_; +goog.userAgent.determineVersion_ = function() { + var version = "", re; + if(goog.userAgent.OPERA && goog.global["opera"]) { + var operaVersion = goog.global["opera"].version; + version = typeof operaVersion == "function" ? operaVersion() : operaVersion + }else { + if(goog.userAgent.GECKO) { + re = /rv\:([^\);]+)(\)|;)/ + }else { + if(goog.userAgent.IE) { + re = /MSIE\s+([^\);]+)(\)|;)/ + }else { + if(goog.userAgent.WEBKIT) { + re = /WebKit\/(\S+)/ + } + } + } + if(re) { + var arr = re.exec(goog.userAgent.getUserAgentString()); + version = arr ? arr[1] : "" + } + } + if(goog.userAgent.IE) { + var docMode = goog.userAgent.getDocumentMode_(); + if(docMode > parseFloat(version)) { + return String(docMode) + } + } + return version +}; +goog.userAgent.getDocumentMode_ = function() { + var doc = goog.global["document"]; + return doc ? doc["documentMode"] : undefined +}; +goog.userAgent.VERSION = goog.userAgent.determineVersion_(); +goog.userAgent.compare = function(v1, v2) { + return goog.string.compareVersions(v1, v2) +}; +goog.userAgent.isVersionCache_ = {}; +goog.userAgent.isVersion = function(version) { + return goog.userAgent.isVersionCache_[version] || (goog.userAgent.isVersionCache_[version] = goog.string.compareVersions(goog.userAgent.VERSION, version) >= 0) +}; +goog.provide("goog.dom.BrowserFeature"); +goog.require("goog.userAgent"); +goog.dom.BrowserFeature = { + CAN_ADD_NAME_OR_TYPE_ATTRIBUTES: !goog.userAgent.IE || goog.userAgent.isVersion("9"), + CAN_USE_INNER_TEXT: goog.userAgent.IE && !goog.userAgent.isVersion("9"), + INNER_HTML_NEEDS_SCOPED_ELEMENT: goog.userAgent.IE +}; +goog.provide("goog.dom.TagName"); +goog.dom.TagName = {A:"A", ABBR:"ABBR", ACRONYM:"ACRONYM", ADDRESS:"ADDRESS", APPLET:"APPLET", AREA:"AREA", B:"B", BASE:"BASE", BASEFONT:"BASEFONT", BDO:"BDO", BIG:"BIG", BLOCKQUOTE:"BLOCKQUOTE", BODY:"BODY", BR:"BR", BUTTON:"BUTTON", CANVAS:"CANVAS", CAPTION:"CAPTION", CENTER:"CENTER", CITE:"CITE", CODE:"CODE", COL:"COL", COLGROUP:"COLGROUP", DD:"DD", DEL:"DEL", DFN:"DFN", DIR:"DIR", DIV:"DIV", DL:"DL", DT:"DT", EM:"EM", FIELDSET:"FIELDSET", FONT:"FONT", FORM:"FORM", FRAME:"FRAME", FRAMESET:"FRAMESET", +H1:"H1", H2:"H2", H3:"H3", H4:"H4", H5:"H5", H6:"H6", HEAD:"HEAD", HR:"HR", HTML:"HTML", I:"I", IFRAME:"IFRAME", IMG:"IMG", INPUT:"INPUT", INS:"INS", ISINDEX:"ISINDEX", KBD:"KBD", LABEL:"LABEL", LEGEND:"LEGEND", LI:"LI", LINK:"LINK", MAP:"MAP", MENU:"MENU", META:"META", NOFRAMES:"NOFRAMES", NOSCRIPT:"NOSCRIPT", OBJECT:"OBJECT", OL:"OL", OPTGROUP:"OPTGROUP", OPTION:"OPTION", P:"P", PARAM:"PARAM", PRE:"PRE", Q:"Q", S:"S", SAMP:"SAMP", SCRIPT:"SCRIPT", SELECT:"SELECT", SMALL:"SMALL", SPAN:"SPAN", STRIKE:"STRIKE", +STRONG:"STRONG", STYLE:"STYLE", SUB:"SUB", SUP:"SUP", TABLE:"TABLE", TBODY:"TBODY", TD:"TD", TEXTAREA:"TEXTAREA", TFOOT:"TFOOT", TH:"TH", THEAD:"THEAD", TITLE:"TITLE", TR:"TR", TT:"TT", U:"U", UL:"UL", VAR:"VAR"}; +goog.provide("goog.dom.classes"); +goog.require("goog.array"); +goog.dom.classes.set = function(element, className) { + element.className = className +}; +goog.dom.classes.get = function(element) { + var className = element.className; + return className && typeof className.split == "function" ? className.split(/\s+/) : [] +}; +goog.dom.classes.add = function(element, var_args) { + var classes = goog.dom.classes.get(element); + var args = goog.array.slice(arguments, 1); + var b = goog.dom.classes.add_(classes, args); + element.className = classes.join(" "); + return b +}; +goog.dom.classes.remove = function(element, var_args) { + var classes = goog.dom.classes.get(element); + var args = goog.array.slice(arguments, 1); + var b = goog.dom.classes.remove_(classes, args); + element.className = classes.join(" "); + return b +}; +goog.dom.classes.add_ = function(classes, args) { + var rv = 0; + for(var i = 0;i < args.length;i++) { + if(!goog.array.contains(classes, args[i])) { + classes.push(args[i]); + rv++ + } + } + return rv == args.length +}; +goog.dom.classes.remove_ = function(classes, args) { + var rv = 0; + for(var i = 0;i < classes.length;i++) { + if(goog.array.contains(args, classes[i])) { + goog.array.splice(classes, i--, 1); + rv++ + } + } + return rv == args.length +}; +goog.dom.classes.swap = function(element, fromClass, toClass) { + var classes = goog.dom.classes.get(element); + var removed = false; + for(var i = 0;i < classes.length;i++) { + if(classes[i] == fromClass) { + goog.array.splice(classes, i--, 1); + removed = true + } + } + if(removed) { + classes.push(toClass); + element.className = classes.join(" ") + } + return removed +}; +goog.dom.classes.addRemove = function(element, classesToRemove, classesToAdd) { + var classes = goog.dom.classes.get(element); + if(goog.isString(classesToRemove)) { + goog.array.remove(classes, classesToRemove) + }else { + if(goog.isArray(classesToRemove)) { + goog.dom.classes.remove_(classes, classesToRemove) + } + } + if(goog.isString(classesToAdd) && !goog.array.contains(classes, classesToAdd)) { + classes.push(classesToAdd) + }else { + if(goog.isArray(classesToAdd)) { + goog.dom.classes.add_(classes, classesToAdd) + } + } + element.className = classes.join(" ") +}; +goog.dom.classes.has = function(element, className) { + return goog.array.contains(goog.dom.classes.get(element), className) +}; +goog.dom.classes.enable = function(element, className, enabled) { + if(enabled) { + goog.dom.classes.add(element, className) + }else { + goog.dom.classes.remove(element, className) + } +}; +goog.dom.classes.toggle = function(element, className) { + var add = !goog.dom.classes.has(element, className); + goog.dom.classes.enable(element, className, add); + return add +}; +goog.provide("goog.math.Coordinate"); +goog.math.Coordinate = function(opt_x, opt_y) { + this.x = goog.isDef(opt_x) ? opt_x : 0; + this.y = goog.isDef(opt_y) ? opt_y : 0 +}; +goog.math.Coordinate.prototype.clone = function() { + return new goog.math.Coordinate(this.x, this.y) +}; +if(goog.DEBUG) { + goog.math.Coordinate.prototype.toString = function() { + return"(" + this.x + ", " + this.y + ")" + } +} +goog.math.Coordinate.equals = function(a, b) { + if(a == b) { + return true + } + if(!a || !b) { + return false + } + return a.x == b.x && a.y == b.y +}; +goog.math.Coordinate.distance = function(a, b) { + var dx = a.x - b.x; + var dy = a.y - b.y; + return Math.sqrt(dx * dx + dy * dy) +}; +goog.math.Coordinate.squaredDistance = function(a, b) { + var dx = a.x - b.x; + var dy = a.y - b.y; + return dx * dx + dy * dy +}; +goog.math.Coordinate.difference = function(a, b) { + return new goog.math.Coordinate(a.x - b.x, a.y - b.y) +}; +goog.math.Coordinate.sum = function(a, b) { + return new goog.math.Coordinate(a.x + b.x, a.y + b.y) +}; +goog.provide("goog.math.Size"); +goog.math.Size = function(width, height) { + this.width = width; + this.height = height +}; +goog.math.Size.equals = function(a, b) { + if(a == b) { + return true + } + if(!a || !b) { + return false + } + return a.width == b.width && a.height == b.height +}; +goog.math.Size.prototype.clone = function() { + return new goog.math.Size(this.width, this.height) +}; +if(goog.DEBUG) { + goog.math.Size.prototype.toString = function() { + return"(" + this.width + " x " + this.height + ")" + } +} +goog.math.Size.prototype.getLongest = function() { + return Math.max(this.width, this.height) +}; +goog.math.Size.prototype.getShortest = function() { + return Math.min(this.width, this.height) +}; +goog.math.Size.prototype.area = function() { + return this.width * this.height +}; +goog.math.Size.prototype.perimeter = function() { + return(this.width + this.height) * 2 +}; +goog.math.Size.prototype.aspectRatio = function() { + return this.width / this.height +}; +goog.math.Size.prototype.isEmpty = function() { + return!this.area() +}; +goog.math.Size.prototype.ceil = function() { + this.width = Math.ceil(this.width); + this.height = Math.ceil(this.height); + return this +}; +goog.math.Size.prototype.fitsInside = function(target) { + return this.width <= target.width && this.height <= target.height +}; +goog.math.Size.prototype.floor = function() { + this.width = Math.floor(this.width); + this.height = Math.floor(this.height); + return this +}; +goog.math.Size.prototype.round = function() { + this.width = Math.round(this.width); + this.height = Math.round(this.height); + return this +}; +goog.math.Size.prototype.scale = function(s) { + this.width *= s; + this.height *= s; + return this +}; +goog.math.Size.prototype.scaleToFit = function(target) { + var s = this.aspectRatio() > target.aspectRatio() ? target.width / this.width : target.height / this.height; + return this.scale(s) +}; +goog.provide("goog.object"); +goog.object.forEach = function(obj, f, opt_obj) { + for(var key in obj) { + f.call(opt_obj, obj[key], key, obj) + } +}; +goog.object.filter = function(obj, f, opt_obj) { + var res = {}; + for(var key in obj) { + if(f.call(opt_obj, obj[key], key, obj)) { + res[key] = obj[key] + } + } + return res +}; +goog.object.map = function(obj, f, opt_obj) { + var res = {}; + for(var key in obj) { + res[key] = f.call(opt_obj, obj[key], key, obj) + } + return res +}; +goog.object.some = function(obj, f, opt_obj) { + for(var key in obj) { + if(f.call(opt_obj, obj[key], key, obj)) { + return true + } + } + return false +}; +goog.object.every = function(obj, f, opt_obj) { + for(var key in obj) { + if(!f.call(opt_obj, obj[key], key, obj)) { + return false + } + } + return true +}; +goog.object.getCount = function(obj) { + var rv = 0; + for(var key in obj) { + rv++ + } + return rv +}; +goog.object.getAnyKey = function(obj) { + for(var key in obj) { + return key + } +}; +goog.object.getAnyValue = function(obj) { + for(var key in obj) { + return obj[key] + } +}; +goog.object.contains = function(obj, val) { + return goog.object.containsValue(obj, val) +}; +goog.object.getValues = function(obj) { + var res = []; + var i = 0; + for(var key in obj) { + res[i++] = obj[key] + } + return res +}; +goog.object.getKeys = function(obj) { + var res = []; + var i = 0; + for(var key in obj) { + res[i++] = key + } + return res +}; +goog.object.containsKey = function(obj, key) { + return key in obj +}; +goog.object.containsValue = function(obj, val) { + for(var key in obj) { + if(obj[key] == val) { + return true + } + } + return false +}; +goog.object.findKey = function(obj, f, opt_this) { + for(var key in obj) { + if(f.call(opt_this, obj[key], key, obj)) { + return key + } + } + return undefined +}; +goog.object.findValue = function(obj, f, opt_this) { + var key = goog.object.findKey(obj, f, opt_this); + return key && obj[key] +}; +goog.object.isEmpty = function(obj) { + for(var key in obj) { + return false + } + return true +}; +goog.object.clear = function(obj) { + var keys = goog.object.getKeys(obj); + for(var i = keys.length - 1;i >= 0;i--) { + goog.object.remove(obj, keys[i]) + } +}; +goog.object.remove = function(obj, key) { + var rv; + if(rv = key in obj) { + delete obj[key] + } + return rv +}; +goog.object.add = function(obj, key, val) { + if(key in obj) { + throw Error('The object already contains the key "' + key + '"'); + } + goog.object.set(obj, key, val) +}; +goog.object.get = function(obj, key, opt_val) { + if(key in obj) { + return obj[key] + } + return opt_val +}; +goog.object.set = function(obj, key, value) { + obj[key] = value +}; +goog.object.setIfUndefined = function(obj, key, value) { + return key in obj ? obj[key] : obj[key] = value +}; +goog.object.clone = function(obj) { + var res = {}; + for(var key in obj) { + res[key] = obj[key] + } + return res +}; +goog.object.transpose = function(obj) { + var transposed = {}; + for(var key in obj) { + transposed[obj[key]] = key + } + return transposed +}; +goog.object.PROTOTYPE_FIELDS_ = ["constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf"]; +goog.object.extend = function(target, var_args) { + var key, source; + for(var i = 1;i < arguments.length;i++) { + source = arguments[i]; + for(key in source) { + target[key] = source[key] + } + for(var j = 0;j < goog.object.PROTOTYPE_FIELDS_.length;j++) { + key = goog.object.PROTOTYPE_FIELDS_[j]; + if(Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key] + } + } + } +}; +goog.object.create = function(var_args) { + var argLength = arguments.length; + if(argLength == 1 && goog.isArray(arguments[0])) { + return goog.object.create.apply(null, arguments[0]) + } + if(argLength % 2) { + throw Error("Uneven number of arguments"); + } + var rv = {}; + for(var i = 0;i < argLength;i += 2) { + rv[arguments[i]] = arguments[i + 1] + } + return rv +}; +goog.object.createSet = function(var_args) { + var argLength = arguments.length; + if(argLength == 1 && goog.isArray(arguments[0])) { + return goog.object.createSet.apply(null, arguments[0]) + } + var rv = {}; + for(var i = 0;i < argLength;i++) { + rv[arguments[i]] = true + } + return rv +}; +goog.provide("goog.dom"); +goog.provide("goog.dom.DomHelper"); +goog.provide("goog.dom.NodeType"); +goog.require("goog.array"); +goog.require("goog.dom.BrowserFeature"); +goog.require("goog.dom.TagName"); +goog.require("goog.dom.classes"); +goog.require("goog.math.Coordinate"); +goog.require("goog.math.Size"); +goog.require("goog.object"); +goog.require("goog.string"); +goog.require("goog.userAgent"); +goog.dom.ASSUME_QUIRKS_MODE = false; +goog.dom.ASSUME_STANDARDS_MODE = false; +goog.dom.COMPAT_MODE_KNOWN_ = goog.dom.ASSUME_QUIRKS_MODE || goog.dom.ASSUME_STANDARDS_MODE; +goog.dom.NodeType = {ELEMENT:1, ATTRIBUTE:2, TEXT:3, CDATA_SECTION:4, ENTITY_REFERENCE:5, ENTITY:6, PROCESSING_INSTRUCTION:7, COMMENT:8, DOCUMENT:9, DOCUMENT_TYPE:10, DOCUMENT_FRAGMENT:11, NOTATION:12}; +goog.dom.getDomHelper = function(opt_element) { + return opt_element ? new goog.dom.DomHelper(goog.dom.getOwnerDocument(opt_element)) : goog.dom.defaultDomHelper_ || (goog.dom.defaultDomHelper_ = new goog.dom.DomHelper) +}; +goog.dom.defaultDomHelper_; +goog.dom.getDocument = function() { + return document +}; +goog.dom.getElement = function(element) { + return goog.isString(element) ? document.getElementById(element) : element +}; +goog.dom.$ = goog.dom.getElement; +goog.dom.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) { + return goog.dom.getElementsByTagNameAndClass_(document, opt_tag, opt_class, opt_el) +}; +goog.dom.getElementsByClass = function(className, opt_el) { + var parent = opt_el || document; + if(goog.dom.canUseQuerySelector_(parent)) { + return parent.querySelectorAll("." + className) + }else { + if(parent.getElementsByClassName) { + return parent.getElementsByClassName(className) + } + } + return goog.dom.getElementsByTagNameAndClass_(document, "*", className, opt_el) +}; +goog.dom.getElementByClass = function(className, opt_el) { + var parent = opt_el || document; + var retVal = null; + if(goog.dom.canUseQuerySelector_(parent)) { + retVal = parent.querySelector("." + className) + }else { + retVal = goog.dom.getElementsByClass(className, opt_el)[0] + } + return retVal || null +}; +goog.dom.canUseQuerySelector_ = function(parent) { + return parent.querySelectorAll && parent.querySelector && (!goog.userAgent.WEBKIT || goog.dom.isCss1CompatMode_(document) || goog.userAgent.isVersion("528")) +}; +goog.dom.getElementsByTagNameAndClass_ = function(doc, opt_tag, opt_class, opt_el) { + var parent = opt_el || doc; + var tagName = opt_tag && opt_tag != "*" ? opt_tag.toUpperCase() : ""; + if(goog.dom.canUseQuerySelector_(parent) && (tagName || opt_class)) { + var query = tagName + (opt_class ? "." + opt_class : ""); + return parent.querySelectorAll(query) + } + if(opt_class && parent.getElementsByClassName) { + var els = parent.getElementsByClassName(opt_class); + if(tagName) { + var arrayLike = {}; + var len = 0; + for(var i = 0, el;el = els[i];i++) { + if(tagName == el.nodeName) { + arrayLike[len++] = el + } + } + arrayLike.length = len; + return arrayLike + }else { + return els + } + } + var els = parent.getElementsByTagName(tagName || "*"); + if(opt_class) { + var arrayLike = {}; + var len = 0; + for(var i = 0, el;el = els[i];i++) { + var className = el.className; + if(typeof className.split == "function" && goog.array.contains(className.split(/\s+/), opt_class)) { + arrayLike[len++] = el + } + } + arrayLike.length = len; + return arrayLike + }else { + return els + } +}; +goog.dom.$$ = goog.dom.getElementsByTagNameAndClass; +goog.dom.setProperties = function(element, properties) { + goog.object.forEach(properties, function(val, key) { + if(key == "style") { + element.style.cssText = val + }else { + if(key == "class") { + element.className = val + }else { + if(key == "for") { + element.htmlFor = val + }else { + if(key in goog.dom.DIRECT_ATTRIBUTE_MAP_) { + element.setAttribute(goog.dom.DIRECT_ATTRIBUTE_MAP_[key], val) + }else { + element[key] = val + } + } + } + } + }) +}; +goog.dom.DIRECT_ATTRIBUTE_MAP_ = {cellpadding:"cellPadding", cellspacing:"cellSpacing", colspan:"colSpan", rowspan:"rowSpan", valign:"vAlign", height:"height", width:"width", usemap:"useMap", frameborder:"frameBorder", type:"type"}; +goog.dom.getViewportSize = function(opt_window) { + return goog.dom.getViewportSize_(opt_window || window) +}; +goog.dom.getViewportSize_ = function(win) { + var doc = win.document; + if(goog.userAgent.WEBKIT && !goog.userAgent.isVersion("500") && !goog.userAgent.MOBILE) { + if(typeof win.innerHeight == "undefined") { + win = window + } + var innerHeight = win.innerHeight; + var scrollHeight = win.document.documentElement.scrollHeight; + if(win == win.top) { + if(scrollHeight < innerHeight) { + innerHeight -= 15 + } + } + return new goog.math.Size(win.innerWidth, innerHeight) + } + var readsFromDocumentElement = goog.dom.isCss1CompatMode_(doc); + if(goog.userAgent.OPERA && !goog.userAgent.isVersion("9.50")) { + readsFromDocumentElement = false + } + var el = readsFromDocumentElement ? doc.documentElement : doc.body; + return new goog.math.Size(el.clientWidth, el.clientHeight) +}; +goog.dom.getDocumentHeight = function() { + return goog.dom.getDocumentHeight_(window) +}; +goog.dom.getDocumentHeight_ = function(win) { + var doc = win.document; + var height = 0; + if(doc) { + var vh = goog.dom.getViewportSize_(win).height; + var body = doc.body; + var docEl = doc.documentElement; + if(goog.dom.isCss1CompatMode_(doc) && docEl.scrollHeight) { + height = docEl.scrollHeight != vh ? docEl.scrollHeight : docEl.offsetHeight + }else { + var sh = docEl.scrollHeight; + var oh = docEl.offsetHeight; + if(docEl.clientHeight != oh) { + sh = body.scrollHeight; + oh = body.offsetHeight + } + if(sh > vh) { + height = sh > oh ? sh : oh + }else { + height = sh < oh ? sh : oh + } + } + } + return height +}; +goog.dom.getPageScroll = function(opt_window) { + var win = opt_window || goog.global || window; + return goog.dom.getDomHelper(win.document).getDocumentScroll() +}; +goog.dom.getDocumentScroll = function() { + return goog.dom.getDocumentScroll_(document) +}; +goog.dom.getDocumentScroll_ = function(doc) { + var el = goog.dom.getDocumentScrollElement_(doc); + return new goog.math.Coordinate(el.scrollLeft, el.scrollTop) +}; +goog.dom.getDocumentScrollElement = function() { + return goog.dom.getDocumentScrollElement_(document) +}; +goog.dom.getDocumentScrollElement_ = function(doc) { + return!goog.userAgent.WEBKIT && goog.dom.isCss1CompatMode_(doc) ? doc.documentElement : doc.body +}; +goog.dom.getWindow = function(opt_doc) { + return opt_doc ? goog.dom.getWindow_(opt_doc) : window +}; +goog.dom.getWindow_ = function(doc) { + return doc.parentWindow || doc.defaultView +}; +goog.dom.createDom = function(tagName, opt_attributes, var_args) { + return goog.dom.createDom_(document, arguments) +}; +goog.dom.createDom_ = function(doc, args) { + var tagName = args[0]; + var attributes = args[1]; + if(!goog.dom.BrowserFeature.CAN_ADD_NAME_OR_TYPE_ATTRIBUTES && attributes && (attributes.name || attributes.type)) { + var tagNameArr = ["<", tagName]; + if(attributes.name) { + tagNameArr.push(' name="', goog.string.htmlEscape(attributes.name), '"') + } + if(attributes.type) { + tagNameArr.push(' type="', goog.string.htmlEscape(attributes.type), '"'); + var clone = {}; + goog.object.extend(clone, attributes); + attributes = clone; + delete attributes.type + } + tagNameArr.push(">"); + tagName = tagNameArr.join("") + } + var element = doc.createElement(tagName); + if(attributes) { + if(goog.isString(attributes)) { + element.className = attributes + }else { + if(goog.isArray(attributes)) { + goog.dom.classes.add.apply(null, [element].concat(attributes)) + }else { + goog.dom.setProperties(element, attributes) + } + } + } + if(args.length > 2) { + goog.dom.append_(doc, element, args, 2) + } + return element +}; +goog.dom.append_ = function(doc, parent, args, startIndex) { + function childHandler(child) { + if(child) { + parent.appendChild(goog.isString(child) ? doc.createTextNode(child) : child) + } + } + for(var i = startIndex;i < args.length;i++) { + var arg = args[i]; + if(goog.isArrayLike(arg) && !goog.dom.isNodeLike(arg)) { + goog.array.forEach(goog.dom.isNodeList(arg) ? goog.array.clone(arg) : arg, childHandler) + }else { + childHandler(arg) + } + } +}; +goog.dom.$dom = goog.dom.createDom; +goog.dom.createElement = function(name) { + return document.createElement(name) +}; +goog.dom.createTextNode = function(content) { + return document.createTextNode(content) +}; +goog.dom.createTable = function(rows, columns, opt_fillWithNbsp) { + return goog.dom.createTable_(document, rows, columns, !!opt_fillWithNbsp) +}; +goog.dom.createTable_ = function(doc, rows, columns, fillWithNbsp) { + var rowHtml = ["<tr>"]; + for(var i = 0;i < columns;i++) { + rowHtml.push(fillWithNbsp ? "<td> </td>" : "<td></td>") + } + rowHtml.push("</tr>"); + rowHtml = rowHtml.join(""); + var totalHtml = ["<table>"]; + for(i = 0;i < rows;i++) { + totalHtml.push(rowHtml) + } + totalHtml.push("</table>"); + var elem = doc.createElement(goog.dom.TagName.DIV); + elem.innerHTML = totalHtml.join(""); + return elem.removeChild(elem.firstChild) +}; +goog.dom.htmlToDocumentFragment = function(htmlString) { + return goog.dom.htmlToDocumentFragment_(document, htmlString) +}; +goog.dom.htmlToDocumentFragment_ = function(doc, htmlString) { + var tempDiv = doc.createElement("div"); + if(goog.dom.BrowserFeature.INNER_HTML_NEEDS_SCOPED_ELEMENT) { + tempDiv.innerHTML = "<br>" + htmlString; + tempDiv.removeChild(tempDiv.firstChild) + }else { + tempDiv.innerHTML = htmlString + } + if(tempDiv.childNodes.length == 1) { + return tempDiv.removeChild(tempDiv.firstChild) + }else { + var fragment = doc.createDocumentFragment(); + while(tempDiv.firstChild) { + fragment.appendChild(tempDiv.firstChild) + } + return fragment + } +}; +goog.dom.getCompatMode = function() { + return goog.dom.isCss1CompatMode() ? "CSS1Compat" : "BackCompat" +}; +goog.dom.isCss1CompatMode = function() { + return goog.dom.isCss1CompatMode_(document) +}; +goog.dom.isCss1CompatMode_ = function(doc) { + if(goog.dom.COMPAT_MODE_KNOWN_) { + return goog.dom.ASSUME_STANDARDS_MODE + } + return doc.compatMode == "CSS1Compat" +}; +goog.dom.canHaveChildren = function(node) { + if(node.nodeType != goog.dom.NodeType.ELEMENT) { + return false + } + switch(node.tagName) { + case goog.dom.TagName.APPLET: + ; + case goog.dom.TagName.AREA: + ; + case goog.dom.TagName.BASE: + ; + case goog.dom.TagName.BR: + ; + case goog.dom.TagName.COL: + ; + case goog.dom.TagName.FRAME: + ; + case goog.dom.TagName.HR: + ; + case goog.dom.TagName.IMG: + ; + case goog.dom.TagName.INPUT: + ; + case goog.dom.TagName.IFRAME: + ; + case goog.dom.TagName.ISINDEX: + ; + case goog.dom.TagName.LINK: + ; + case goog.dom.TagName.NOFRAMES: + ; + case goog.dom.TagName.NOSCRIPT: + ; + case goog.dom.TagName.META: + ; + case goog.dom.TagName.OBJECT: + ; + case goog.dom.TagName.PARAM: + ; + case goog.dom.TagName.SCRIPT: + ; + case goog.dom.TagName.STYLE: + return false + } + return true +}; +goog.dom.appendChild = function(parent, child) { + parent.appendChild(child) +}; +goog.dom.append = function(parent, var_args) { + goog.dom.append_(goog.dom.getOwnerDocument(parent), parent, arguments, 1) +}; +goog.dom.removeChildren = function(node) { + var child; + while(child = node.firstChild) { + node.removeChild(child) + } +}; +goog.dom.insertSiblingBefore = function(newNode, refNode) { + if(refNode.parentNode) { + refNode.parentNode.insertBefore(newNode, refNode) + } +}; +goog.dom.insertSiblingAfter = function(newNode, refNode) { + if(refNode.parentNode) { + refNode.parentNode.insertBefore(newNode, refNode.nextSibling) + } +}; +goog.dom.removeNode = function(node) { + return node && node.parentNode ? node.parentNode.removeChild(node) : null +}; +goog.dom.replaceNode = function(newNode, oldNode) { + var parent = oldNode.parentNode; + if(parent) { + parent.replaceChild(newNode, oldNode) + } +}; +goog.dom.flattenElement = function(element) { + var child, parent = element.parentNode; + if(parent && parent.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) { + if(element.removeNode) { + return element.removeNode(false) + }else { + while(child = element.firstChild) { + parent.insertBefore(child, element) + } + return goog.dom.removeNode(element) + } + } +}; +goog.dom.getFirstElementChild = function(node) { + return goog.dom.getNextElementNode_(node.firstChild, true) +}; +goog.dom.getLastElementChild = function(node) { + return goog.dom.getNextElementNode_(node.lastChild, false) +}; +goog.dom.getNextElementSibling = function(node) { + return goog.dom.getNextElementNode_(node.nextSibling, true) +}; +goog.dom.getPreviousElementSibling = function(node) { + return goog.dom.getNextElementNode_(node.previousSibling, false) +}; +goog.dom.getNextElementNode_ = function(node, forward) { + while(node && node.nodeType != goog.dom.NodeType.ELEMENT) { + node = forward ? node.nextSibling : node.previousSibling + } + return node +}; +goog.dom.getNextNode = function(node) { + if(!node) { + return null + } + if(node.firstChild) { + return node.firstChild + } + while(node && !node.nextSibling) { + node = node.parentNode + } + return node ? node.nextSibling : null +}; +goog.dom.getPreviousNode = function(node) { + if(!node) { + return null + } + if(!node.previousSibling) { + return node.parentNode + } + node = node.previousSibling; + while(node && node.lastChild) { + node = node.lastChild + } + return node +}; +goog.dom.isNodeLike = function(obj) { + return goog.isObject(obj) && obj.nodeType > 0 +}; +goog.dom.contains = function(parent, descendant) { + if(parent.contains && descendant.nodeType == goog.dom.NodeType.ELEMENT) { + return parent == descendant || parent.contains(descendant) + } + if(typeof parent.compareDocumentPosition != "undefined") { + return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16) + } + while(descendant && parent != descendant) { + descendant = descendant.parentNode + } + return descendant == parent +}; +goog.dom.compareNodeOrder = function(node1, node2) { + if(node1 == node2) { + return 0 + } + if(node1.compareDocumentPosition) { + return node1.compareDocumentPosition(node2) & 2 ? 1 : -1 + } + if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) { + var isElement1 = node1.nodeType == goog.dom.NodeType.ELEMENT; + var isElement2 = node2.nodeType == goog.dom.NodeType.ELEMENT; + if(isElement1 && isElement2) { + return node1.sourceIndex - node2.sourceIndex + }else { + var parent1 = node1.parentNode; + var parent2 = node2.parentNode; + if(parent1 == parent2) { + return goog.dom.compareSiblingOrder_(node1, node2) + } + if(!isElement1 && goog.dom.contains(parent1, node2)) { + return-1 * goog.dom.compareParentsDescendantNodeIe_(node1, node2) + } + if(!isElement2 && goog.dom.contains(parent2, node1)) { + return goog.dom.compareParentsDescendantNodeIe_(node2, node1) + } + return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex) + } + } + var doc = goog.dom.getOwnerDocument(node1); + var range1, range2; + range1 = doc.createRange(); + range1.selectNode(node1); + range1.collapse(true); + range2 = doc.createRange(); + range2.selectNode(node2); + range2.collapse(true); + return range1.compareBoundaryPoints(goog.global["Range"].START_TO_END, range2) +}; +goog.dom.compareParentsDescendantNodeIe_ = function(textNode, node) { + var parent = textNode.parentNode; + if(parent == node) { + return-1 + } + var sibling = node; + while(sibling.parentNode != parent) { + sibling = sibling.parentNode + } + return goog.dom.compareSiblingOrder_(sibling, textNode) +}; +goog.dom.compareSiblingOrder_ = function(node1, node2) { + var s = node2; + while(s = s.previousSibling) { + if(s == node1) { + return-1 + } + } + return 1 +}; +goog.dom.findCommonAncestor = function(var_args) { + var i, count = arguments.length; + if(!count) { + return null + }else { + if(count == 1) { + return arguments[0] + } + } + var paths = []; + var minLength = Infinity; + for(i = 0;i < count;i++) { + var ancestors = []; + var node = arguments[i]; + while(node) { + ancestors.unshift(node); + node = node.parentNode + } + paths.push(ancestors); + minLength = Math.min(minLength, ancestors.length) + } + var output = null; + for(i = 0;i < minLength;i++) { + var first = paths[0][i]; + for(var j = 1;j < count;j++) { + if(first != paths[j][i]) { + return output + } + } + output = first + } + return output +}; +goog.dom.getOwnerDocument = function(node) { + return node.nodeType == goog.dom.NodeType.DOCUMENT ? node : node.ownerDocument || node.document +}; +goog.dom.getFrameContentDocument = function(frame) { + var doc; + if(goog.userAgent.WEBKIT) { + doc = frame.document || frame.contentWindow.document + }else { + doc = frame.contentDocument || frame.contentWindow.document + } + return doc +}; +goog.dom.getFrameContentWindow = function(frame) { + return frame.contentWindow || goog.dom.getWindow_(goog.dom.getFrameContentDocument(frame)) +}; +goog.dom.setTextContent = function(element, text) { + if("textContent" in element) { + element.textContent = text + }else { + if(element.firstChild && element.firstChild.nodeType == goog.dom.NodeType.TEXT) { + while(element.lastChild != element.firstChild) { + element.removeChild(element.lastChild) + } + element.firstChild.data = text + }else { + goog.dom.removeChildren(element); + var doc = goog.dom.getOwnerDocument(element); + element.appendChild(doc.createTextNode(text)) + } + } +}; +goog.dom.getOuterHtml = function(element) { + if("outerHTML" in element) { + return element.outerHTML + }else { + var doc = goog.dom.getOwnerDocument(element); + var div = doc.createElement("div"); + div.appendChild(element.cloneNode(true)); + return div.innerHTML + } +}; +goog.dom.findNode = function(root, p) { + var rv = []; + var found = goog.dom.findNodes_(root, p, rv, true); + return found ? rv[0] : undefined +}; +goog.dom.findNodes = function(root, p) { + var rv = []; + goog.dom.findNodes_(root, p, rv, false); + return rv +}; +goog.dom.findNodes_ = function(root, p, rv, findOne) { + if(root != null) { + for(var i = 0, child;child = root.childNodes[i];i++) { + if(p(child)) { + rv.push(child); + if(findOne) { + return true + } + } + if(goog.dom.findNodes_(child, p, rv, findOne)) { + return true + } + } + } + return false +}; +goog.dom.TAGS_TO_IGNORE_ = {SCRIPT:1, STYLE:1, HEAD:1, IFRAME:1, OBJECT:1}; +goog.dom.PREDEFINED_TAG_VALUES_ = {IMG:" ", BR:"\n"}; +goog.dom.isFocusableTabIndex = function(element) { + var attrNode = element.getAttributeNode("tabindex"); + if(attrNode && attrNode.specified) { + var index = element.tabIndex; + return goog.isNumber(index) && index >= 0 + } + return false +}; +goog.dom.setFocusableTabIndex = function(element, enable) { + if(enable) { + element.tabIndex = 0 + }else { + element.removeAttribute("tabIndex") + } +}; +goog.dom.getTextContent = function(node) { + var textContent; + if(goog.dom.BrowserFeature.CAN_USE_INNER_TEXT && "innerText" in node) { + textContent = goog.string.canonicalizeNewlines(node.innerText) + }else { + var buf = []; + goog.dom.getTextContent_(node, buf, true); + textContent = buf.join("") + } + textContent = textContent.replace(/ \xAD /g, " ").replace(/\xAD/g, ""); + if(!goog.userAgent.IE) { + textContent = textContent.replace(/ +/g, " ") + } + if(textContent != " ") { + textContent = textContent.replace(/^\s*/, "") + } + return textContent +}; +goog.dom.getRawTextContent = function(node) { + var buf = []; + goog.dom.getTextContent_(node, buf, false); + return buf.join("") +}; +goog.dom.getTextContent_ = function(node, buf, normalizeWhitespace) { + if(node.nodeName in goog.dom.TAGS_TO_IGNORE_) { + }else { + if(node.nodeType == goog.dom.NodeType.TEXT) { + if(normalizeWhitespace) { + buf.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, "")) + }else { + buf.push(node.nodeValue) + } + }else { + if(node.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) { + buf.push(goog.dom.PREDEFINED_TAG_VALUES_[node.nodeName]) + }else { + var child = node.firstChild; + while(child) { + goog.dom.getTextContent_(child, buf, normalizeWhitespace); + child = child.nextSibling + } + } + } + } +}; +goog.dom.getNodeTextLength = function(node) { + return goog.dom.getTextContent(node).length +}; +goog.dom.getNodeTextOffset = function(node, opt_offsetParent) { + var root = opt_offsetParent || goog.dom.getOwnerDocument(node).body; + var buf = []; + while(node && node != root) { + var cur = node; + while(cur = cur.previousSibling) { + buf.unshift(goog.dom.getTextContent(cur)) + } + node = node.parentNode + } + return goog.string.trimLeft(buf.join("")).replace(/ +/g, " ").length +}; +goog.dom.getNodeAtOffset = function(parent, offset, opt_result) { + var stack = [parent], pos = 0, cur; + while(stack.length > 0 && pos < offset) { + cur = stack.pop(); + if(cur.nodeName in goog.dom.TAGS_TO_IGNORE_) { + }else { + if(cur.nodeType == goog.dom.NodeType.TEXT) { + var text = cur.nodeValue.replace(/(\r\n|\r|\n)/g, "").replace(/ +/g, " "); + pos += text.length + }else { + if(cur.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) { + pos += goog.dom.PREDEFINED_TAG_VALUES_[cur.nodeName].length + }else { + for(var i = cur.childNodes.length - 1;i >= 0;i--) { + stack.push(cur.childNodes[i]) + } + } + } + } + } + if(goog.isObject(opt_result)) { + opt_result.remainder = cur ? cur.nodeValue.length + offset - pos - 1 : 0; + opt_result.node = cur + } + return cur +}; +goog.dom.isNodeList = function(val) { + if(val && typeof val.length == "number") { + if(goog.isObject(val)) { + return typeof val.item == "function" || typeof val.item == "string" + }else { + if(goog.isFunction(val)) { + return typeof val.item == "function" + } + } + } + return false +}; +goog.dom.getAncestorByTagNameAndClass = function(element, opt_tag, opt_class) { + var tagName = opt_tag ? opt_tag.toUpperCase() : null; + return goog.dom.getAncestor(element, function(node) { + return(!tagName || node.nodeName == tagName) && (!opt_class || goog.dom.classes.has(node, opt_class)) + }, true) +}; +goog.dom.getAncestor = function(element, matcher, opt_includeNode, opt_maxSearchSteps) { + if(!opt_includeNode) { + element = element.parentNode + } + var ignoreSearchSteps = opt_maxSearchSteps == null; + var steps = 0; + while(element && (ignoreSearchSteps || steps <= opt_maxSearchSteps)) { + if(matcher(element)) { + return element + } + element = element.parentNode; + steps++ + } + return null +}; +goog.dom.DomHelper = function(opt_document) { + this.document_ = opt_document || goog.global.document || document +}; +goog.dom.DomHelper.prototype.getDomHelper = goog.dom.getDomHelper; +goog.dom.DomHelper.prototype.setDocument = function(document) { + this.document_ = document +}; +goog.dom.DomHelper.prototype.getDocument = function() { + return this.document_ +}; +goog.dom.DomHelper.prototype.getElement = function(element) { + if(goog.isString(element)) { + return this.document_.getElementById(element) + }else { + return element + } +}; +goog.dom.DomHelper.prototype.$ = goog.dom.DomHelper.prototype.getElement; +goog.dom.DomHelper.prototype.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) { + return goog.dom.getElementsByTagNameAndClass_(this.document_, opt_tag, opt_class, opt_el) +}; +goog.dom.DomHelper.prototype.getElementsByClass = function(className, opt_el) { + var doc = opt_el || this.document_; + return goog.dom.getElementsByClass(className, doc) +}; +goog.dom.DomHelper.prototype.getElementByClass = function(className, opt_el) { + var doc = opt_el || this.document_; + return goog.dom.getElementByClass(className, doc) +}; +goog.dom.DomHelper.prototype.$$ = goog.dom.DomHelper.prototype.getElementsByTagNameAndClass; +goog.dom.DomHelper.prototype.setProperties = goog.dom.setProperties; +goog.dom.DomHelper.prototype.getViewportSize = function(opt_window) { + return goog.dom.getViewportSize(opt_window || this.getWindow()) +}; +goog.dom.DomHelper.prototype.getDocumentHeight = function() { + return goog.dom.getDocumentHeight_(this.getWindow()) +}; +goog.dom.Appendable; +goog.dom.DomHelper.prototype.createDom = function(tagName, opt_attributes, var_args) { + return goog.dom.createDom_(this.document_, arguments) +}; +goog.dom.DomHelper.prototype.$dom = goog.dom.DomHelper.prototype.createDom; +goog.dom.DomHelper.prototype.createElement = function(name) { + return this.document_.createElement(name) +}; +goog.dom.DomHelper.prototype.createTextNode = function(content) { + return this.document_.createTextNode(content) +}; +goog.dom.DomHelper.prototype.createTable = function(rows, columns, opt_fillWithNbsp) { + return goog.dom.createTable_(this.document_, rows, columns, !!opt_fillWithNbsp) +}; +goog.dom.DomHelper.prototype.htmlToDocumentFragment = function(htmlString) { + return goog.dom.htmlToDocumentFragment_(this.document_, htmlString) +}; +goog.dom.DomHelper.prototype.getCompatMode = function() { + return this.isCss1CompatMode() ? "CSS1Compat" : "BackCompat" +}; +goog.dom.DomHelper.prototype.isCss1CompatMode = function() { + return goog.dom.isCss1CompatMode_(this.document_) +}; +goog.dom.DomHelper.prototype.getWindow = function() { + return goog.dom.getWindow_(this.document_) +}; +goog.dom.DomHelper.prototype.getDocumentScrollElement = function() { + return goog.dom.getDocumentScrollElement_(this.document_) +}; +goog.dom.DomHelper.prototype.getDocumentScroll = function() { + return goog.dom.getDocumentScroll_(this.document_) +}; +goog.dom.DomHelper.prototype.appendChild = goog.dom.appendChild; +goog.dom.DomHelper.prototype.append = goog.dom.append; +goog.dom.DomHelper.prototype.removeChildren = goog.dom.removeChildren; +goog.dom.DomHelper.prototype.insertSiblingBefore = goog.dom.insertSiblingBefore; +goog.dom.DomHelper.prototype.insertSiblingAfter = goog.dom.insertSiblingAfter; +goog.dom.DomHelper.prototype.removeNode = goog.dom.removeNode; +goog.dom.DomHelper.prototype.replaceNode = goog.dom.replaceNode; +goog.dom.DomHelper.prototype.flattenElement = goog.dom.flattenElement; +goog.dom.DomHelper.prototype.getFirstElementChild = goog.dom.getFirstElementChild; +goog.dom.DomHelper.prototype.getLastElementChild = goog.dom.getLastElementChild; +goog.dom.DomHelper.prototype.getNextElementSibling = goog.dom.getNextElementSibling; +goog.dom.DomHelper.prototype.getPreviousElementSibling = goog.dom.getPreviousElementSibling; +goog.dom.DomHelper.prototype.getNextNode = goog.dom.getNextNode; +goog.dom.DomHelper.prototype.getPreviousNode = goog.dom.getPreviousNode; +goog.dom.DomHelper.prototype.isNodeLike = goog.dom.isNodeLike; +goog.dom.DomHelper.prototype.contains = goog.dom.contains; +goog.dom.DomHelper.prototype.getOwnerDocument = goog.dom.getOwnerDocument; +goog.dom.DomHelper.prototype.getFrameContentDocument = goog.dom.getFrameContentDocument; +goog.dom.DomHelper.prototype.getFrameContentWindow = goog.dom.getFrameContentWindow; +goog.dom.DomHelper.prototype.setTextContent = goog.dom.setTextContent; +goog.dom.DomHelper.prototype.findNode = goog.dom.findNode; +goog.dom.DomHelper.prototype.findNodes = goog.dom.findNodes; +goog.dom.DomHelper.prototype.getTextContent = goog.dom.getTextContent; +goog.dom.DomHelper.prototype.getNodeTextLength = goog.dom.getNodeTextLength; +goog.dom.DomHelper.prototype.getNodeTextOffset = goog.dom.getNodeTextOffset; +goog.dom.DomHelper.prototype.getAncestorByTagNameAndClass = goog.dom.getAncestorByTagNameAndClass; +goog.dom.DomHelper.prototype.getAncestor = goog.dom.getAncestor; +goog.provide("goog.Disposable"); +goog.provide("goog.dispose"); +goog.Disposable = function() { +}; +goog.Disposable.prototype.disposed_ = false; +goog.Disposable.prototype.isDisposed = function() { + return this.disposed_ +}; +goog.Disposable.prototype.getDisposed = goog.Disposable.prototype.isDisposed; +goog.Disposable.prototype.dispose = function() { + if(!this.disposed_) { + this.disposed_ = true; + this.disposeInternal() + } +}; +goog.Disposable.prototype.disposeInternal = function() { +}; +goog.dispose = function(obj) { + if(obj && typeof obj.dispose == "function") { + obj.dispose() + } +}; +goog.provide("goog.structs"); +goog.require("goog.array"); +goog.require("goog.object"); +goog.structs.getCount = function(col) { + if(typeof col.getCount == "function") { + return col.getCount() + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return col.length + } + return goog.object.getCount(col) +}; +goog.structs.getValues = function(col) { + if(typeof col.getValues == "function") { + return col.getValues() + } + if(goog.isString(col)) { + return col.split("") + } + if(goog.isArrayLike(col)) { + var rv = []; + var l = col.length; + for(var i = 0;i < l;i++) { + rv.push(col[i]) + } + return rv + } + return goog.object.getValues(col) +}; +goog.structs.getKeys = function(col) { + if(typeof col.getKeys == "function") { + return col.getKeys() + } + if(typeof col.getValues == "function") { + return undefined + } + if(goog.isArrayLike(col) || goog.isString(col)) { + var rv = []; + var l = col.length; + for(var i = 0;i < l;i++) { + rv.push(i) + } + return rv + } + return goog.object.getKeys(col) +}; +goog.structs.contains = function(col, val) { + if(typeof col.contains == "function") { + return col.contains(val) + } + if(typeof col.containsValue == "function") { + return col.containsValue(val) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.contains(col, val) + } + return goog.object.containsValue(col, val) +}; +goog.structs.isEmpty = function(col) { + if(typeof col.isEmpty == "function") { + return col.isEmpty() + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.isEmpty(col) + } + return goog.object.isEmpty(col) +}; +goog.structs.clear = function(col) { + if(typeof col.clear == "function") { + col.clear() + }else { + if(goog.isArrayLike(col)) { + goog.array.clear(col) + }else { + goog.object.clear(col) + } + } +}; +goog.structs.forEach = function(col, f, opt_obj) { + if(typeof col.forEach == "function") { + col.forEach(f, opt_obj) + }else { + if(goog.isArrayLike(col) || goog.isString(col)) { + goog.array.forEach(col, f, opt_obj) + }else { + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + f.call(opt_obj, values[i], keys && keys[i], col) + } + } + } +}; +goog.structs.filter = function(col, f, opt_obj) { + if(typeof col.filter == "function") { + return col.filter(f, opt_obj) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.filter(col, f, opt_obj) + } + var rv; + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + if(keys) { + rv = {}; + for(var i = 0;i < l;i++) { + if(f.call(opt_obj, values[i], keys[i], col)) { + rv[keys[i]] = values[i] + } + } + }else { + rv = []; + for(var i = 0;i < l;i++) { + if(f.call(opt_obj, values[i], undefined, col)) { + rv.push(values[i]) + } + } + } + return rv +}; +goog.structs.map = function(col, f, opt_obj) { + if(typeof col.map == "function") { + return col.map(f, opt_obj) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.map(col, f, opt_obj) + } + var rv; + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + if(keys) { + rv = {}; + for(var i = 0;i < l;i++) { + rv[keys[i]] = f.call(opt_obj, values[i], keys[i], col) + } + }else { + rv = []; + for(var i = 0;i < l;i++) { + rv[i] = f.call(opt_obj, values[i], undefined, col) + } + } + return rv +}; +goog.structs.some = function(col, f, opt_obj) { + if(typeof col.some == "function") { + return col.some(f, opt_obj) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.some(col, f, opt_obj) + } + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + if(f.call(opt_obj, values[i], keys && keys[i], col)) { + return true + } + } + return false +}; +goog.structs.every = function(col, f, opt_obj) { + if(typeof col.every == "function") { + return col.every(f, opt_obj) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.every(col, f, opt_obj) + } + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + if(!f.call(opt_obj, values[i], keys && keys[i], col)) { + return false + } + } + return true +}; +goog.provide("goog.iter"); +goog.provide("goog.iter.Iterator"); +goog.provide("goog.iter.StopIteration"); +goog.require("goog.array"); +goog.iter.Iterable; +if("StopIteration" in goog.global) { + goog.iter.StopIteration = goog.global["StopIteration"] +}else { + goog.iter.StopIteration = Error("StopIteration") +} +goog.iter.Iterator = function() { +}; +goog.iter.Iterator.prototype.next = function() { + throw goog.iter.StopIteration; +}; +goog.iter.Iterator.prototype.__iterator__ = function(opt_keys) { + return this +}; +goog.iter.toIterator = function(iterable) { + if(iterable instanceof goog.iter.Iterator) { + return iterable + } + if(typeof iterable.__iterator__ == "function") { + return iterable.__iterator__(false) + } + if(goog.isArrayLike(iterable)) { + var i = 0; + var newIter = new goog.iter.Iterator; + newIter.next = function() { + while(true) { + if(i >= iterable.length) { + throw goog.iter.StopIteration; + } + if(!(i in iterable)) { + i++; + continue + } + return iterable[i++] + } + }; + return newIter + } + throw Error("Not implemented"); +}; +goog.iter.forEach = function(iterable, f, opt_obj) { + if(goog.isArrayLike(iterable)) { + try { + goog.array.forEach(iterable, f, opt_obj) + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + } + } + }else { + iterable = goog.iter.toIterator(iterable); + try { + while(true) { + f.call(opt_obj, iterable.next(), undefined, iterable) + } + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + } + } + } +}; +goog.iter.filter = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + var newIter = new goog.iter.Iterator; + newIter.next = function() { + while(true) { + var val = iterable.next(); + if(f.call(opt_obj, val, undefined, iterable)) { + return val + } + } + }; + return newIter +}; +goog.iter.range = function(startOrStop, opt_stop, opt_step) { + var start = 0; + var stop = startOrStop; + var step = opt_step || 1; + if(arguments.length > 1) { + start = startOrStop; + stop = opt_stop + } + if(step == 0) { + throw Error("Range step argument must not be zero"); + } + var newIter = new goog.iter.Iterator; + newIter.next = function() { + if(step > 0 && start >= stop || step < 0 && start <= stop) { + throw goog.iter.StopIteration; + } + var rv = start; + start += step; + return rv + }; + return newIter +}; +goog.iter.join = function(iterable, deliminator) { + return goog.iter.toArray(iterable).join(deliminator) +}; +goog.iter.map = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + var newIter = new goog.iter.Iterator; + newIter.next = function() { + while(true) { + var val = iterable.next(); + return f.call(opt_obj, val, undefined, iterable) + } + }; + return newIter +}; +goog.iter.reduce = function(iterable, f, val, opt_obj) { + var rval = val; + goog.iter.forEach(iterable, function(val) { + rval = f.call(opt_obj, rval, val) + }); + return rval +}; +goog.iter.some = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + try { + while(true) { + if(f.call(opt_obj, iterable.next(), undefined, iterable)) { + return true + } + } + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + } + } + return false +}; +goog.iter.every = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + try { + while(true) { + if(!f.call(opt_obj, iterable.next(), undefined, iterable)) { + return false + } + } + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + } + } + return true +}; +goog.iter.chain = function(var_args) { + var args = arguments; + var length = args.length; + var i = 0; + var newIter = new goog.iter.Iterator; + newIter.next = function() { + try { + if(i >= length) { + throw goog.iter.StopIteration; + } + var current = goog.iter.toIterator(args[i]); + return current.next() + }catch(ex) { + if(ex !== goog.iter.StopIteration || i >= length) { + throw ex; + }else { + i++; + return this.next() + } + } + }; + return newIter +}; +goog.iter.dropWhile = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + var newIter = new goog.iter.Iterator; + var dropping = true; + newIter.next = function() { + while(true) { + var val = iterable.next(); + if(dropping && f.call(opt_obj, val, undefined, iterable)) { + continue + }else { + dropping = false + } + return val + } + }; + return newIter +}; +goog.iter.takeWhile = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + var newIter = new goog.iter.Iterator; + var taking = true; + newIter.next = function() { + while(true) { + if(taking) { + var val = iterable.next(); + if(f.call(opt_obj, val, undefined, iterable)) { + return val + }else { + taking = false + } + }else { + throw goog.iter.StopIteration; + } + } + }; + return newIter +}; +goog.iter.toArray = function(iterable) { + if(goog.isArrayLike(iterable)) { + return goog.array.toArray(iterable) + } + iterable = goog.iter.toIterator(iterable); + var array = []; + goog.iter.forEach(iterable, function(val) { + array.push(val) + }); + return array +}; +goog.iter.equals = function(iterable1, iterable2) { + iterable1 = goog.iter.toIterator(iterable1); + iterable2 = goog.iter.toIterator(iterable2); + var b1, b2; + try { + while(true) { + b1 = b2 = false; + var val1 = iterable1.next(); + b1 = true; + var val2 = iterable2.next(); + b2 = true; + if(val1 != val2) { + return false + } + } + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + }else { + if(b1 && !b2) { + return false + } + if(!b2) { + try { + val2 = iterable2.next(); + return false + }catch(ex1) { + if(ex1 !== goog.iter.StopIteration) { + throw ex1; + } + return true + } + } + } + } + return false +}; +goog.iter.nextOrValue = function(iterable, defaultValue) { + try { + return goog.iter.toIterator(iterable).next() + }catch(e) { + if(e != goog.iter.StopIteration) { + throw e; + } + return defaultValue + } +}; +goog.provide("goog.structs.Map"); +goog.require("goog.iter.Iterator"); +goog.require("goog.iter.StopIteration"); +goog.require("goog.object"); +goog.require("goog.structs"); +goog.structs.Map = function(opt_map, var_args) { + this.map_ = {}; + this.keys_ = []; + var argLength = arguments.length; + if(argLength > 1) { + if(argLength % 2) { + throw Error("Uneven number of arguments"); + } + for(var i = 0;i < argLength;i += 2) { + this.set(arguments[i], arguments[i + 1]) + } + }else { + if(opt_map) { + this.addAll(opt_map) + } + } +}; +goog.structs.Map.prototype.count_ = 0; +goog.structs.Map.prototype.version_ = 0; +goog.structs.Map.prototype.getCount = function() { + return this.count_ +}; +goog.structs.Map.prototype.getValues = function() { + this.cleanupKeysArray_(); + var rv = []; + for(var i = 0;i < this.keys_.length;i++) { + var key = this.keys_[i]; + rv.push(this.map_[key]) + } + return rv +}; +goog.structs.Map.prototype.getKeys = function() { + this.cleanupKeysArray_(); + return this.keys_.concat() +}; +goog.structs.Map.prototype.containsKey = function(key) { + return goog.structs.Map.hasKey_(this.map_, key) +}; +goog.structs.Map.prototype.containsValue = function(val) { + for(var i = 0;i < this.keys_.length;i++) { + var key = this.keys_[i]; + if(goog.structs.Map.hasKey_(this.map_, key) && this.map_[key] == val) { + return true + } + } + return false +}; +goog.structs.Map.prototype.equals = function(otherMap, opt_equalityFn) { + if(this === otherMap) { + return true + } + if(this.count_ != otherMap.getCount()) { + return false + } + var equalityFn = opt_equalityFn || goog.structs.Map.defaultEquals; + this.cleanupKeysArray_(); + for(var key, i = 0;key = this.keys_[i];i++) { + if(!equalityFn(this.get(key), otherMap.get(key))) { + return false + } + } + return true +}; +goog.structs.Map.defaultEquals = function(a, b) { + return a === b +}; +goog.structs.Map.prototype.isEmpty = function() { + return this.count_ == 0 +}; +goog.structs.Map.prototype.clear = function() { + this.map_ = {}; + this.keys_.length = 0; + this.count_ = 0; + this.version_ = 0 +}; +goog.structs.Map.prototype.remove = function(key) { + if(goog.structs.Map.hasKey_(this.map_, key)) { + delete this.map_[key]; + this.count_--; + this.version_++; + if(this.keys_.length > 2 * this.count_) { + this.cleanupKeysArray_() + } + return true + } + return false +}; +goog.structs.Map.prototype.cleanupKeysArray_ = function() { + if(this.count_ != this.keys_.length) { + var srcIndex = 0; + var destIndex = 0; + while(srcIndex < this.keys_.length) { + var key = this.keys_[srcIndex]; + if(goog.structs.Map.hasKey_(this.map_, key)) { + this.keys_[destIndex++] = key + } + srcIndex++ + } + this.keys_.length = destIndex + } + if(this.count_ != this.keys_.length) { + var seen = {}; + var srcIndex = 0; + var destIndex = 0; + while(srcIndex < this.keys_.length) { + var key = this.keys_[srcIndex]; + if(!goog.structs.Map.hasKey_(seen, key)) { + this.keys_[destIndex++] = key; + seen[key] = 1 + } + srcIndex++ + } + this.keys_.length = destIndex + } +}; +goog.structs.Map.prototype.get = function(key, opt_val) { + if(goog.structs.Map.hasKey_(this.map_, key)) { + return this.map_[key] + } + return opt_val +}; +goog.structs.Map.prototype.set = function(key, value) { + if(!goog.structs.Map.hasKey_(this.map_, key)) { + this.count_++; + this.keys_.push(key); + this.version_++ + } + this.map_[key] = value +}; +goog.structs.Map.prototype.addAll = function(map) { + var keys, values; + if(map instanceof goog.structs.Map) { + keys = map.getKeys(); + values = map.getValues() + }else { + keys = goog.object.getKeys(map); + values = goog.object.getValues(map) + } + for(var i = 0;i < keys.length;i++) { + this.set(keys[i], values[i]) + } +}; +goog.structs.Map.prototype.clone = function() { + return new goog.structs.Map(this) +}; +goog.structs.Map.prototype.transpose = function() { + var transposed = new goog.structs.Map; + for(var i = 0;i < this.keys_.length;i++) { + var key = this.keys_[i]; + var value = this.map_[key]; + transposed.set(value, key) + } + return transposed +}; +goog.structs.Map.prototype.toObject = function() { + this.cleanupKeysArray_(); + var obj = {}; + for(var i = 0;i < this.keys_.length;i++) { + var key = this.keys_[i]; + obj[key] = this.map_[key] + } + return obj +}; +goog.structs.Map.prototype.getKeyIterator = function() { + return this.__iterator__(true) +}; +goog.structs.Map.prototype.getValueIterator = function() { + return this.__iterator__(false) +}; +goog.structs.Map.prototype.__iterator__ = function(opt_keys) { + this.cleanupKeysArray_(); + var i = 0; + var keys = this.keys_; + var map = this.map_; + var version = this.version_; + var selfObj = this; + var newIter = new goog.iter.Iterator; + newIter.next = function() { + while(true) { + if(version != selfObj.version_) { + throw Error("The map has changed since the iterator was created"); + } + if(i >= keys.length) { + throw goog.iter.StopIteration; + } + var key = keys[i++]; + return opt_keys ? key : map[key] + } + }; + return newIter +}; +goog.structs.Map.hasKey_ = function(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key) +}; +goog.provide("goog.structs.Set"); +goog.require("goog.structs"); +goog.require("goog.structs.Map"); +goog.structs.Set = function(opt_values) { + this.map_ = new goog.structs.Map; + if(opt_values) { + this.addAll(opt_values) + } +}; +goog.structs.Set.getKey_ = function(val) { + var type = typeof val; + if(type == "object" && val || type == "function") { + return"o" + goog.getUid(val) + }else { + return type.substr(0, 1) + val + } +}; +goog.structs.Set.prototype.getCount = function() { + return this.map_.getCount() +}; +goog.structs.Set.prototype.add = function(element) { + this.map_.set(goog.structs.Set.getKey_(element), element) +}; +goog.structs.Set.prototype.addAll = function(col) { + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + this.add(values[i]) + } +}; +goog.structs.Set.prototype.removeAll = function(col) { + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + this.remove(values[i]) + } +}; +goog.structs.Set.prototype.remove = function(element) { + return this.map_.remove(goog.structs.Set.getKey_(element)) +}; +goog.structs.Set.prototype.clear = function() { + this.map_.clear() +}; +goog.structs.Set.prototype.isEmpty = function() { + return this.map_.isEmpty() +}; +goog.structs.Set.prototype.contains = function(element) { + return this.map_.containsKey(goog.structs.Set.getKey_(element)) +}; +goog.structs.Set.prototype.containsAll = function(col) { + return goog.structs.every(col, this.contains, this) +}; +goog.structs.Set.prototype.intersection = function(col) { + var result = new goog.structs.Set; + var values = goog.structs.getValues(col); + for(var i = 0;i < values.length;i++) { + var value = values[i]; + if(this.contains(value)) { + result.add(value) + } + } + return result +}; +goog.structs.Set.prototype.getValues = function() { + return this.map_.getValues() +}; +goog.structs.Set.prototype.clone = function() { + return new goog.structs.Set(this) +}; +goog.structs.Set.prototype.equals = function(col) { + return this.getCount() == goog.structs.getCount(col) && this.isSubsetOf(col) +}; +goog.structs.Set.prototype.isSubsetOf = function(col) { + var colCount = goog.structs.getCount(col); + if(this.getCount() > colCount) { + return false + } + if(!(col instanceof goog.structs.Set) && colCount > 5) { + col = new goog.structs.Set(col) + } + return goog.structs.every(this, function(value) { + return goog.structs.contains(col, value) + }) +}; +goog.structs.Set.prototype.__iterator__ = function(opt_keys) { + return this.map_.__iterator__(false) +}; +goog.provide("goog.debug"); +goog.require("goog.array"); +goog.require("goog.string"); +goog.require("goog.structs.Set"); +goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) { + var target = opt_target || goog.global; + var oldErrorHandler = target.onerror; + target.onerror = function(message, url, line) { + if(oldErrorHandler) { + oldErrorHandler(message, url, line) + } + logFunc({message:message, fileName:url, line:line}); + return Boolean(opt_cancel) + } +}; +goog.debug.expose = function(obj, opt_showFn) { + if(typeof obj == "undefined") { + return"undefined" + } + if(obj == null) { + return"NULL" + } + var str = []; + for(var x in obj) { + if(!opt_showFn && goog.isFunction(obj[x])) { + continue + } + var s = x + " = "; + try { + s += obj[x] + }catch(e) { + s += "*** " + e + " ***" + } + str.push(s) + } + return str.join("\n") +}; +goog.debug.deepExpose = function(obj, opt_showFn) { + var previous = new goog.structs.Set; + var str = []; + var helper = function(obj, space) { + var nestspace = space + " "; + var indentMultiline = function(str) { + return str.replace(/\n/g, "\n" + space) + }; + try { + if(!goog.isDef(obj)) { + str.push("undefined") + }else { + if(goog.isNull(obj)) { + str.push("NULL") + }else { + if(goog.isString(obj)) { + str.push('"' + indentMultiline(obj) + '"') + }else { + if(goog.isFunction(obj)) { + str.push(indentMultiline(String(obj))) + }else { + if(goog.isObject(obj)) { + if(previous.contains(obj)) { + str.push("*** reference loop detected ***") + }else { + previous.add(obj); + str.push("{"); + for(var x in obj) { + if(!opt_showFn && goog.isFunction(obj[x])) { + continue + } + str.push("\n"); + str.push(nestspace); + str.push(x + " = "); + helper(obj[x], nestspace) + } + str.push("\n" + space + "}") + } + }else { + str.push(obj) + } + } + } + } + } + }catch(e) { + str.push("*** " + e + " ***") + } + }; + helper(obj, ""); + return str.join("") +}; +goog.debug.exposeArray = function(arr) { + var str = []; + for(var i = 0;i < arr.length;i++) { + if(goog.isArray(arr[i])) { + str.push(goog.debug.exposeArray(arr[i])) + }else { + str.push(arr[i]) + } + } + return"[ " + str.join(", ") + " ]" +}; +goog.debug.exposeException = function(err, opt_fn) { + try { + var e = goog.debug.normalizeErrorObject(err); + var error = "Message: " + goog.string.htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog.string.htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog.string.htmlEscape(goog.debug.getStacktrace(opt_fn) + "-> "); + return error + }catch(e2) { + return"Exception trying to expose exception! You win, we lose. " + e2 + } +}; +goog.debug.normalizeErrorObject = function(err) { + var href = goog.getObjectByName("window.location.href"); + return typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:href, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || href, stack:err.stack || "Not available"} : err +}; +goog.debug.enhanceError = function(err, opt_message) { + var error = typeof err == "string" ? Error(err) : err; + if(!error.stack) { + error.stack = goog.debug.getStacktrace(arguments.callee.caller) + } + if(opt_message) { + var x = 0; + while(error["message" + x]) { + ++x + } + error["message" + x] = String(opt_message) + } + return error +}; +goog.debug.getStacktraceSimple = function(opt_depth) { + var sb = []; + var fn = arguments.callee.caller; + var depth = 0; + while(fn && (!opt_depth || depth < opt_depth)) { + sb.push(goog.debug.getFunctionName(fn)); + sb.push("()\n"); + try { + fn = fn.caller + }catch(e) { + sb.push("[exception trying to get caller]\n"); + break + } + depth++; + if(depth >= goog.debug.MAX_STACK_DEPTH) { + sb.push("[...long stack...]"); + break + } + } + if(opt_depth && depth >= opt_depth) { + sb.push("[...reached max depth limit...]") + }else { + sb.push("[end]") + } + return sb.join("") +}; +goog.debug.MAX_STACK_DEPTH = 50; +goog.debug.getStacktrace = function(opt_fn) { + return goog.debug.getStacktraceHelper_(opt_fn || arguments.callee.caller, []) +}; +goog.debug.getStacktraceHelper_ = function(fn, visited) { + var sb = []; + if(goog.array.contains(visited, fn)) { + sb.push("[...circular reference...]") + }else { + if(fn && visited.length < goog.debug.MAX_STACK_DEPTH) { + sb.push(goog.debug.getFunctionName(fn) + "("); + var args = fn.arguments; + for(var i = 0;i < args.length;i++) { + if(i > 0) { + sb.push(", ") + } + var argDesc; + var arg = args[i]; + switch(typeof arg) { + case "object": + argDesc = arg ? "object" : "null"; + break; + case "string": + argDesc = arg; + break; + case "number": + argDesc = String(arg); + break; + case "boolean": + argDesc = arg ? "true" : "false"; + break; + case "function": + argDesc = goog.debug.getFunctionName(arg); + argDesc = argDesc ? argDesc : "[fn]"; + break; + case "undefined": + ; + default: + argDesc = typeof arg; + break + } + if(argDesc.length > 40) { + argDesc = argDesc.substr(0, 40) + "..." + } + sb.push(argDesc) + } + visited.push(fn); + sb.push(")\n"); + try { + sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited)) + }catch(e) { + sb.push("[exception trying to get caller]\n") + } + }else { + if(fn) { + sb.push("[...long stack...]") + }else { + sb.push("[end]") + } + } + } + return sb.join("") +}; +goog.debug.getFunctionName = function(fn) { + var functionSource = String(fn); + if(!goog.debug.fnNameCache_[functionSource]) { + var matches = /function ([^\(]+)/.exec(functionSource); + if(matches) { + var method = matches[1]; + goog.debug.fnNameCache_[functionSource] = method + }else { + goog.debug.fnNameCache_[functionSource] = "[Anonymous]" + } + } + return goog.debug.fnNameCache_[functionSource] +}; +goog.debug.makeWhitespaceVisible = function(string) { + return string.replace(/ /g, "[_]").replace(/\f/g, "[f]").replace(/\n/g, "[n]\n").replace(/\r/g, "[r]").replace(/\t/g, "[t]") +}; +goog.debug.fnNameCache_ = {}; +goog.provide("goog.debug.LogRecord"); +goog.debug.LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) { + this.reset(level, msg, loggerName, opt_time, opt_sequenceNumber) +}; +goog.debug.LogRecord.prototype.time_; +goog.debug.LogRecord.prototype.level_; +goog.debug.LogRecord.prototype.msg_; +goog.debug.LogRecord.prototype.loggerName_; +goog.debug.LogRecord.prototype.sequenceNumber_ = 0; +goog.debug.LogRecord.prototype.exception_ = null; +goog.debug.LogRecord.prototype.exceptionText_ = null; +goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS = true; +goog.debug.LogRecord.nextSequenceNumber_ = 0; +goog.debug.LogRecord.prototype.reset = function(level, msg, loggerName, opt_time, opt_sequenceNumber) { + if(goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS) { + this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog.debug.LogRecord.nextSequenceNumber_++ + } + this.time_ = opt_time || goog.now(); + this.level_ = level; + this.msg_ = msg; + this.loggerName_ = loggerName; + delete this.exception_; + delete this.exceptionText_ +}; +goog.debug.LogRecord.prototype.getLoggerName = function() { + return this.loggerName_ +}; +goog.debug.LogRecord.prototype.getException = function() { + return this.exception_ +}; +goog.debug.LogRecord.prototype.setException = function(exception) { + this.exception_ = exception +}; +goog.debug.LogRecord.prototype.getExceptionText = function() { + return this.exceptionText_ +}; +goog.debug.LogRecord.prototype.setExceptionText = function(text) { + this.exceptionText_ = text +}; +goog.debug.LogRecord.prototype.setLoggerName = function(loggerName) { + this.loggerName_ = loggerName +}; +goog.debug.LogRecord.prototype.getLevel = function() { + return this.level_ +}; +goog.debug.LogRecord.prototype.setLevel = function(level) { + this.level_ = level +}; +goog.debug.LogRecord.prototype.getMessage = function() { + return this.msg_ +}; +goog.debug.LogRecord.prototype.setMessage = function(msg) { + this.msg_ = msg +}; +goog.debug.LogRecord.prototype.getMillis = function() { + return this.time_ +}; +goog.debug.LogRecord.prototype.setMillis = function(time) { + this.time_ = time +}; +goog.debug.LogRecord.prototype.getSequenceNumber = function() { + return this.sequenceNumber_ +}; +goog.provide("goog.debug.LogBuffer"); +goog.require("goog.asserts"); +goog.require("goog.debug.LogRecord"); +goog.debug.LogBuffer = function() { + goog.asserts.assert(goog.debug.LogBuffer.isBufferingEnabled(), "Cannot use goog.debug.LogBuffer without defining " + "goog.debug.LogBuffer.CAPACITY."); + this.clear() +}; +goog.debug.LogBuffer.getInstance = function() { + if(!goog.debug.LogBuffer.instance_) { + goog.debug.LogBuffer.instance_ = new goog.debug.LogBuffer + } + return goog.debug.LogBuffer.instance_ +}; +goog.debug.LogBuffer.CAPACITY = 0; +goog.debug.LogBuffer.prototype.buffer_; +goog.debug.LogBuffer.prototype.curIndex_; +goog.debug.LogBuffer.prototype.isFull_; +goog.debug.LogBuffer.prototype.addRecord = function(level, msg, loggerName) { + var curIndex = (this.curIndex_ + 1) % goog.debug.LogBuffer.CAPACITY; + this.curIndex_ = curIndex; + if(this.isFull_) { + var ret = this.buffer_[curIndex]; + ret.reset(level, msg, loggerName); + return ret + } + this.isFull_ = curIndex == goog.debug.LogBuffer.CAPACITY - 1; + return this.buffer_[curIndex] = new goog.debug.LogRecord(level, msg, loggerName) +}; +goog.debug.LogBuffer.isBufferingEnabled = function() { + return goog.debug.LogBuffer.CAPACITY > 0 +}; +goog.debug.LogBuffer.prototype.clear = function() { + this.buffer_ = new Array(goog.debug.LogBuffer.CAPACITY); + this.curIndex_ = -1; + this.isFull_ = false +}; +goog.debug.LogBuffer.prototype.forEachRecord = function(func) { + var buffer = this.buffer_; + if(!buffer[0]) { + return + } + var curIndex = this.curIndex_; + var i = this.isFull_ ? curIndex : -1; + do { + i = (i + 1) % goog.debug.LogBuffer.CAPACITY; + func(buffer[i]) + }while(i != curIndex) +}; +goog.provide("goog.debug.LogManager"); +goog.provide("goog.debug.Logger"); +goog.provide("goog.debug.Logger.Level"); +goog.require("goog.array"); +goog.require("goog.asserts"); +goog.require("goog.debug"); +goog.require("goog.debug.LogBuffer"); +goog.require("goog.debug.LogRecord"); +goog.debug.Logger = function(name) { + this.name_ = name +}; +goog.debug.Logger.prototype.parent_ = null; +goog.debug.Logger.prototype.level_ = null; +goog.debug.Logger.prototype.children_ = null; +goog.debug.Logger.prototype.handlers_ = null; +goog.debug.Logger.ENABLE_HIERARCHY = true; +if(!goog.debug.Logger.ENABLE_HIERARCHY) { + goog.debug.Logger.rootHandlers_ = []; + goog.debug.Logger.rootLevel_ +} +goog.debug.Logger.Level = function(name, value) { + this.name = name; + this.value = value +}; +goog.debug.Logger.Level.prototype.toString = function() { + return this.name +}; +goog.debug.Logger.Level.OFF = new goog.debug.Logger.Level("OFF", Infinity); +goog.debug.Logger.Level.SHOUT = new goog.debug.Logger.Level("SHOUT", 1200); +goog.debug.Logger.Level.SEVERE = new goog.debug.Logger.Level("SEVERE", 1E3); +goog.debug.Logger.Level.WARNING = new goog.debug.Logger.Level("WARNING", 900); +goog.debug.Logger.Level.INFO = new goog.debug.Logger.Level("INFO", 800); +goog.debug.Logger.Level.CONFIG = new goog.debug.Logger.Level("CONFIG", 700); +goog.debug.Logger.Level.FINE = new goog.debug.Logger.Level("FINE", 500); +goog.debug.Logger.Level.FINER = new goog.debug.Logger.Level("FINER", 400); +goog.debug.Logger.Level.FINEST = new goog.debug.Logger.Level("FINEST", 300); +goog.debug.Logger.Level.ALL = new goog.debug.Logger.Level("ALL", 0); +goog.debug.Logger.Level.PREDEFINED_LEVELS = [goog.debug.Logger.Level.OFF, goog.debug.Logger.Level.SHOUT, goog.debug.Logger.Level.SEVERE, goog.debug.Logger.Level.WARNING, goog.debug.Logger.Level.INFO, goog.debug.Logger.Level.CONFIG, goog.debug.Logger.Level.FINE, goog.debug.Logger.Level.FINER, goog.debug.Logger.Level.FINEST, goog.debug.Logger.Level.ALL]; +goog.debug.Logger.Level.predefinedLevelsCache_ = null; +goog.debug.Logger.Level.createPredefinedLevelsCache_ = function() { + goog.debug.Logger.Level.predefinedLevelsCache_ = {}; + for(var i = 0, level;level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i];i++) { + goog.debug.Logger.Level.predefinedLevelsCache_[level.value] = level; + goog.debug.Logger.Level.predefinedLevelsCache_[level.name] = level + } +}; +goog.debug.Logger.Level.getPredefinedLevel = function(name) { + if(!goog.debug.Logger.Level.predefinedLevelsCache_) { + goog.debug.Logger.Level.createPredefinedLevelsCache_() + } + return goog.debug.Logger.Level.predefinedLevelsCache_[name] || null +}; +goog.debug.Logger.Level.getPredefinedLevelByValue = function(value) { + if(!goog.debug.Logger.Level.predefinedLevelsCache_) { + goog.debug.Logger.Level.createPredefinedLevelsCache_() + } + if(value in goog.debug.Logger.Level.predefinedLevelsCache_) { + return goog.debug.Logger.Level.predefinedLevelsCache_[value] + } + for(var i = 0;i < goog.debug.Logger.Level.PREDEFINED_LEVELS.length;++i) { + var level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i]; + if(level.value <= value) { + return level + } + } + return null +}; +goog.debug.Logger.getLogger = function(name) { + return goog.debug.LogManager.getLogger(name) +}; +goog.debug.Logger.prototype.getName = function() { + return this.name_ +}; +goog.debug.Logger.prototype.addHandler = function(handler) { + if(goog.debug.Logger.ENABLE_HIERARCHY) { + if(!this.handlers_) { + this.handlers_ = [] + } + this.handlers_.push(handler) + }else { + goog.asserts.assert(!this.name_, "Cannot call addHandler on a non-root logger when " + "goog.debug.Logger.ENABLE_HIERARCHY is false."); + goog.debug.Logger.rootHandlers_.push(handler) + } +}; +goog.debug.Logger.prototype.removeHandler = function(handler) { + var handlers = goog.debug.Logger.ENABLE_HIERARCHY ? this.handlers_ : goog.debug.Logger.rootHandlers_; + return!!handlers && goog.array.remove(handlers, handler) +}; +goog.debug.Logger.prototype.getParent = function() { + return this.parent_ +}; +goog.debug.Logger.prototype.getChildren = function() { + if(!this.children_) { + this.children_ = {} + } + return this.children_ +}; +goog.debug.Logger.prototype.setLevel = function(level) { + if(goog.debug.Logger.ENABLE_HIERARCHY) { + this.level_ = level + }else { + goog.asserts.assert(!this.name_, "Cannot call setLevel() on a non-root logger when " + "goog.debug.Logger.ENABLE_HIERARCHY is false."); + goog.debug.Logger.rootLevel_ = level + } +}; +goog.debug.Logger.prototype.getLevel = function() { + return this.level_ +}; +goog.debug.Logger.prototype.getEffectiveLevel = function() { + if(!goog.debug.Logger.ENABLE_HIERARCHY) { + return goog.debug.Logger.rootLevel_ + } + if(this.level_) { + return this.level_ + } + if(this.parent_) { + return this.parent_.getEffectiveLevel() + } + goog.asserts.fail("Root logger has no level set."); + return null +}; +goog.debug.Logger.prototype.isLoggable = function(level) { + return level.value >= this.getEffectiveLevel().value +}; +goog.debug.Logger.prototype.log = function(level, msg, opt_exception) { + if(this.isLoggable(level)) { + this.doLogRecord_(this.getLogRecord(level, msg, opt_exception)) + } +}; +goog.debug.Logger.prototype.getLogRecord = function(level, msg, opt_exception) { + if(goog.debug.LogBuffer.isBufferingEnabled()) { + var logRecord = goog.debug.LogBuffer.getInstance().addRecord(level, msg, this.name_) + }else { + logRecord = new goog.debug.LogRecord(level, String(msg), this.name_) + } + if(opt_exception) { + logRecord.setException(opt_exception); + logRecord.setExceptionText(goog.debug.exposeException(opt_exception, arguments.callee.caller)) + } + return logRecord +}; +goog.debug.Logger.prototype.shout = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.SHOUT, msg, opt_exception) +}; +goog.debug.Logger.prototype.severe = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.SEVERE, msg, opt_exception) +}; +goog.debug.Logger.prototype.warning = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.WARNING, msg, opt_exception) +}; +goog.debug.Logger.prototype.info = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.INFO, msg, opt_exception) +}; +goog.debug.Logger.prototype.config = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.CONFIG, msg, opt_exception) +}; +goog.debug.Logger.prototype.fine = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.FINE, msg, opt_exception) +}; +goog.debug.Logger.prototype.finer = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.FINER, msg, opt_exception) +}; +goog.debug.Logger.prototype.finest = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.FINEST, msg, opt_exception) +}; +goog.debug.Logger.prototype.logRecord = function(logRecord) { + if(this.isLoggable(logRecord.getLevel())) { + this.doLogRecord_(logRecord) + } +}; +goog.debug.Logger.prototype.doLogRecord_ = function(logRecord) { + if(goog.debug.Logger.ENABLE_HIERARCHY) { + var target = this; + while(target) { + target.callPublish_(logRecord); + target = target.getParent() + } + }else { + for(var i = 0, handler;handler = goog.debug.Logger.rootHandlers_[i++];) { + handler(logRecord) + } + } +}; +goog.debug.Logger.prototype.callPublish_ = function(logRecord) { + if(this.handlers_) { + for(var i = 0, handler;handler = this.handlers_[i];i++) { + handler(logRecord) + } + } +}; +goog.debug.Logger.prototype.setParent_ = function(parent) { + this.parent_ = parent +}; +goog.debug.Logger.prototype.addChild_ = function(name, logger) { + this.getChildren()[name] = logger +}; +goog.debug.LogManager = {}; +goog.debug.LogManager.loggers_ = {}; +goog.debug.LogManager.rootLogger_ = null; +goog.debug.LogManager.initialize = function() { + if(!goog.debug.LogManager.rootLogger_) { + goog.debug.LogManager.rootLogger_ = new goog.debug.Logger(""); + goog.debug.LogManager.loggers_[""] = goog.debug.LogManager.rootLogger_; + goog.debug.LogManager.rootLogger_.setLevel(goog.debug.Logger.Level.CONFIG) + } +}; +goog.debug.LogManager.getLoggers = function() { + return goog.debug.LogManager.loggers_ +}; +goog.debug.LogManager.getRoot = function() { + goog.debug.LogManager.initialize(); + return goog.debug.LogManager.rootLogger_ +}; +goog.debug.LogManager.getLogger = function(name) { + goog.debug.LogManager.initialize(); + var ret = goog.debug.LogManager.loggers_[name]; + return ret || goog.debug.LogManager.createLogger_(name) +}; +goog.debug.LogManager.createFunctionForCatchErrors = function(opt_logger) { + return function(info) { + var logger = opt_logger || goog.debug.LogManager.getRoot(); + logger.severe("Error: " + info.message + " (" + info.fileName + " @ Line: " + info.line + ")") + } +}; +goog.debug.LogManager.createLogger_ = function(name) { + var logger = new goog.debug.Logger(name); + if(goog.debug.Logger.ENABLE_HIERARCHY) { + var lastDotIndex = name.lastIndexOf("."); + var parentName = name.substr(0, lastDotIndex); + var leafName = name.substr(lastDotIndex + 1); + var parentLogger = goog.debug.LogManager.getLogger(parentName); + parentLogger.addChild_(leafName, logger); + logger.setParent_(parentLogger) + } + goog.debug.LogManager.loggers_[name] = logger; + return logger +}; +goog.provide("goog.dom.SavedRange"); +goog.require("goog.Disposable"); +goog.require("goog.debug.Logger"); +goog.dom.SavedRange = function() { + goog.Disposable.call(this) +}; +goog.inherits(goog.dom.SavedRange, goog.Disposable); +goog.dom.SavedRange.logger_ = goog.debug.Logger.getLogger("goog.dom.SavedRange"); +goog.dom.SavedRange.prototype.restore = function(opt_stayAlive) { + if(this.isDisposed()) { + goog.dom.SavedRange.logger_.severe("Disposed SavedRange objects cannot be restored.") + } + var range = this.restoreInternal(); + if(!opt_stayAlive) { + this.dispose() + } + return range +}; +goog.dom.SavedRange.prototype.restoreInternal = goog.abstractMethod; +goog.provide("goog.dom.SavedCaretRange"); +goog.require("goog.array"); +goog.require("goog.dom"); +goog.require("goog.dom.SavedRange"); +goog.require("goog.dom.TagName"); +goog.require("goog.string"); +goog.dom.SavedCaretRange = function(range) { + goog.dom.SavedRange.call(this); + this.startCaretId_ = goog.string.createUniqueString(); + this.endCaretId_ = goog.string.createUniqueString(); + this.dom_ = goog.dom.getDomHelper(range.getDocument()); + range.surroundWithNodes(this.createCaret_(true), this.createCaret_(false)) +}; +goog.inherits(goog.dom.SavedCaretRange, goog.dom.SavedRange); +goog.dom.SavedCaretRange.prototype.toAbstractRange = function() { + var range = null; + var startCaret = this.getCaret(true); + var endCaret = this.getCaret(false); + if(startCaret && endCaret) { + range = goog.dom.Range.createFromNodes(startCaret, 0, endCaret, 0) + } + return range +}; +goog.dom.SavedCaretRange.prototype.getCaret = function(start) { + return this.dom_.getElement(start ? this.startCaretId_ : this.endCaretId_) +}; +goog.dom.SavedCaretRange.prototype.removeCarets = function(opt_range) { + goog.dom.removeNode(this.getCaret(true)); + goog.dom.removeNode(this.getCaret(false)); + return opt_range +}; +goog.dom.SavedCaretRange.prototype.setRestorationDocument = function(doc) { + this.dom_.setDocument(doc) +}; +goog.dom.SavedCaretRange.prototype.restoreInternal = function() { + var range = null; + var startCaret = this.getCaret(true); + var endCaret = this.getCaret(false); + if(startCaret && endCaret) { + var startNode = startCaret.parentNode; + var startOffset = goog.array.indexOf(startNode.childNodes, startCaret); + var endNode = endCaret.parentNode; + var endOffset = goog.array.indexOf(endNode.childNodes, endCaret); + if(endNode == startNode) { + endOffset -= 1 + } + range = goog.dom.Range.createFromNodes(startNode, startOffset, endNode, endOffset); + range = this.removeCarets(range); + range.select() + }else { + this.removeCarets() + } + return range +}; +goog.dom.SavedCaretRange.prototype.disposeInternal = function() { + this.removeCarets(); + this.dom_ = null +}; +goog.dom.SavedCaretRange.prototype.createCaret_ = function(start) { + return this.dom_.createDom(goog.dom.TagName.SPAN, {id:start ? this.startCaretId_ : this.endCaretId_}) +}; +goog.dom.SavedCaretRange.CARET_REGEX = /<span\s+id="?goog_\d+"?><\/span>/ig; +goog.dom.SavedCaretRange.htmlEqual = function(str1, str2) { + return str1 == str2 || str1.replace(goog.dom.SavedCaretRange.CARET_REGEX, "") == str2.replace(goog.dom.SavedCaretRange.CARET_REGEX, "") +}; +goog.provide("goog.dom.TagIterator"); +goog.provide("goog.dom.TagWalkType"); +goog.require("goog.dom.NodeType"); +goog.require("goog.iter.Iterator"); +goog.require("goog.iter.StopIteration"); +goog.dom.TagWalkType = {START_TAG:1, OTHER:0, END_TAG:-1}; +goog.dom.TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) { + this.reversed = !!opt_reversed; + if(opt_node) { + this.setPosition(opt_node, opt_tagType) + } + this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0; + if(this.reversed) { + this.depth *= -1 + } + this.constrained = !opt_unconstrained +}; +goog.inherits(goog.dom.TagIterator, goog.iter.Iterator); +goog.dom.TagIterator.prototype.node = null; +goog.dom.TagIterator.prototype.tagType = goog.dom.TagWalkType.OTHER; +goog.dom.TagIterator.prototype.depth; +goog.dom.TagIterator.prototype.reversed; +goog.dom.TagIterator.prototype.constrained; +goog.dom.TagIterator.prototype.started_ = false; +goog.dom.TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) { + this.node = node; + if(node) { + if(goog.isNumber(opt_tagType)) { + this.tagType = opt_tagType + }else { + this.tagType = this.node.nodeType != goog.dom.NodeType.ELEMENT ? goog.dom.TagWalkType.OTHER : this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG + } + } + if(goog.isNumber(opt_depth)) { + this.depth = opt_depth + } +}; +goog.dom.TagIterator.prototype.copyFrom = function(other) { + this.node = other.node; + this.tagType = other.tagType; + this.depth = other.depth; + this.reversed = other.reversed; + this.constrained = other.constrained +}; +goog.dom.TagIterator.prototype.clone = function() { + return new goog.dom.TagIterator(this.node, this.reversed, !this.constrained, this.tagType, this.depth) +}; +goog.dom.TagIterator.prototype.skipTag = function() { + var check = this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG; + if(this.tagType == check) { + this.tagType = check * -1; + this.depth += this.tagType * (this.reversed ? -1 : 1) + } +}; +goog.dom.TagIterator.prototype.restartTag = function() { + var check = this.reversed ? goog.dom.TagWalkType.START_TAG : goog.dom.TagWalkType.END_TAG; + if(this.tagType == check) { + this.tagType = check * -1; + this.depth += this.tagType * (this.reversed ? -1 : 1) + } +}; +goog.dom.TagIterator.prototype.next = function() { + var node; + if(this.started_) { + if(!this.node || this.constrained && this.depth == 0) { + throw goog.iter.StopIteration; + } + node = this.node; + var startType = this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG; + if(this.tagType == startType) { + var child = this.reversed ? node.lastChild : node.firstChild; + if(child) { + this.setPosition(child) + }else { + this.setPosition(node, startType * -1) + } + }else { + var sibling = this.reversed ? node.previousSibling : node.nextSibling; + if(sibling) { + this.setPosition(sibling) + }else { + this.setPosition(node.parentNode, startType * -1) + } + } + this.depth += this.tagType * (this.reversed ? -1 : 1) + }else { + this.started_ = true + } + node = this.node; + if(!this.node) { + throw goog.iter.StopIteration; + } + return node +}; +goog.dom.TagIterator.prototype.isStarted = function() { + return this.started_ +}; +goog.dom.TagIterator.prototype.isStartTag = function() { + return this.tagType == goog.dom.TagWalkType.START_TAG +}; +goog.dom.TagIterator.prototype.isEndTag = function() { + return this.tagType == goog.dom.TagWalkType.END_TAG +}; +goog.dom.TagIterator.prototype.isNonElement = function() { + return this.tagType == goog.dom.TagWalkType.OTHER +}; +goog.dom.TagIterator.prototype.equals = function(other) { + return other.node == this.node && (!this.node || other.tagType == this.tagType) +}; +goog.dom.TagIterator.prototype.splice = function(var_args) { + var node = this.node; + this.restartTag(); + this.reversed = !this.reversed; + goog.dom.TagIterator.prototype.next.call(this); + this.reversed = !this.reversed; + var arr = goog.isArrayLike(arguments[0]) ? arguments[0] : arguments; + for(var i = arr.length - 1;i >= 0;i--) { + goog.dom.insertSiblingAfter(arr[i], node) + } + goog.dom.removeNode(node) +}; +goog.provide("goog.dom.AbstractRange"); +goog.provide("goog.dom.RangeIterator"); +goog.provide("goog.dom.RangeType"); +goog.require("goog.dom"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.SavedCaretRange"); +goog.require("goog.dom.TagIterator"); +goog.require("goog.userAgent"); +goog.dom.RangeType = {TEXT:"text", CONTROL:"control", MULTI:"mutli"}; +goog.dom.AbstractRange = function() { +}; +goog.dom.AbstractRange.getBrowserSelectionForWindow = function(win) { + if(win.getSelection) { + return win.getSelection() + }else { + var doc = win.document; + var sel = doc.selection; + if(sel) { + try { + var range = sel.createRange(); + if(range.parentElement) { + if(range.parentElement().document != doc) { + return null + } + }else { + if(!range.length || range.item(0).document != doc) { + return null + } + } + }catch(e) { + return null + } + return sel + } + return null + } +}; +goog.dom.AbstractRange.isNativeControlRange = function(range) { + return!!range && !!range.addElement +}; +goog.dom.AbstractRange.prototype.clone = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getType = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getBrowserRangeObject = goog.abstractMethod; +goog.dom.AbstractRange.prototype.setBrowserRangeObject = function(nativeRange) { + return false +}; +goog.dom.AbstractRange.prototype.getTextRangeCount = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getTextRange = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getTextRanges = function() { + var output = []; + for(var i = 0, len = this.getTextRangeCount();i < len;i++) { + output.push(this.getTextRange(i)) + } + return output +}; +goog.dom.AbstractRange.prototype.getContainer = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getContainerElement = function() { + var node = this.getContainer(); + return node.nodeType == goog.dom.NodeType.ELEMENT ? node : node.parentNode +}; +goog.dom.AbstractRange.prototype.getStartNode = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getStartOffset = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getEndNode = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getEndOffset = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getAnchorNode = function() { + return this.isReversed() ? this.getEndNode() : this.getStartNode() +}; +goog.dom.AbstractRange.prototype.getAnchorOffset = function() { + return this.isReversed() ? this.getEndOffset() : this.getStartOffset() +}; +goog.dom.AbstractRange.prototype.getFocusNode = function() { + return this.isReversed() ? this.getStartNode() : this.getEndNode() +}; +goog.dom.AbstractRange.prototype.getFocusOffset = function() { + return this.isReversed() ? this.getStartOffset() : this.getEndOffset() +}; +goog.dom.AbstractRange.prototype.isReversed = function() { + return false +}; +goog.dom.AbstractRange.prototype.getDocument = function() { + return goog.dom.getOwnerDocument(goog.userAgent.IE ? this.getContainer() : this.getStartNode()) +}; +goog.dom.AbstractRange.prototype.getWindow = function() { + return goog.dom.getWindow(this.getDocument()) +}; +goog.dom.AbstractRange.prototype.containsRange = goog.abstractMethod; +goog.dom.AbstractRange.prototype.containsNode = function(node, opt_allowPartial) { + return this.containsRange(goog.dom.Range.createFromNodeContents(node), opt_allowPartial) +}; +goog.dom.AbstractRange.prototype.isRangeInDocument = goog.abstractMethod; +goog.dom.AbstractRange.prototype.isCollapsed = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getText = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getHtmlFragment = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getValidHtml = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getPastableHtml = goog.abstractMethod; +goog.dom.AbstractRange.prototype.__iterator__ = goog.abstractMethod; +goog.dom.AbstractRange.prototype.select = goog.abstractMethod; +goog.dom.AbstractRange.prototype.removeContents = goog.abstractMethod; +goog.dom.AbstractRange.prototype.insertNode = goog.abstractMethod; +goog.dom.AbstractRange.prototype.replaceContentsWithNode = function(node) { + if(!this.isCollapsed()) { + this.removeContents() + } + return this.insertNode(node, true) +}; +goog.dom.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod; +goog.dom.AbstractRange.prototype.saveUsingDom = goog.abstractMethod; +goog.dom.AbstractRange.prototype.saveUsingCarets = function() { + return this.getStartNode() && this.getEndNode() ? new goog.dom.SavedCaretRange(this) : null +}; +goog.dom.AbstractRange.prototype.collapse = goog.abstractMethod; +goog.dom.RangeIterator = function(node, opt_reverse) { + goog.dom.TagIterator.call(this, node, opt_reverse, true) +}; +goog.inherits(goog.dom.RangeIterator, goog.dom.TagIterator); +goog.dom.RangeIterator.prototype.getStartTextOffset = goog.abstractMethod; +goog.dom.RangeIterator.prototype.getEndTextOffset = goog.abstractMethod; +goog.dom.RangeIterator.prototype.getStartNode = goog.abstractMethod; +goog.dom.RangeIterator.prototype.getEndNode = goog.abstractMethod; +goog.dom.RangeIterator.prototype.isLast = goog.abstractMethod; +goog.provide("goog.dom.AbstractMultiRange"); +goog.require("goog.array"); +goog.require("goog.dom"); +goog.require("goog.dom.AbstractRange"); +goog.dom.AbstractMultiRange = function() { +}; +goog.inherits(goog.dom.AbstractMultiRange, goog.dom.AbstractRange); +goog.dom.AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) { + var ranges = this.getTextRanges(); + var otherRanges = otherRange.getTextRanges(); + var fn = opt_allowPartial ? goog.array.some : goog.array.every; + return fn(otherRanges, function(otherRange) { + return goog.array.some(ranges, function(range) { + return range.containsRange(otherRange, opt_allowPartial) + }) + }) +}; +goog.dom.AbstractMultiRange.prototype.insertNode = function(node, before) { + if(before) { + goog.dom.insertSiblingBefore(node, this.getStartNode()) + }else { + goog.dom.insertSiblingAfter(node, this.getEndNode()) + } + return node +}; +goog.dom.AbstractMultiRange.prototype.surroundWithNodes = function(startNode, endNode) { + this.insertNode(startNode, true); + this.insertNode(endNode, false) +}; +goog.provide("goog.dom.TextRangeIterator"); +goog.require("goog.array"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.RangeIterator"); +goog.require("goog.dom.TagName"); +goog.require("goog.iter.StopIteration"); +goog.dom.TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) { + var goNext; + if(startNode) { + this.startNode_ = startNode; + this.startOffset_ = startOffset; + this.endNode_ = endNode; + this.endOffset_ = endOffset; + if(startNode.nodeType == goog.dom.NodeType.ELEMENT && startNode.tagName != goog.dom.TagName.BR) { + var startChildren = startNode.childNodes; + var candidate = startChildren[startOffset]; + if(candidate) { + this.startNode_ = candidate; + this.startOffset_ = 0 + }else { + if(startChildren.length) { + this.startNode_ = goog.array.peek(startChildren) + } + goNext = true + } + } + if(endNode.nodeType == goog.dom.NodeType.ELEMENT) { + this.endNode_ = endNode.childNodes[endOffset]; + if(this.endNode_) { + this.endOffset_ = 0 + }else { + this.endNode_ = endNode + } + } + } + goog.dom.RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse); + if(goNext) { + try { + this.next() + }catch(e) { + if(e != goog.iter.StopIteration) { + throw e; + } + } + } +}; +goog.inherits(goog.dom.TextRangeIterator, goog.dom.RangeIterator); +goog.dom.TextRangeIterator.prototype.startNode_ = null; +goog.dom.TextRangeIterator.prototype.endNode_ = null; +goog.dom.TextRangeIterator.prototype.startOffset_ = 0; +goog.dom.TextRangeIterator.prototype.endOffset_ = 0; +goog.dom.TextRangeIterator.prototype.getStartTextOffset = function() { + return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.startNode_ ? this.startOffset_ : 0 +}; +goog.dom.TextRangeIterator.prototype.getEndTextOffset = function() { + return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.endNode_ ? this.endOffset_ : this.node.nodeValue.length +}; +goog.dom.TextRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog.dom.TextRangeIterator.prototype.setStartNode = function(node) { + if(!this.isStarted()) { + this.setPosition(node) + } + this.startNode_ = node; + this.startOffset_ = 0 +}; +goog.dom.TextRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog.dom.TextRangeIterator.prototype.setEndNode = function(node) { + this.endNode_ = node; + this.endOffset_ = 0 +}; +goog.dom.TextRangeIterator.prototype.isLast = function() { + return this.isStarted() && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag()) +}; +goog.dom.TextRangeIterator.prototype.next = function() { + if(this.isLast()) { + throw goog.iter.StopIteration; + } + return goog.dom.TextRangeIterator.superClass_.next.call(this) +}; +goog.dom.TextRangeIterator.prototype.skipTag = function() { + goog.dom.TextRangeIterator.superClass_.skipTag.apply(this); + if(goog.dom.contains(this.node, this.endNode_)) { + throw goog.iter.StopIteration; + } +}; +goog.dom.TextRangeIterator.prototype.copyFrom = function(other) { + this.startNode_ = other.startNode_; + this.endNode_ = other.endNode_; + this.startOffset_ = other.startOffset_; + this.endOffset_ = other.endOffset_; + this.isReversed_ = other.isReversed_; + goog.dom.TextRangeIterator.superClass_.copyFrom.call(this, other) +}; +goog.dom.TextRangeIterator.prototype.clone = function() { + var copy = new goog.dom.TextRangeIterator(this.startNode_, this.startOffset_, this.endNode_, this.endOffset_, this.isReversed_); + copy.copyFrom(this); + return copy +}; +goog.provide("goog.dom.RangeEndpoint"); +goog.dom.RangeEndpoint = {START:1, END:0}; +goog.provide("goog.userAgent.jscript"); +goog.require("goog.string"); +goog.userAgent.jscript.ASSUME_NO_JSCRIPT = false; +goog.userAgent.jscript.init_ = function() { + var hasScriptEngine = "ScriptEngine" in goog.global; + goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_ = hasScriptEngine && goog.global["ScriptEngine"]() == "JScript"; + goog.userAgent.jscript.DETECTED_VERSION_ = goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_ ? goog.global["ScriptEngineMajorVersion"]() + "." + goog.global["ScriptEngineMinorVersion"]() + "." + goog.global["ScriptEngineBuildVersion"]() : "0" +}; +if(!goog.userAgent.jscript.ASSUME_NO_JSCRIPT) { + goog.userAgent.jscript.init_() +} +goog.userAgent.jscript.HAS_JSCRIPT = goog.userAgent.jscript.ASSUME_NO_JSCRIPT ? false : goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_; +goog.userAgent.jscript.VERSION = goog.userAgent.jscript.ASSUME_NO_JSCRIPT ? "0" : goog.userAgent.jscript.DETECTED_VERSION_; +goog.userAgent.jscript.isVersion = function(version) { + return goog.string.compareVersions(goog.userAgent.jscript.VERSION, version) >= 0 +}; +goog.provide("goog.string.StringBuffer"); +goog.require("goog.userAgent.jscript"); +goog.string.StringBuffer = function(opt_a1, var_args) { + this.buffer_ = goog.userAgent.jscript.HAS_JSCRIPT ? [] : ""; + if(opt_a1 != null) { + this.append.apply(this, arguments) + } +}; +goog.string.StringBuffer.prototype.set = function(s) { + this.clear(); + this.append(s) +}; +if(goog.userAgent.jscript.HAS_JSCRIPT) { + goog.string.StringBuffer.prototype.bufferLength_ = 0; + goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) { + if(opt_a2 == null) { + this.buffer_[this.bufferLength_++] = a1 + }else { + this.buffer_.push.apply(this.buffer_, arguments); + this.bufferLength_ = this.buffer_.length + } + return this + } +}else { + goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) { + this.buffer_ += a1; + if(opt_a2 != null) { + for(var i = 1;i < arguments.length;i++) { + this.buffer_ += arguments[i] + } + } + return this + } +} +goog.string.StringBuffer.prototype.clear = function() { + if(goog.userAgent.jscript.HAS_JSCRIPT) { + this.buffer_.length = 0; + this.bufferLength_ = 0 + }else { + this.buffer_ = "" + } +}; +goog.string.StringBuffer.prototype.getLength = function() { + return this.toString().length +}; +goog.string.StringBuffer.prototype.toString = function() { + if(goog.userAgent.jscript.HAS_JSCRIPT) { + var str = this.buffer_.join(""); + this.clear(); + if(str) { + this.append(str) + } + return str + }else { + return this.buffer_ + } +}; +goog.provide("goog.dom.browserrange.AbstractRange"); +goog.require("goog.dom"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.RangeEndpoint"); +goog.require("goog.dom.TagName"); +goog.require("goog.dom.TextRangeIterator"); +goog.require("goog.iter"); +goog.require("goog.string"); +goog.require("goog.string.StringBuffer"); +goog.require("goog.userAgent"); +goog.dom.browserrange.AbstractRange = function() { +}; +goog.dom.browserrange.AbstractRange.prototype.clone = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getBrowserRange = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getContainer = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getStartNode = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getStartOffset = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getEndNode = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getEndOffset = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.compareBrowserRangeEndpoints = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.containsRange = function(abstractRange, opt_allowPartial) { + var checkPartial = opt_allowPartial && !abstractRange.isCollapsed(); + var range = abstractRange.getBrowserRange(); + var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END; + try { + if(checkPartial) { + return this.compareBrowserRangeEndpoints(range, end, start) >= 0 && this.compareBrowserRangeEndpoints(range, start, end) <= 0 + }else { + return this.compareBrowserRangeEndpoints(range, end, end) >= 0 && this.compareBrowserRangeEndpoints(range, start, start) <= 0 + } + }catch(e) { + if(!goog.userAgent.IE) { + throw e; + } + return false + } +}; +goog.dom.browserrange.AbstractRange.prototype.containsNode = function(node, opt_allowPartial) { + return this.containsRange(goog.dom.browserrange.createRangeFromNodeContents(node), opt_allowPartial) +}; +goog.dom.browserrange.AbstractRange.prototype.isCollapsed = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getText = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getHtmlFragment = function() { + var output = new goog.string.StringBuffer; + goog.iter.forEach(this, function(node, ignore, it) { + if(node.nodeType == goog.dom.NodeType.TEXT) { + output.append(goog.string.htmlEscape(node.nodeValue.substring(it.getStartTextOffset(), it.getEndTextOffset()))) + }else { + if(node.nodeType == goog.dom.NodeType.ELEMENT) { + if(it.isEndTag()) { + if(goog.dom.canHaveChildren(node)) { + output.append("</" + node.tagName + ">") + } + }else { + var shallow = node.cloneNode(false); + var html = goog.dom.getOuterHtml(shallow); + if(goog.userAgent.IE && node.tagName == goog.dom.TagName.LI) { + output.append(html) + }else { + var index = html.lastIndexOf("<"); + output.append(index ? html.substr(0, index) : html) + } + } + } + } + }, this); + return output.toString() +}; +goog.dom.browserrange.AbstractRange.prototype.getValidHtml = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.__iterator__ = function(opt_keys) { + return new goog.dom.TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +}; +goog.dom.browserrange.AbstractRange.prototype.select = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.removeContents = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.surroundContents = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.insertNode = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.collapse = goog.abstractMethod; +goog.provide("goog.dom.browserrange.W3cRange"); +goog.require("goog.dom"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.RangeEndpoint"); +goog.require("goog.dom.browserrange.AbstractRange"); +goog.require("goog.string"); +goog.dom.browserrange.W3cRange = function(range) { + this.range_ = range +}; +goog.inherits(goog.dom.browserrange.W3cRange, goog.dom.browserrange.AbstractRange); +goog.dom.browserrange.W3cRange.getBrowserRangeForNode = function(node) { + var nodeRange = goog.dom.getOwnerDocument(node).createRange(); + if(node.nodeType == goog.dom.NodeType.TEXT) { + nodeRange.setStart(node, 0); + nodeRange.setEnd(node, node.length) + }else { + if(!goog.dom.browserrange.canContainRangeEndpoint(node)) { + var rangeParent = node.parentNode; + var rangeStartOffset = goog.array.indexOf(rangeParent.childNodes, node); + nodeRange.setStart(rangeParent, rangeStartOffset); + nodeRange.setEnd(rangeParent, rangeStartOffset + 1) + }else { + var tempNode, leaf = node; + while((tempNode = leaf.firstChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) { + leaf = tempNode + } + nodeRange.setStart(leaf, 0); + leaf = node; + while((tempNode = leaf.lastChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) { + leaf = tempNode + } + nodeRange.setEnd(leaf, leaf.nodeType == goog.dom.NodeType.ELEMENT ? leaf.childNodes.length : leaf.length) + } + } + return nodeRange +}; +goog.dom.browserrange.W3cRange.getBrowserRangeForNodes = function(startNode, startOffset, endNode, endOffset) { + var nodeRange = goog.dom.getOwnerDocument(startNode).createRange(); + nodeRange.setStart(startNode, startOffset); + nodeRange.setEnd(endNode, endOffset); + return nodeRange +}; +goog.dom.browserrange.W3cRange.createFromNodeContents = function(node) { + return new goog.dom.browserrange.W3cRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node)) +}; +goog.dom.browserrange.W3cRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return new goog.dom.browserrange.W3cRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset)) +}; +goog.dom.browserrange.W3cRange.prototype.clone = function() { + return new this.constructor(this.range_.cloneRange()) +}; +goog.dom.browserrange.W3cRange.prototype.getBrowserRange = function() { + return this.range_ +}; +goog.dom.browserrange.W3cRange.prototype.getContainer = function() { + return this.range_.commonAncestorContainer +}; +goog.dom.browserrange.W3cRange.prototype.getStartNode = function() { + return this.range_.startContainer +}; +goog.dom.browserrange.W3cRange.prototype.getStartOffset = function() { + return this.range_.startOffset +}; +goog.dom.browserrange.W3cRange.prototype.getEndNode = function() { + return this.range_.endContainer +}; +goog.dom.browserrange.W3cRange.prototype.getEndOffset = function() { + return this.range_.endOffset +}; +goog.dom.browserrange.W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + return this.range_.compareBoundaryPoints(otherEndpoint == goog.dom.RangeEndpoint.START ? thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_START : goog.global["Range"].START_TO_END : thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].END_TO_START : goog.global["Range"].END_TO_END, range) +}; +goog.dom.browserrange.W3cRange.prototype.isCollapsed = function() { + return this.range_.collapsed +}; +goog.dom.browserrange.W3cRange.prototype.getText = function() { + return this.range_.toString() +}; +goog.dom.browserrange.W3cRange.prototype.getValidHtml = function() { + var div = goog.dom.getDomHelper(this.range_.startContainer).createDom("div"); + div.appendChild(this.range_.cloneContents()); + var result = div.innerHTML; + if(goog.string.startsWith(result, "<") || !this.isCollapsed() && !goog.string.contains(result, "<")) { + return result + } + var container = this.getContainer(); + container = container.nodeType == goog.dom.NodeType.ELEMENT ? container : container.parentNode; + var html = goog.dom.getOuterHtml(container.cloneNode(false)); + return html.replace(">", ">" + result) +}; +goog.dom.browserrange.W3cRange.prototype.select = function(reverse) { + var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode())); + this.selectInternal(win.getSelection(), reverse) +}; +goog.dom.browserrange.W3cRange.prototype.selectInternal = function(selection, reverse) { + selection.removeAllRanges(); + selection.addRange(this.range_) +}; +goog.dom.browserrange.W3cRange.prototype.removeContents = function() { + var range = this.range_; + range.extractContents(); + if(range.startContainer.hasChildNodes()) { + var rangeStartContainer = range.startContainer.childNodes[range.startOffset]; + if(rangeStartContainer) { + var rangePrevious = rangeStartContainer.previousSibling; + if(goog.dom.getRawTextContent(rangeStartContainer) == "") { + goog.dom.removeNode(rangeStartContainer) + } + if(rangePrevious && goog.dom.getRawTextContent(rangePrevious) == "") { + goog.dom.removeNode(rangePrevious) + } + } + } +}; +goog.dom.browserrange.W3cRange.prototype.surroundContents = function(element) { + this.range_.surroundContents(element); + return element +}; +goog.dom.browserrange.W3cRange.prototype.insertNode = function(node, before) { + var range = this.range_.cloneRange(); + range.collapse(before); + range.insertNode(node); + range.detach(); + return node +}; +goog.dom.browserrange.W3cRange.prototype.surroundWithNodes = function(startNode, endNode) { + var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode())); + var selectionRange = goog.dom.Range.createFromWindow(win); + if(selectionRange) { + var sNode = selectionRange.getStartNode(); + var eNode = selectionRange.getEndNode(); + var sOffset = selectionRange.getStartOffset(); + var eOffset = selectionRange.getEndOffset() + } + var clone1 = this.range_.cloneRange(); + var clone2 = this.range_.cloneRange(); + clone1.collapse(false); + clone2.collapse(true); + clone1.insertNode(endNode); + clone2.insertNode(startNode); + clone1.detach(); + clone2.detach(); + if(selectionRange) { + var isInsertedNode = function(n) { + return n == startNode || n == endNode + }; + if(sNode.nodeType == goog.dom.NodeType.TEXT) { + while(sOffset > sNode.length) { + sOffset -= sNode.length; + do { + sNode = sNode.nextSibling + }while(isInsertedNode(sNode)) + } + } + if(eNode.nodeType == goog.dom.NodeType.TEXT) { + while(eOffset > eNode.length) { + eOffset -= eNode.length; + do { + eNode = eNode.nextSibling + }while(isInsertedNode(eNode)) + } + } + goog.dom.Range.createFromNodes(sNode, sOffset, eNode, eOffset).select() + } +}; +goog.dom.browserrange.W3cRange.prototype.collapse = function(toStart) { + this.range_.collapse(toStart) +}; +goog.provide("goog.dom.browserrange.GeckoRange"); +goog.require("goog.dom.browserrange.W3cRange"); +goog.dom.browserrange.GeckoRange = function(range) { + goog.dom.browserrange.W3cRange.call(this, range) +}; +goog.inherits(goog.dom.browserrange.GeckoRange, goog.dom.browserrange.W3cRange); +goog.dom.browserrange.GeckoRange.createFromNodeContents = function(node) { + return new goog.dom.browserrange.GeckoRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node)) +}; +goog.dom.browserrange.GeckoRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return new goog.dom.browserrange.GeckoRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset)) +}; +goog.dom.browserrange.GeckoRange.prototype.selectInternal = function(selection, reversed) { + var anchorNode = reversed ? this.getEndNode() : this.getStartNode(); + var anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset(); + var focusNode = reversed ? this.getStartNode() : this.getEndNode(); + var focusOffset = reversed ? this.getStartOffset() : this.getEndOffset(); + selection.collapse(anchorNode, anchorOffset); + if(anchorNode != focusNode || anchorOffset != focusOffset) { + selection.extend(focusNode, focusOffset) + } +}; +goog.provide("goog.dom.NodeIterator"); +goog.require("goog.dom.TagIterator"); +goog.dom.NodeIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_depth) { + goog.dom.TagIterator.call(this, opt_node, opt_reversed, opt_unconstrained, null, opt_depth) +}; +goog.inherits(goog.dom.NodeIterator, goog.dom.TagIterator); +goog.dom.NodeIterator.prototype.next = function() { + do { + goog.dom.NodeIterator.superClass_.next.call(this) + }while(this.isEndTag()); + return this.node +}; +goog.provide("goog.dom.browserrange.IeRange"); +goog.require("goog.array"); +goog.require("goog.debug.Logger"); +goog.require("goog.dom"); +goog.require("goog.dom.NodeIterator"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.RangeEndpoint"); +goog.require("goog.dom.TagName"); +goog.require("goog.dom.browserrange.AbstractRange"); +goog.require("goog.iter"); +goog.require("goog.iter.StopIteration"); +goog.require("goog.string"); +goog.dom.browserrange.IeRange = function(range, doc) { + this.range_ = range; + this.doc_ = doc +}; +goog.inherits(goog.dom.browserrange.IeRange, goog.dom.browserrange.AbstractRange); +goog.dom.browserrange.IeRange.logger_ = goog.debug.Logger.getLogger("goog.dom.browserrange.IeRange"); +goog.dom.browserrange.IeRange.getBrowserRangeForNode_ = function(node) { + var nodeRange = goog.dom.getOwnerDocument(node).body.createTextRange(); + if(node.nodeType == goog.dom.NodeType.ELEMENT) { + nodeRange.moveToElementText(node); + if(goog.dom.browserrange.canContainRangeEndpoint(node) && !node.childNodes.length) { + nodeRange.collapse(false) + } + }else { + var offset = 0; + var sibling = node; + while(sibling = sibling.previousSibling) { + var nodeType = sibling.nodeType; + if(nodeType == goog.dom.NodeType.TEXT) { + offset += sibling.length + }else { + if(nodeType == goog.dom.NodeType.ELEMENT) { + nodeRange.moveToElementText(sibling); + break + } + } + } + if(!sibling) { + nodeRange.moveToElementText(node.parentNode) + } + nodeRange.collapse(!sibling); + if(offset) { + nodeRange.move("character", offset) + } + nodeRange.moveEnd("character", node.length) + } + return nodeRange +}; +goog.dom.browserrange.IeRange.getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) { + var child, collapse = false; + if(startNode.nodeType == goog.dom.NodeType.ELEMENT) { + if(startOffset > startNode.childNodes.length) { + goog.dom.browserrange.IeRange.logger_.severe("Cannot have startOffset > startNode child count") + } + child = startNode.childNodes[startOffset]; + collapse = !child; + startNode = child || startNode.lastChild || startNode; + startOffset = 0 + } + var leftRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(startNode); + if(startOffset) { + leftRange.move("character", startOffset) + } + if(startNode == endNode && startOffset == endOffset) { + leftRange.collapse(true); + return leftRange + } + if(collapse) { + leftRange.collapse(false) + } + collapse = false; + if(endNode.nodeType == goog.dom.NodeType.ELEMENT) { + if(endOffset > endNode.childNodes.length) { + goog.dom.browserrange.IeRange.logger_.severe("Cannot have endOffset > endNode child count") + } + child = endNode.childNodes[endOffset]; + endNode = child || endNode.lastChild || endNode; + endOffset = 0; + collapse = !child + } + var rightRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(endNode); + rightRange.collapse(!collapse); + if(endOffset) { + rightRange.moveEnd("character", endOffset) + } + leftRange.setEndPoint("EndToEnd", rightRange); + return leftRange +}; +goog.dom.browserrange.IeRange.createFromNodeContents = function(node) { + var range = new goog.dom.browserrange.IeRange(goog.dom.browserrange.IeRange.getBrowserRangeForNode_(node), goog.dom.getOwnerDocument(node)); + if(!goog.dom.browserrange.canContainRangeEndpoint(node)) { + range.startNode_ = range.endNode_ = range.parentNode_ = node.parentNode; + range.startOffset_ = goog.array.indexOf(range.parentNode_.childNodes, node); + range.endOffset_ = range.startOffset_ + 1 + }else { + var tempNode, leaf = node; + while((tempNode = leaf.firstChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) { + leaf = tempNode + } + range.startNode_ = leaf; + range.startOffset_ = 0; + leaf = node; + while((tempNode = leaf.lastChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) { + leaf = tempNode + } + range.endNode_ = leaf; + range.endOffset_ = leaf.nodeType == goog.dom.NodeType.ELEMENT ? leaf.childNodes.length : leaf.length; + range.parentNode_ = node + } + return range +}; +goog.dom.browserrange.IeRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + var range = new goog.dom.browserrange.IeRange(goog.dom.browserrange.IeRange.getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog.dom.getOwnerDocument(startNode)); + range.startNode_ = startNode; + range.startOffset_ = startOffset; + range.endNode_ = endNode; + range.endOffset_ = endOffset; + return range +}; +goog.dom.browserrange.IeRange.prototype.parentNode_ = null; +goog.dom.browserrange.IeRange.prototype.startNode_ = null; +goog.dom.browserrange.IeRange.prototype.endNode_ = null; +goog.dom.browserrange.IeRange.prototype.startOffset_ = -1; +goog.dom.browserrange.IeRange.prototype.endOffset_ = -1; +goog.dom.browserrange.IeRange.prototype.clone = function() { + var range = new goog.dom.browserrange.IeRange(this.range_.duplicate(), this.doc_); + range.parentNode_ = this.parentNode_; + range.startNode_ = this.startNode_; + range.endNode_ = this.endNode_; + return range +}; +goog.dom.browserrange.IeRange.prototype.getBrowserRange = function() { + return this.range_ +}; +goog.dom.browserrange.IeRange.prototype.clearCachedValues_ = function() { + this.parentNode_ = this.startNode_ = this.endNode_ = null; + this.startOffset_ = this.endOffset_ = -1 +}; +goog.dom.browserrange.IeRange.prototype.getContainer = function() { + if(!this.parentNode_) { + var selectText = this.range_.text; + var range = this.range_.duplicate(); + var rightTrimmedSelectText = selectText.replace(/ +$/, ""); + var numSpacesAtEnd = selectText.length - rightTrimmedSelectText.length; + if(numSpacesAtEnd) { + range.moveEnd("character", -numSpacesAtEnd) + } + var parent = range.parentElement(); + var htmlText = range.htmlText; + var htmlTextLen = goog.string.stripNewlines(htmlText).length; + if(this.isCollapsed() && htmlTextLen > 0) { + return this.parentNode_ = parent + } + while(htmlTextLen > goog.string.stripNewlines(parent.outerHTML).length) { + parent = parent.parentNode + } + while(parent.childNodes.length == 1 && parent.innerText == goog.dom.browserrange.IeRange.getNodeText_(parent.firstChild)) { + if(!goog.dom.browserrange.canContainRangeEndpoint(parent.firstChild)) { + break + } + parent = parent.firstChild + } + if(selectText.length == 0) { + parent = this.findDeepestContainer_(parent) + } + this.parentNode_ = parent + } + return this.parentNode_ +}; +goog.dom.browserrange.IeRange.prototype.findDeepestContainer_ = function(node) { + var childNodes = node.childNodes; + for(var i = 0, len = childNodes.length;i < len;i++) { + var child = childNodes[i]; + if(goog.dom.browserrange.canContainRangeEndpoint(child)) { + var childRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(child); + var start = goog.dom.RangeEndpoint.START; + var end = goog.dom.RangeEndpoint.END; + var isChildRangeErratic = childRange.htmlText != child.outerHTML; + var isNativeInRangeErratic = this.isCollapsed() && isChildRangeErratic; + var inChildRange = isNativeInRangeErratic ? this.compareBrowserRangeEndpoints(childRange, start, start) >= 0 && this.compareBrowserRangeEndpoints(childRange, start, end) <= 0 : this.range_.inRange(childRange); + if(inChildRange) { + return this.findDeepestContainer_(child) + } + } + } + return node +}; +goog.dom.browserrange.IeRange.prototype.getStartNode = function() { + if(!this.startNode_) { + this.startNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.START); + if(this.isCollapsed()) { + this.endNode_ = this.startNode_ + } + } + return this.startNode_ +}; +goog.dom.browserrange.IeRange.prototype.getStartOffset = function() { + if(this.startOffset_ < 0) { + this.startOffset_ = this.getOffset_(goog.dom.RangeEndpoint.START); + if(this.isCollapsed()) { + this.endOffset_ = this.startOffset_ + } + } + return this.startOffset_ +}; +goog.dom.browserrange.IeRange.prototype.getEndNode = function() { + if(this.isCollapsed()) { + return this.getStartNode() + } + if(!this.endNode_) { + this.endNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.END) + } + return this.endNode_ +}; +goog.dom.browserrange.IeRange.prototype.getEndOffset = function() { + if(this.isCollapsed()) { + return this.getStartOffset() + } + if(this.endOffset_ < 0) { + this.endOffset_ = this.getOffset_(goog.dom.RangeEndpoint.END); + if(this.isCollapsed()) { + this.startOffset_ = this.endOffset_ + } + } + return this.endOffset_ +}; +goog.dom.browserrange.IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + return this.range_.compareEndPoints((thisEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End") + "To" + (otherEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End"), range) +}; +goog.dom.browserrange.IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) { + var node = opt_node || this.getContainer(); + if(!node || !node.firstChild) { + return node + } + var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END; + var isStartEndpoint = endpoint == start; + for(var j = 0, length = node.childNodes.length;j < length;j++) { + var i = isStartEndpoint ? j : length - j - 1; + var child = node.childNodes[i]; + var childRange; + try { + childRange = goog.dom.browserrange.createRangeFromNodeContents(child) + }catch(e) { + continue + } + var ieRange = childRange.getBrowserRange(); + if(this.isCollapsed()) { + if(!goog.dom.browserrange.canContainRangeEndpoint(child)) { + if(this.compareBrowserRangeEndpoints(ieRange, start, start) == 0) { + this.startOffset_ = this.endOffset_ = i; + return node + } + }else { + if(childRange.containsRange(this)) { + return this.getEndpointNode_(endpoint, child) + } + } + }else { + if(this.containsRange(childRange)) { + if(!goog.dom.browserrange.canContainRangeEndpoint(child)) { + if(isStartEndpoint) { + this.startOffset_ = i + }else { + this.endOffset_ = i + 1 + } + return node + } + return this.getEndpointNode_(endpoint, child) + }else { + if(this.compareBrowserRangeEndpoints(ieRange, start, end) < 0 && this.compareBrowserRangeEndpoints(ieRange, end, start) > 0) { + return this.getEndpointNode_(endpoint, child) + } + } + } + } + return node +}; +goog.dom.browserrange.IeRange.prototype.compareNodeEndpoints_ = function(node, thisEndpoint, otherEndpoint) { + return this.range_.compareEndPoints((thisEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End") + "To" + (otherEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End"), goog.dom.browserrange.createRangeFromNodeContents(node).getBrowserRange()) +}; +goog.dom.browserrange.IeRange.prototype.getOffset_ = function(endpoint, opt_container) { + var isStartEndpoint = endpoint == goog.dom.RangeEndpoint.START; + var container = opt_container || (isStartEndpoint ? this.getStartNode() : this.getEndNode()); + if(container.nodeType == goog.dom.NodeType.ELEMENT) { + var children = container.childNodes; + var len = children.length; + var edge = isStartEndpoint ? 0 : len - 1; + var sign = isStartEndpoint ? 1 : -1; + for(var i = edge;i >= 0 && i < len;i += sign) { + var child = children[i]; + if(goog.dom.browserrange.canContainRangeEndpoint(child)) { + continue + } + var endPointCompare = this.compareNodeEndpoints_(child, endpoint, endpoint); + if(endPointCompare == 0) { + return isStartEndpoint ? i : i + 1 + } + } + return i == -1 ? 0 : i + }else { + var range = this.range_.duplicate(); + var nodeRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(container); + range.setEndPoint(isStartEndpoint ? "EndToEnd" : "StartToStart", nodeRange); + var rangeLength = range.text.length; + return isStartEndpoint ? container.length - rangeLength : rangeLength + } +}; +goog.dom.browserrange.IeRange.getNodeText_ = function(node) { + return node.nodeType == goog.dom.NodeType.TEXT ? node.nodeValue : node.innerText +}; +goog.dom.browserrange.IeRange.prototype.isRangeInDocument = function() { + var range = this.doc_.body.createTextRange(); + range.moveToElementText(this.doc_.body); + return this.containsRange(new goog.dom.browserrange.IeRange(range, this.doc_), true) +}; +goog.dom.browserrange.IeRange.prototype.isCollapsed = function() { + return this.range_.compareEndPoints("StartToEnd", this.range_) == 0 +}; +goog.dom.browserrange.IeRange.prototype.getText = function() { + return this.range_.text +}; +goog.dom.browserrange.IeRange.prototype.getValidHtml = function() { + return this.range_.htmlText +}; +goog.dom.browserrange.IeRange.prototype.select = function(opt_reverse) { + this.range_.select() +}; +goog.dom.browserrange.IeRange.prototype.removeContents = function() { + if(this.range_.htmlText) { + var startNode = this.getStartNode(); + var endNode = this.getEndNode(); + var oldText = this.range_.text; + var clone = this.range_.duplicate(); + clone.moveStart("character", 1); + clone.moveStart("character", -1); + if(clone.text != oldText) { + var iter = new goog.dom.NodeIterator(startNode, false, true); + var toDelete = []; + goog.iter.forEach(iter, function(node) { + if(node.nodeType != goog.dom.NodeType.TEXT && this.containsNode(node)) { + toDelete.push(node); + iter.skipTag() + } + if(node == endNode) { + throw goog.iter.StopIteration; + } + }); + this.collapse(true); + goog.array.forEach(toDelete, goog.dom.removeNode); + this.clearCachedValues_(); + return + } + this.range_ = clone; + this.range_.text = ""; + this.clearCachedValues_(); + var newStartNode = this.getStartNode(); + var newStartOffset = this.getStartOffset(); + try { + var sibling = startNode.nextSibling; + if(startNode == endNode && startNode.parentNode && startNode.nodeType == goog.dom.NodeType.TEXT && sibling && sibling.nodeType == goog.dom.NodeType.TEXT) { + startNode.nodeValue += sibling.nodeValue; + goog.dom.removeNode(sibling); + this.range_ = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(newStartNode); + this.range_.move("character", newStartOffset); + this.clearCachedValues_() + } + }catch(e) { + } + } +}; +goog.dom.browserrange.IeRange.getDomHelper_ = function(range) { + return goog.dom.getDomHelper(range.parentElement()) +}; +goog.dom.browserrange.IeRange.pasteElement_ = function(range, element, opt_domHelper) { + opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(range); + var id; + var originalId = id = element.id; + if(!id) { + id = element.id = goog.string.createUniqueString() + } + range.pasteHTML(element.outerHTML); + element = opt_domHelper.getElement(id); + if(element) { + if(!originalId) { + element.removeAttribute("id") + } + } + return element +}; +goog.dom.browserrange.IeRange.prototype.surroundContents = function(element) { + goog.dom.removeNode(element); + element.innerHTML = this.range_.htmlText; + element = goog.dom.browserrange.IeRange.pasteElement_(this.range_, element); + if(element) { + this.range_.moveToElementText(element) + } + this.clearCachedValues_(); + return element +}; +goog.dom.browserrange.IeRange.insertNode_ = function(clone, node, before, opt_domHelper) { + opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(clone); + var isNonElement; + if(node.nodeType != goog.dom.NodeType.ELEMENT) { + isNonElement = true; + node = opt_domHelper.createDom(goog.dom.TagName.DIV, null, node) + } + clone.collapse(before); + node = goog.dom.browserrange.IeRange.pasteElement_(clone, node, opt_domHelper); + if(isNonElement) { + var newNonElement = node.firstChild; + opt_domHelper.flattenElement(node); + node = newNonElement + } + return node +}; +goog.dom.browserrange.IeRange.prototype.insertNode = function(node, before) { + var output = goog.dom.browserrange.IeRange.insertNode_(this.range_.duplicate(), node, before); + this.clearCachedValues_(); + return output +}; +goog.dom.browserrange.IeRange.prototype.surroundWithNodes = function(startNode, endNode) { + var clone1 = this.range_.duplicate(); + var clone2 = this.range_.duplicate(); + goog.dom.browserrange.IeRange.insertNode_(clone1, startNode, true); + goog.dom.browserrange.IeRange.insertNode_(clone2, endNode, false); + this.clearCachedValues_() +}; +goog.dom.browserrange.IeRange.prototype.collapse = function(toStart) { + this.range_.collapse(toStart); + if(toStart) { + this.endNode_ = this.startNode_; + this.endOffset_ = this.startOffset_ + }else { + this.startNode_ = this.endNode_; + this.startOffset_ = this.endOffset_ + } +}; +goog.provide("goog.dom.browserrange.OperaRange"); +goog.require("goog.dom.browserrange.W3cRange"); +goog.dom.browserrange.OperaRange = function(range) { + goog.dom.browserrange.W3cRange.call(this, range) +}; +goog.inherits(goog.dom.browserrange.OperaRange, goog.dom.browserrange.W3cRange); +goog.dom.browserrange.OperaRange.createFromNodeContents = function(node) { + return new goog.dom.browserrange.OperaRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node)) +}; +goog.dom.browserrange.OperaRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return new goog.dom.browserrange.OperaRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset)) +}; +goog.dom.browserrange.OperaRange.prototype.selectInternal = function(selection, reversed) { + selection.collapse(this.getStartNode(), this.getStartOffset()); + if(this.getEndNode() != this.getStartNode() || this.getEndOffset() != this.getStartOffset()) { + selection.extend(this.getEndNode(), this.getEndOffset()) + } + if(selection.rangeCount == 0) { + selection.addRange(this.range_) + } +}; +goog.provide("goog.dom.browserrange.WebKitRange"); +goog.require("goog.dom.RangeEndpoint"); +goog.require("goog.dom.browserrange.W3cRange"); +goog.require("goog.userAgent"); +goog.dom.browserrange.WebKitRange = function(range) { + goog.dom.browserrange.W3cRange.call(this, range) +}; +goog.inherits(goog.dom.browserrange.WebKitRange, goog.dom.browserrange.W3cRange); +goog.dom.browserrange.WebKitRange.createFromNodeContents = function(node) { + return new goog.dom.browserrange.WebKitRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node)) +}; +goog.dom.browserrange.WebKitRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return new goog.dom.browserrange.WebKitRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset)) +}; +goog.dom.browserrange.WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + if(goog.userAgent.isVersion("528")) { + return goog.dom.browserrange.WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint) + } + return this.range_.compareBoundaryPoints(otherEndpoint == goog.dom.RangeEndpoint.START ? thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_START : goog.global["Range"].END_TO_START : thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_END : goog.global["Range"].END_TO_END, range) +}; +goog.dom.browserrange.WebKitRange.prototype.selectInternal = function(selection, reversed) { + selection.removeAllRanges(); + if(reversed) { + selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset()) + }else { + selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) + } +}; +goog.provide("goog.dom.browserrange"); +goog.provide("goog.dom.browserrange.Error"); +goog.require("goog.dom"); +goog.require("goog.dom.browserrange.GeckoRange"); +goog.require("goog.dom.browserrange.IeRange"); +goog.require("goog.dom.browserrange.OperaRange"); +goog.require("goog.dom.browserrange.W3cRange"); +goog.require("goog.dom.browserrange.WebKitRange"); +goog.require("goog.userAgent"); +goog.dom.browserrange.Error = {NOT_IMPLEMENTED:"Not Implemented"}; +goog.dom.browserrange.createRange = function(range) { + if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) { + return new goog.dom.browserrange.IeRange(range, goog.dom.getOwnerDocument(range.parentElement())) + }else { + if(goog.userAgent.WEBKIT) { + return new goog.dom.browserrange.WebKitRange(range) + }else { + if(goog.userAgent.GECKO) { + return new goog.dom.browserrange.GeckoRange(range) + }else { + if(goog.userAgent.OPERA) { + return new goog.dom.browserrange.OperaRange(range) + }else { + return new goog.dom.browserrange.W3cRange(range) + } + } + } + } +}; +goog.dom.browserrange.createRangeFromNodeContents = function(node) { + if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) { + return goog.dom.browserrange.IeRange.createFromNodeContents(node) + }else { + if(goog.userAgent.WEBKIT) { + return goog.dom.browserrange.WebKitRange.createFromNodeContents(node) + }else { + if(goog.userAgent.GECKO) { + return goog.dom.browserrange.GeckoRange.createFromNodeContents(node) + }else { + if(goog.userAgent.OPERA) { + return goog.dom.browserrange.OperaRange.createFromNodeContents(node) + }else { + return goog.dom.browserrange.W3cRange.createFromNodeContents(node) + } + } + } + } +}; +goog.dom.browserrange.createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) { + if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) { + return goog.dom.browserrange.IeRange.createFromNodes(startNode, startOffset, endNode, endOffset) + }else { + if(goog.userAgent.WEBKIT) { + return goog.dom.browserrange.WebKitRange.createFromNodes(startNode, startOffset, endNode, endOffset) + }else { + if(goog.userAgent.GECKO) { + return goog.dom.browserrange.GeckoRange.createFromNodes(startNode, startOffset, endNode, endOffset) + }else { + if(goog.userAgent.OPERA) { + return goog.dom.browserrange.OperaRange.createFromNodes(startNode, startOffset, endNode, endOffset) + }else { + return goog.dom.browserrange.W3cRange.createFromNodes(startNode, startOffset, endNode, endOffset) + } + } + } + } +}; +goog.dom.browserrange.canContainRangeEndpoint = function(node) { + return goog.dom.canHaveChildren(node) || node.nodeType == goog.dom.NodeType.TEXT +}; +goog.provide("goog.dom.TextRange"); +goog.require("goog.array"); +goog.require("goog.dom"); +goog.require("goog.dom.AbstractRange"); +goog.require("goog.dom.RangeType"); +goog.require("goog.dom.SavedRange"); +goog.require("goog.dom.TagName"); +goog.require("goog.dom.TextRangeIterator"); +goog.require("goog.dom.browserrange"); +goog.require("goog.string"); +goog.require("goog.userAgent"); +goog.dom.TextRange = function() { +}; +goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange); +goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) { + return goog.dom.TextRange.createFromBrowserRangeWrapper_(goog.dom.browserrange.createRange(range), opt_isReversed) +}; +goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) { + var range = new goog.dom.TextRange; + range.browserRangeWrapper_ = browserRange; + range.isReversed_ = !!opt_isReversed; + return range +}; +goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) { + return goog.dom.TextRange.createFromBrowserRangeWrapper_(goog.dom.browserrange.createRangeFromNodeContents(node), opt_isReversed) +}; +goog.dom.TextRange.createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) { + var range = new goog.dom.TextRange; + range.isReversed_ = goog.dom.Range.isReversed(anchorNode, anchorOffset, focusNode, focusOffset); + if(anchorNode.tagName == "BR") { + var parent = anchorNode.parentNode; + anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode); + anchorNode = parent + } + if(focusNode.tagName == "BR") { + var parent = focusNode.parentNode; + focusOffset = goog.array.indexOf(parent.childNodes, focusNode); + focusNode = parent + } + if(range.isReversed_) { + range.startNode_ = focusNode; + range.startOffset_ = focusOffset; + range.endNode_ = anchorNode; + range.endOffset_ = anchorOffset + }else { + range.startNode_ = anchorNode; + range.startOffset_ = anchorOffset; + range.endNode_ = focusNode; + range.endOffset_ = focusOffset + } + return range +}; +goog.dom.TextRange.prototype.browserRangeWrapper_ = null; +goog.dom.TextRange.prototype.startNode_ = null; +goog.dom.TextRange.prototype.startOffset_ = null; +goog.dom.TextRange.prototype.endNode_ = null; +goog.dom.TextRange.prototype.endOffset_ = null; +goog.dom.TextRange.prototype.isReversed_ = false; +goog.dom.TextRange.prototype.clone = function() { + var range = new goog.dom.TextRange; + range.browserRangeWrapper_ = this.browserRangeWrapper_; + range.startNode_ = this.startNode_; + range.startOffset_ = this.startOffset_; + range.endNode_ = this.endNode_; + range.endOffset_ = this.endOffset_; + range.isReversed_ = this.isReversed_; + return range +}; +goog.dom.TextRange.prototype.getType = function() { + return goog.dom.RangeType.TEXT +}; +goog.dom.TextRange.prototype.getBrowserRangeObject = function() { + return this.getBrowserRangeWrapper_().getBrowserRange() +}; +goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) { + if(goog.dom.AbstractRange.isNativeControlRange(nativeRange)) { + return false + } + this.browserRangeWrapper_ = goog.dom.browserrange.createRange(nativeRange); + this.clearCachedValues_(); + return true +}; +goog.dom.TextRange.prototype.clearCachedValues_ = function() { + this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null +}; +goog.dom.TextRange.prototype.getTextRangeCount = function() { + return 1 +}; +goog.dom.TextRange.prototype.getTextRange = function(i) { + return this +}; +goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() { + return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())) +}; +goog.dom.TextRange.prototype.getContainer = function() { + return this.getBrowserRangeWrapper_().getContainer() +}; +goog.dom.TextRange.prototype.getStartNode = function() { + return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode()) +}; +goog.dom.TextRange.prototype.getStartOffset = function() { + return this.startOffset_ != null ? this.startOffset_ : this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset() +}; +goog.dom.TextRange.prototype.getEndNode = function() { + return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode()) +}; +goog.dom.TextRange.prototype.getEndOffset = function() { + return this.endOffset_ != null ? this.endOffset_ : this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset() +}; +goog.dom.TextRange.prototype.moveToNodes = function(startNode, startOffset, endNode, endOffset, isReversed) { + this.startNode_ = startNode; + this.startOffset_ = startOffset; + this.endNode_ = endNode; + this.endOffset_ = endOffset; + this.isReversed_ = isReversed; + this.browserRangeWrapper_ = null +}; +goog.dom.TextRange.prototype.isReversed = function() { + return this.isReversed_ +}; +goog.dom.TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) { + var otherRangeType = otherRange.getType(); + if(otherRangeType == goog.dom.RangeType.TEXT) { + return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial) + }else { + if(otherRangeType == goog.dom.RangeType.CONTROL) { + var elements = otherRange.getElements(); + var fn = opt_allowPartial ? goog.array.some : goog.array.every; + return fn(elements, function(el) { + return this.containsNode(el, opt_allowPartial) + }, this) + } + } + return false +}; +goog.dom.TextRange.isAttachedNode = function(node) { + if(goog.userAgent.IE) { + var returnValue = false; + try { + returnValue = node.parentNode + }catch(e) { + } + return!!returnValue + }else { + return goog.dom.contains(node.ownerDocument.body, node) + } +}; +goog.dom.TextRange.prototype.isRangeInDocument = function() { + return(!this.startNode_ || goog.dom.TextRange.isAttachedNode(this.startNode_)) && (!this.endNode_ || goog.dom.TextRange.isAttachedNode(this.endNode_)) && (!goog.userAgent.IE || this.getBrowserRangeWrapper_().isRangeInDocument()) +}; +goog.dom.TextRange.prototype.isCollapsed = function() { + return this.getBrowserRangeWrapper_().isCollapsed() +}; +goog.dom.TextRange.prototype.getText = function() { + return this.getBrowserRangeWrapper_().getText() +}; +goog.dom.TextRange.prototype.getHtmlFragment = function() { + return this.getBrowserRangeWrapper_().getHtmlFragment() +}; +goog.dom.TextRange.prototype.getValidHtml = function() { + return this.getBrowserRangeWrapper_().getValidHtml() +}; +goog.dom.TextRange.prototype.getPastableHtml = function() { + var html = this.getValidHtml(); + if(html.match(/^\s*<td\b/i)) { + html = "<table><tbody><tr>" + html + "</tr></tbody></table>" + }else { + if(html.match(/^\s*<tr\b/i)) { + html = "<table><tbody>" + html + "</tbody></table>" + }else { + if(html.match(/^\s*<tbody\b/i)) { + html = "<table>" + html + "</table>" + }else { + if(html.match(/^\s*<li\b/i)) { + var container = this.getContainer(); + var tagType = goog.dom.TagName.UL; + while(container) { + if(container.tagName == goog.dom.TagName.OL) { + tagType = goog.dom.TagName.OL; + break + }else { + if(container.tagName == goog.dom.TagName.UL) { + break + } + } + container = container.parentNode + } + html = goog.string.buildString("<", tagType, ">", html, "</", tagType, ">") + } + } + } + } + return html +}; +goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) { + return new goog.dom.TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +}; +goog.dom.TextRange.prototype.select = function() { + this.getBrowserRangeWrapper_().select(this.isReversed_) +}; +goog.dom.TextRange.prototype.removeContents = function() { + this.getBrowserRangeWrapper_().removeContents(); + this.clearCachedValues_() +}; +goog.dom.TextRange.prototype.surroundContents = function(element) { + var output = this.getBrowserRangeWrapper_().surroundContents(element); + this.clearCachedValues_(); + return output +}; +goog.dom.TextRange.prototype.insertNode = function(node, before) { + var output = this.getBrowserRangeWrapper_().insertNode(node, before); + this.clearCachedValues_(); + return output +}; +goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) { + this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode); + this.clearCachedValues_() +}; +goog.dom.TextRange.prototype.saveUsingDom = function() { + return new goog.dom.DomSavedTextRange_(this) +}; +goog.dom.TextRange.prototype.collapse = function(toAnchor) { + var toStart = this.isReversed() ? !toAnchor : toAnchor; + if(this.browserRangeWrapper_) { + this.browserRangeWrapper_.collapse(toStart) + } + if(toStart) { + this.endNode_ = this.startNode_; + this.endOffset_ = this.startOffset_ + }else { + this.startNode_ = this.endNode_; + this.startOffset_ = this.endOffset_ + } + this.isReversed_ = false +}; +goog.dom.DomSavedTextRange_ = function(range) { + this.anchorNode_ = range.getAnchorNode(); + this.anchorOffset_ = range.getAnchorOffset(); + this.focusNode_ = range.getFocusNode(); + this.focusOffset_ = range.getFocusOffset() +}; +goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange); +goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() { + return goog.dom.Range.createFromNodes(this.anchorNode_, this.anchorOffset_, this.focusNode_, this.focusOffset_) +}; +goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() { + goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this); + this.anchorNode_ = null; + this.focusNode_ = null +}; +goog.provide("goog.dom.ControlRange"); +goog.provide("goog.dom.ControlRangeIterator"); +goog.require("goog.array"); +goog.require("goog.dom"); +goog.require("goog.dom.AbstractMultiRange"); +goog.require("goog.dom.AbstractRange"); +goog.require("goog.dom.RangeIterator"); +goog.require("goog.dom.RangeType"); +goog.require("goog.dom.SavedRange"); +goog.require("goog.dom.TagWalkType"); +goog.require("goog.dom.TextRange"); +goog.require("goog.iter.StopIteration"); +goog.require("goog.userAgent"); +goog.dom.ControlRange = function() { +}; +goog.inherits(goog.dom.ControlRange, goog.dom.AbstractMultiRange); +goog.dom.ControlRange.createFromBrowserRange = function(controlRange) { + var range = new goog.dom.ControlRange; + range.range_ = controlRange; + return range +}; +goog.dom.ControlRange.createFromElements = function(var_args) { + var range = goog.dom.getOwnerDocument(arguments[0]).body.createControlRange(); + for(var i = 0, len = arguments.length;i < len;i++) { + range.addElement(arguments[i]) + } + return goog.dom.ControlRange.createFromBrowserRange(range) +}; +goog.dom.ControlRange.prototype.range_ = null; +goog.dom.ControlRange.prototype.elements_ = null; +goog.dom.ControlRange.prototype.sortedElements_ = null; +goog.dom.ControlRange.prototype.clearCachedValues_ = function() { + this.elements_ = null; + this.sortedElements_ = null +}; +goog.dom.ControlRange.prototype.clone = function() { + return goog.dom.ControlRange.createFromElements.apply(this, this.getElements()) +}; +goog.dom.ControlRange.prototype.getType = function() { + return goog.dom.RangeType.CONTROL +}; +goog.dom.ControlRange.prototype.getBrowserRangeObject = function() { + return this.range_ || document.body.createControlRange() +}; +goog.dom.ControlRange.prototype.setBrowserRangeObject = function(nativeRange) { + if(!goog.dom.AbstractRange.isNativeControlRange(nativeRange)) { + return false + } + this.range_ = nativeRange; + return true +}; +goog.dom.ControlRange.prototype.getTextRangeCount = function() { + return this.range_ ? this.range_.length : 0 +}; +goog.dom.ControlRange.prototype.getTextRange = function(i) { + return goog.dom.TextRange.createFromNodeContents(this.range_.item(i)) +}; +goog.dom.ControlRange.prototype.getContainer = function() { + return goog.dom.findCommonAncestor.apply(null, this.getElements()) +}; +goog.dom.ControlRange.prototype.getStartNode = function() { + return this.getSortedElements()[0] +}; +goog.dom.ControlRange.prototype.getStartOffset = function() { + return 0 +}; +goog.dom.ControlRange.prototype.getEndNode = function() { + var sorted = this.getSortedElements(); + var startsLast = goog.array.peek(sorted); + return goog.array.find(sorted, function(el) { + return goog.dom.contains(el, startsLast) + }) +}; +goog.dom.ControlRange.prototype.getEndOffset = function() { + return this.getEndNode().childNodes.length +}; +goog.dom.ControlRange.prototype.getElements = function() { + if(!this.elements_) { + this.elements_ = []; + if(this.range_) { + for(var i = 0;i < this.range_.length;i++) { + this.elements_.push(this.range_.item(i)) + } + } + } + return this.elements_ +}; +goog.dom.ControlRange.prototype.getSortedElements = function() { + if(!this.sortedElements_) { + this.sortedElements_ = this.getElements().concat(); + this.sortedElements_.sort(function(a, b) { + return a.sourceIndex - b.sourceIndex + }) + } + return this.sortedElements_ +}; +goog.dom.ControlRange.prototype.isRangeInDocument = function() { + var returnValue = false; + try { + returnValue = goog.array.every(this.getElements(), function(element) { + return goog.userAgent.IE ? element.parentNode : goog.dom.contains(element.ownerDocument.body, element) + }) + }catch(e) { + } + return returnValue +}; +goog.dom.ControlRange.prototype.isCollapsed = function() { + return!this.range_ || !this.range_.length +}; +goog.dom.ControlRange.prototype.getText = function() { + return"" +}; +goog.dom.ControlRange.prototype.getHtmlFragment = function() { + return goog.array.map(this.getSortedElements(), goog.dom.getOuterHtml).join("") +}; +goog.dom.ControlRange.prototype.getValidHtml = function() { + return this.getHtmlFragment() +}; +goog.dom.ControlRange.prototype.getPastableHtml = goog.dom.ControlRange.prototype.getValidHtml; +goog.dom.ControlRange.prototype.__iterator__ = function(opt_keys) { + return new goog.dom.ControlRangeIterator(this) +}; +goog.dom.ControlRange.prototype.select = function() { + if(this.range_) { + this.range_.select() + } +}; +goog.dom.ControlRange.prototype.removeContents = function() { + if(this.range_) { + var nodes = []; + for(var i = 0, len = this.range_.length;i < len;i++) { + nodes.push(this.range_.item(i)) + } + goog.array.forEach(nodes, goog.dom.removeNode); + this.collapse(false) + } +}; +goog.dom.ControlRange.prototype.replaceContentsWithNode = function(node) { + var result = this.insertNode(node, true); + if(!this.isCollapsed()) { + this.removeContents() + } + return result +}; +goog.dom.ControlRange.prototype.saveUsingDom = function() { + return new goog.dom.DomSavedControlRange_(this) +}; +goog.dom.ControlRange.prototype.collapse = function(toAnchor) { + this.range_ = null; + this.clearCachedValues_() +}; +goog.dom.DomSavedControlRange_ = function(range) { + this.elements_ = range.getElements() +}; +goog.inherits(goog.dom.DomSavedControlRange_, goog.dom.SavedRange); +goog.dom.DomSavedControlRange_.prototype.restoreInternal = function() { + var doc = this.elements_.length ? goog.dom.getOwnerDocument(this.elements_[0]) : document; + var controlRange = doc.body.createControlRange(); + for(var i = 0, len = this.elements_.length;i < len;i++) { + controlRange.addElement(this.elements_[i]) + } + return goog.dom.ControlRange.createFromBrowserRange(controlRange) +}; +goog.dom.DomSavedControlRange_.prototype.disposeInternal = function() { + goog.dom.DomSavedControlRange_.superClass_.disposeInternal.call(this); + delete this.elements_ +}; +goog.dom.ControlRangeIterator = function(range) { + if(range) { + this.elements_ = range.getSortedElements(); + this.startNode_ = this.elements_.shift(); + this.endNode_ = goog.array.peek(this.elements_) || this.startNode_ + } + goog.dom.RangeIterator.call(this, this.startNode_, false) +}; +goog.inherits(goog.dom.ControlRangeIterator, goog.dom.RangeIterator); +goog.dom.ControlRangeIterator.prototype.startNode_ = null; +goog.dom.ControlRangeIterator.prototype.endNode_ = null; +goog.dom.ControlRangeIterator.prototype.elements_ = null; +goog.dom.ControlRangeIterator.prototype.getStartTextOffset = function() { + return 0 +}; +goog.dom.ControlRangeIterator.prototype.getEndTextOffset = function() { + return 0 +}; +goog.dom.ControlRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog.dom.ControlRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog.dom.ControlRangeIterator.prototype.isLast = function() { + return!this.depth && !this.elements_.length +}; +goog.dom.ControlRangeIterator.prototype.next = function() { + if(this.isLast()) { + throw goog.iter.StopIteration; + }else { + if(!this.depth) { + var el = this.elements_.shift(); + this.setPosition(el, goog.dom.TagWalkType.START_TAG, goog.dom.TagWalkType.START_TAG); + return el + } + } + return goog.dom.ControlRangeIterator.superClass_.next.call(this) +}; +goog.dom.ControlRangeIterator.prototype.copyFrom = function(other) { + this.elements_ = other.elements_; + this.startNode_ = other.startNode_; + this.endNode_ = other.endNode_; + goog.dom.ControlRangeIterator.superClass_.copyFrom.call(this, other) +}; +goog.dom.ControlRangeIterator.prototype.clone = function() { + var copy = new goog.dom.ControlRangeIterator(null); + copy.copyFrom(this); + return copy +}; +goog.provide("goog.dom.MultiRange"); +goog.provide("goog.dom.MultiRangeIterator"); +goog.require("goog.array"); +goog.require("goog.debug.Logger"); +goog.require("goog.dom.AbstractMultiRange"); +goog.require("goog.dom.AbstractRange"); +goog.require("goog.dom.RangeIterator"); +goog.require("goog.dom.RangeType"); +goog.require("goog.dom.SavedRange"); +goog.require("goog.dom.TextRange"); +goog.require("goog.iter.StopIteration"); +goog.dom.MultiRange = function() { + this.browserRanges_ = []; + this.ranges_ = []; + this.sortedRanges_ = null; + this.container_ = null +}; +goog.inherits(goog.dom.MultiRange, goog.dom.AbstractMultiRange); +goog.dom.MultiRange.createFromBrowserSelection = function(selection) { + var range = new goog.dom.MultiRange; + for(var i = 0, len = selection.rangeCount;i < len;i++) { + range.browserRanges_.push(selection.getRangeAt(i)) + } + return range +}; +goog.dom.MultiRange.createFromBrowserRanges = function(browserRanges) { + var range = new goog.dom.MultiRange; + range.browserRanges_ = goog.array.clone(browserRanges); + return range +}; +goog.dom.MultiRange.createFromTextRanges = function(textRanges) { + var range = new goog.dom.MultiRange; + range.ranges_ = textRanges; + range.browserRanges_ = goog.array.map(textRanges, function(range) { + return range.getBrowserRangeObject() + }); + return range +}; +goog.dom.MultiRange.prototype.logger_ = goog.debug.Logger.getLogger("goog.dom.MultiRange"); +goog.dom.MultiRange.prototype.clearCachedValues_ = function() { + this.ranges_ = []; + this.sortedRanges_ = null; + this.container_ = null +}; +goog.dom.MultiRange.prototype.clone = function() { + return goog.dom.MultiRange.createFromBrowserRanges(this.browserRanges_) +}; +goog.dom.MultiRange.prototype.getType = function() { + return goog.dom.RangeType.MULTI +}; +goog.dom.MultiRange.prototype.getBrowserRangeObject = function() { + if(this.browserRanges_.length > 1) { + this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range") + } + return this.browserRanges_[0] +}; +goog.dom.MultiRange.prototype.setBrowserRangeObject = function(nativeRange) { + return false +}; +goog.dom.MultiRange.prototype.getTextRangeCount = function() { + return this.browserRanges_.length +}; +goog.dom.MultiRange.prototype.getTextRange = function(i) { + if(!this.ranges_[i]) { + this.ranges_[i] = goog.dom.TextRange.createFromBrowserRange(this.browserRanges_[i]) + } + return this.ranges_[i] +}; +goog.dom.MultiRange.prototype.getContainer = function() { + if(!this.container_) { + var nodes = []; + for(var i = 0, len = this.getTextRangeCount();i < len;i++) { + nodes.push(this.getTextRange(i).getContainer()) + } + this.container_ = goog.dom.findCommonAncestor.apply(null, nodes) + } + return this.container_ +}; +goog.dom.MultiRange.prototype.getSortedRanges = function() { + if(!this.sortedRanges_) { + this.sortedRanges_ = this.getTextRanges(); + this.sortedRanges_.sort(function(a, b) { + var aStartNode = a.getStartNode(); + var aStartOffset = a.getStartOffset(); + var bStartNode = b.getStartNode(); + var bStartOffset = b.getStartOffset(); + if(aStartNode == bStartNode && aStartOffset == bStartOffset) { + return 0 + } + return goog.dom.Range.isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1 + }) + } + return this.sortedRanges_ +}; +goog.dom.MultiRange.prototype.getStartNode = function() { + return this.getSortedRanges()[0].getStartNode() +}; +goog.dom.MultiRange.prototype.getStartOffset = function() { + return this.getSortedRanges()[0].getStartOffset() +}; +goog.dom.MultiRange.prototype.getEndNode = function() { + return goog.array.peek(this.getSortedRanges()).getEndNode() +}; +goog.dom.MultiRange.prototype.getEndOffset = function() { + return goog.array.peek(this.getSortedRanges()).getEndOffset() +}; +/* +goog.dom.MultiRange.prototype.isRangeInDocument = function() { + return goog.array.every(this.getTextRanges(), function(range) { + return range.isRangeInDocument() + }) +}; +*/ +goog.dom.MultiRange.prototype.isCollapsed = function() { + return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed() +}; +goog.dom.MultiRange.prototype.getText = function() { + return goog.array.map(this.getTextRanges(), function(range) { + return range.getText() + }).join("") +}; +goog.dom.MultiRange.prototype.getHtmlFragment = function() { + return this.getValidHtml() +}; +goog.dom.MultiRange.prototype.getValidHtml = function() { + return goog.array.map(this.getTextRanges(), function(range) { + return range.getValidHtml() + }).join("") +}; +goog.dom.MultiRange.prototype.getPastableHtml = function() { + return this.getValidHtml() +}; +goog.dom.MultiRange.prototype.__iterator__ = function(opt_keys) { + return new goog.dom.MultiRangeIterator(this) +}; +goog.dom.MultiRange.prototype.select = function() { + var selection = goog.dom.AbstractRange.getBrowserSelectionForWindow(this.getWindow()); + selection.removeAllRanges(); + for(var i = 0, len = this.getTextRangeCount();i < len;i++) { + selection.addRange(this.getTextRange(i).getBrowserRangeObject()) + } +}; +goog.dom.MultiRange.prototype.removeContents = function() { + goog.array.forEach(this.getTextRanges(), function(range) { + range.removeContents() + }) +}; +goog.dom.MultiRange.prototype.saveUsingDom = function() { + return new goog.dom.DomSavedMultiRange_(this) +}; +goog.dom.MultiRange.prototype.collapse = function(toAnchor) { + if(!this.isCollapsed()) { + var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1); + this.clearCachedValues_(); + range.collapse(toAnchor); + this.ranges_ = [range]; + this.sortedRanges_ = [range]; + this.browserRanges_ = [range.getBrowserRangeObject()] + } +}; +goog.dom.DomSavedMultiRange_ = function(range) { + this.savedRanges_ = goog.array.map(range.getTextRanges(), function(range) { + return range.saveUsingDom() + }) +}; +goog.inherits(goog.dom.DomSavedMultiRange_, goog.dom.SavedRange); +goog.dom.DomSavedMultiRange_.prototype.restoreInternal = function() { + var ranges = goog.array.map(this.savedRanges_, function(savedRange) { + return savedRange.restore() + }); + return goog.dom.MultiRange.createFromTextRanges(ranges) +}; +goog.dom.DomSavedMultiRange_.prototype.disposeInternal = function() { + goog.dom.DomSavedMultiRange_.superClass_.disposeInternal.call(this); + goog.array.forEach(this.savedRanges_, function(savedRange) { + savedRange.dispose() + }); + delete this.savedRanges_ +}; +goog.dom.MultiRangeIterator = function(range) { + if(range) { + this.iterators_ = goog.array.map(range.getSortedRanges(), function(r) { + return goog.iter.toIterator(r) + }) + } + goog.dom.RangeIterator.call(this, range ? this.getStartNode() : null, false) +}; +goog.inherits(goog.dom.MultiRangeIterator, goog.dom.RangeIterator); +goog.dom.MultiRangeIterator.prototype.iterators_ = null; +goog.dom.MultiRangeIterator.prototype.currentIdx_ = 0; +goog.dom.MultiRangeIterator.prototype.getStartTextOffset = function() { + return this.iterators_[this.currentIdx_].getStartTextOffset() +}; +goog.dom.MultiRangeIterator.prototype.getEndTextOffset = function() { + return this.iterators_[this.currentIdx_].getEndTextOffset() +}; +goog.dom.MultiRangeIterator.prototype.getStartNode = function() { + return this.iterators_[0].getStartNode() +}; +goog.dom.MultiRangeIterator.prototype.getEndNode = function() { + return goog.array.peek(this.iterators_).getEndNode() +}; +goog.dom.MultiRangeIterator.prototype.isLast = function() { + return this.iterators_[this.currentIdx_].isLast() +}; +goog.dom.MultiRangeIterator.prototype.next = function() { + try { + var it = this.iterators_[this.currentIdx_]; + var next = it.next(); + this.setPosition(it.node, it.tagType, it.depth); + return next + }catch(ex) { + if(ex !== goog.iter.StopIteration || this.iterators_.length - 1 == this.currentIdx_) { + throw ex; + }else { + this.currentIdx_++; + return this.next() + } + } +}; +goog.dom.MultiRangeIterator.prototype.copyFrom = function(other) { + this.iterators_ = goog.array.clone(other.iterators_); + goog.dom.MultiRangeIterator.superClass_.copyFrom.call(this, other) +}; +goog.dom.MultiRangeIterator.prototype.clone = function() { + var copy = new goog.dom.MultiRangeIterator(null); + copy.copyFrom(this); + return copy +}; +goog.provide("goog.dom.Range"); +goog.require("goog.dom"); +goog.require("goog.dom.AbstractRange"); +goog.require("goog.dom.ControlRange"); +goog.require("goog.dom.MultiRange"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.TextRange"); +goog.require("goog.userAgent"); +goog.dom.Range.createFromWindow = function(opt_win) { + var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window); + return sel && goog.dom.Range.createFromBrowserSelection(sel) +}; +goog.dom.Range.createFromBrowserSelection = function(selection) { + var range; + var isReversed = false; + if(selection.createRange) { + try { + range = selection.createRange() + }catch(e) { + return null + } + }else { + if(selection.rangeCount) { + if(selection.rangeCount > 1) { + return goog.dom.MultiRange.createFromBrowserSelection(selection) + }else { + range = selection.getRangeAt(0); + isReversed = goog.dom.Range.isReversed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset) + } + }else { + return null + } + } + return goog.dom.Range.createFromBrowserRange(range, isReversed) +}; +goog.dom.Range.createFromBrowserRange = function(range, opt_isReversed) { + return goog.dom.AbstractRange.isNativeControlRange(range) ? goog.dom.ControlRange.createFromBrowserRange(range) : goog.dom.TextRange.createFromBrowserRange(range, opt_isReversed) +}; +goog.dom.Range.createFromNodeContents = function(node, opt_isReversed) { + return goog.dom.TextRange.createFromNodeContents(node, opt_isReversed) +}; +goog.dom.Range.createCaret = function(node, offset) { + return goog.dom.TextRange.createFromNodes(node, offset, node, offset) +}; +goog.dom.Range.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return goog.dom.TextRange.createFromNodes(startNode, startOffset, endNode, endOffset) +}; +goog.dom.Range.clearSelection = function(opt_win) { + var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window); + if(!sel) { + return + } + if(sel.empty) { + try { + sel.empty() + }catch(e) { + } + }else { + sel.removeAllRanges() + } +}; +goog.dom.Range.hasSelection = function(opt_win) { + var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window); + return!!sel && (goog.userAgent.IE ? sel.type != "None" : !!sel.rangeCount) +}; +goog.dom.Range.isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) { + if(anchorNode == focusNode) { + return focusOffset < anchorOffset + } + var child; + if(anchorNode.nodeType == goog.dom.NodeType.ELEMENT && anchorOffset) { + child = anchorNode.childNodes[anchorOffset]; + if(child) { + anchorNode = child; + anchorOffset = 0 + }else { + if(goog.dom.contains(anchorNode, focusNode)) { + return true + } + } + } + if(focusNode.nodeType == goog.dom.NodeType.ELEMENT && focusOffset) { + child = focusNode.childNodes[focusOffset]; + if(child) { + focusNode = child; + focusOffset = 0 + }else { + if(goog.dom.contains(focusNode, anchorNode)) { + return false + } + } + } + return(goog.dom.compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0 +}; +window.createFromWindow = goog.dom.Range.createFromWindow; +window.createFromNodes = goog.dom.Range.createFromNodes; +window.createCaret = goog.dom.Range.createCaret;
\ No newline at end of file diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js new file mode 100644 index 000000000..6e2acd937 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js @@ -0,0 +1,383 @@ +/** + * @fileoverview + * Main functions used in running the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * Info function: returns true if the suite (mainly) tests the result HTML/Text. + * + * @param suite {String} the test suite + * @return {boolean} Whether the suite main focus is the output HTML/Text + */ +function suiteChecksHTMLOrText(suite) { + return suite.id[0] != 'S'; +} + +/** + * Info function: returns true if the suite checks the result selection. + * + * @param suite {String} the test suite + * @return {boolean} Whether the suite checks the selection + */ +function suiteChecksSelection(suite) { + return suite.id[0] != 'Q'; +} + +/** + * Helper function returning the effective value of a test parameter. + * + * @param suite {Object} the test suite + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test + * @param param {String} the test parameter to be checked + * @return {Any} the effective value of the parameter (can be undefined) + */ +function getTestParameter(suite, group, test, param) { + var val = test[param]; + if (val === undefined) { + val = group[param]; + } + if (val === undefined) { + val = suite[param]; + } + return val; +} + +/** + * Helper function returning the effective value of a container/test parameter. + * + * @param suite {Object} the test suite + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test + * @param container {Object} the container descriptor object + * @param param {String} the test parameter to be checked + * @return {Any} the effective value of the parameter (can be undefined) + */ +function getContainerParameter(suite, group, test, container, param) { + var val = undefined; + if (test[container.id]) { + val = test[container.id][param]; + } + if (val === undefined) { + val = test[param]; + } + if (val === undefined) { + val = group[param]; + } + if (val === undefined) { + val = suite[param]; + } + return val; +} + +/** + * Initializes the global variables before any tests are run. + */ +function initVariables() { + results = { + count: 0, + valscore: 0, + selscore: 0 + }; +} + +/** + * Runs a single test - outputs and sets the result variables. + * + * @param suite {Object} suite that test originates in as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} test to be run as object reference + * @param container {Object} container descriptor as object reference + * @see variables.js for RESULT... values + */ +function runSingleTest(suite, group, test, container) { + var result = { + valscore: 0, + selscore: 0, + valresult: VALRESULT_NOT_RUN, + selresult: SELRESULT_NOT_RUN, + output: '' + }; + + // 1.) Populate the editor element with the initial test setup HTML. + try { + initContainer(suite, group, test, container); + } catch(ex) { + result.valresult = VALRESULT_SETUP_EXCEPTION; + result.selresult = SELRESULT_NA; + result.output = SETUP_EXCEPTION + ex.toString(); + return result; + } + + // 2.) Run the test command, general function or query function. + var isHTMLTest = false; + + try { + var cmd = undefined; + + if (cmd = getTestParameter(suite, group, test, PARAM_EXECCOMMAND)) { + isHTMLTest = true; + // Note: "getTestParameter(suite, group, test, PARAM_VALUE) || null" + // doesn't work, since value might be the empty string, e.g., for 'insertText'! + var value = getTestParameter(suite, group, test, PARAM_VALUE); + if (value === undefined) { + value = null; + } + container.doc.execCommand(cmd, false, value); + } else if (cmd = getTestParameter(suite, group, test, PARAM_FUNCTION)) { + isHTMLTest = true; + eval(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSUPPORTED)) { + result.output = container.doc.queryCommandSupported(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDENABLED)) { + result.output = container.doc.queryCommandEnabled(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDINDETERM)) { + result.output = container.doc.queryCommandIndeterm(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSTATE)) { + result.output = container.doc.queryCommandState(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) { + result.output = container.doc.queryCommandValue(cmd); + if (result.output === false) { + // A return value of boolean 'false' for queryCommandValue means 'not supported'. + result.valresult = VALRESULT_UNSUPPORTED; + result.selresult = SELRESULT_NA; + result.output = UNSUPPORTED; + return result; + } + } else { + result.valresult = VALRESULT_SETUP_EXCEPTION; + result.selresult = SELRESULT_NA; + result.output = SETUP_EXCEPTION + SETUP_NOCOMMAND; + return result; + } + } catch (ex) { + result.valresult = VALRESULT_EXECUTION_EXCEPTION; + result.selresult = SELRESULT_NA; + result.output = EXECUTION_EXCEPTION + ex.toString(); + return result; + } + + // 4.) Verify test result + try { + if (isHTMLTest) { + // First, retrieve HTML from container + prepareHTMLTestResult(container, result); + + // Compare result to expectations + compareHTMLTestResult(suite, group, test, container, result); + + result.valscore = (result.valresult === VALRESULT_EQUAL) ? 1 : 0; + result.selscore = (result.selresult === SELRESULT_EQUAL) ? 1 : 0; + } else { + compareTextTestResult(suite, group, test, result); + + result.selresult = SELRESULT_NA; + result.valscore = (result.valresult === VALRESULT_EQUAL) ? 1 : 0; + } + } catch (ex) { + result.valresult = VALRESULT_VERIFICATION_EXCEPTION; + result.selresult = SELRESULT_NA; + result.output = VERIFICATION_EXCEPTION + ex.toString(); + return result; + } + + return result; +} + +/** + * Initializes the results dictionary for a given test suite. + * (for all classes -> tests -> containers) + * + * @param {Object} suite as object reference + */ +function initTestSuiteResults(suite) { + var suiteID = suite.id; + + // Initialize results entries for this suite + results[suiteID] = { + count: 0, + valscore: 0, + selscore: 0, + time: 0 + }; + var totalTestCount = 0; + + for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) { + var clsID = testClassIDs[clsIdx]; + var cls = suite[clsID]; + if (!cls) + continue; + + results[suiteID][clsID] = { + count: 0, + valscore: 0, + selscore: 0 + }; + var clsTestCount = 0; + + var groupCount = cls.length; + for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) { + var group = cls[groupIdx]; + var testCount = group.tests.length; + + clsTestCount += testCount; + totalTestCount += testCount; + + for (var testIdx = 0; testIdx < testCount; ++testIdx) { + var test = group.tests[testIdx]; + + results[suiteID][clsID ][test.id] = { + valscore: 0, + selscore: 0, + valresult: VALRESULT_NOT_RUN, + selresult: SELRESULT_NOT_RUN + }; + for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) { + var cntID = containers[cntIdx].id; + + results[suiteID][clsID][test.id][cntID] = { + valscore: 0, + selscore: 0, + valresult: VALRESULT_NOT_RUN, + selresult: SELRESULT_NOT_RUN, + output: '' + } + } + } + } + results[suiteID][clsID].count = clsTestCount; + } + results[suiteID].count = totalTestCount; +} + +/** + * Runs a single test suite (such as DELETE tests or INSERT tests). + * + * @param suite {Object} suite as object reference + */ +function runTestSuite(suite) { + var suiteID = suite.id; + var suiteStartTime = new Date().getTime(); + + initTestSuiteResults(suite); + + for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) { + var clsID = testClassIDs[clsIdx]; + var cls = suite[clsID]; + if (!cls) + continue; + + var groupCount = cls.length; + + for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) { + var group = cls[groupIdx]; + var testCount = group.tests.length; + + for (var testIdx = 0; testIdx < testCount; ++testIdx) { + var test = group.tests[testIdx]; + + var valscore = 1; + var selscore = 1; + var valresult = VALRESULT_EQUAL; + var selresult = SELRESULT_EQUAL; + + for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) { + var container = containers[cntIdx]; + var cntID = container.id; + + var result = runSingleTest(suite, group, test, container); + + results[suiteID][clsID][test.id][cntID] = result; + + valscore = Math.min(valscore, result.valscore); + selscore = Math.min(selscore, result.selscore); + valresult = Math.min(valresult, result.valresult); + selresult = Math.min(selresult, result.selresult); + + resetContainer(container); + } + + results[suiteID][clsID][test.id].valscore = valscore; + results[suiteID][clsID][test.id].selscore = selscore; + results[suiteID][clsID][test.id].valresult = valresult; + results[suiteID][clsID][test.id].selresult = selresult; + + results[suiteID][clsID].valscore += valscore; + results[suiteID][clsID].selscore += selscore; + results[suiteID].valscore += valscore; + results[suiteID].selscore += selscore; + results.valscore += valscore; + results.selscore += selscore; + } + } + } + + results[suiteID].time = new Date().getTime() - suiteStartTime; +} + +/** + * Runs a single test suite (such as DELETE tests or INSERT tests) + * and updates the output HTML. + * + * @param {Object} suite as object reference + */ +function runAndOutputTestSuite(suite) { + runTestSuite(suite); + outputTestSuiteResults(suite); +} + +/** + * Fills the beacon with the test results. + */ +function fillResults() { + // Result totals of the individual categories + categoryTotals = [ + 'selection=' + results['S'].selscore, + 'apply=' + results['A'].valscore, + 'applyCSS=' + results['AC'].valscore, + 'change=' + results['C'].valscore, + 'changeCSS=' + results['CC'].valscore, + 'unapply=' + results['U'].valscore, + 'unapplyCSS=' + results['UC'].valscore, + 'delete=' + results['D'].valscore, + 'forwarddelete=' + results['FD'].valscore, + 'insert=' + results['I'].valscore, + 'selectionResult=' + (results['A'].selscore + + results['AC'].selscore + + results['C'].selscore + + results['CC'].selscore + + results['U'].selscore + + results['UC'].selscore + + results['D'].selscore + + results['FD'].selscore + + results['I'].selscore), + 'querySupported=' + results['Q'].valscore, + 'queryEnabled=' + results['QE'].valscore, + 'queryIndeterm=' + results['QI'].valscore, + 'queryState=' + results['QS'].valscore, + 'queryStateCSS=' + results['QSC'].valscore, + 'queryValue=' + results['QV'].valscore, + 'queryValueCSS=' + results['QVC'].valscore + ]; + + // Beacon copies category results + beacon = categoryTotals.slice(0); +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js new file mode 100644 index 000000000..f2c23fbe5 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js @@ -0,0 +1,416 @@ +/** + * @fileoverview + * Common constants and variables used in the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +// All colors defined in CSS3. +var colorChart = { + 'aliceblue': {red: 0xF0, green: 0xF8, blue: 0xFF}, + 'antiquewhite': {red: 0xFA, green: 0xEB, blue: 0xD7}, + 'aqua': {red: 0x00, green: 0xFF, blue: 0xFF}, + 'aquamarine': {red: 0x7F, green: 0xFF, blue: 0xD4}, + 'azure': {red: 0xF0, green: 0xFF, blue: 0xFF}, + 'beige': {red: 0xF5, green: 0xF5, blue: 0xDC}, + 'bisque': {red: 0xFF, green: 0xE4, blue: 0xC4}, + 'black': {red: 0x00, green: 0x00, blue: 0x00}, + 'blanchedalmond': {red: 0xFF, green: 0xEB, blue: 0xCD}, + 'blue': {red: 0x00, green: 0x00, blue: 0xFF}, + 'blueviolet': {red: 0x8A, green: 0x2B, blue: 0xE2}, + 'brown': {red: 0xA5, green: 0x2A, blue: 0x2A}, + 'burlywood': {red: 0xDE, green: 0xB8, blue: 0x87}, + 'cadetblue': {red: 0x5F, green: 0x9E, blue: 0xA0}, + 'chartreuse': {red: 0x7F, green: 0xFF, blue: 0x00}, + 'chocolate': {red: 0xD2, green: 0x69, blue: 0x1E}, + 'coral': {red: 0xFF, green: 0x7F, blue: 0x50}, + 'cornflowerblue': {red: 0x64, green: 0x95, blue: 0xED}, + 'cornsilk': {red: 0xFF, green: 0xF8, blue: 0xDC}, + 'crimson': {red: 0xDC, green: 0x14, blue: 0x3C}, + 'cyan': {red: 0x00, green: 0xFF, blue: 0xFF}, + 'darkblue': {red: 0x00, green: 0x00, blue: 0x8B}, + 'darkcyan': {red: 0x00, green: 0x8B, blue: 0x8B}, + 'darkgoldenrod': {red: 0xB8, green: 0x86, blue: 0x0B}, + 'darkgray': {red: 0xA9, green: 0xA9, blue: 0xA9}, + 'darkgreen': {red: 0x00, green: 0x64, blue: 0x00}, + 'darkgrey': {red: 0xA9, green: 0xA9, blue: 0xA9}, + 'darkkhaki': {red: 0xBD, green: 0xB7, blue: 0x6B}, + 'darkmagenta': {red: 0x8B, green: 0x00, blue: 0x8B}, + 'darkolivegreen': {red: 0x55, green: 0x6B, blue: 0x2F}, + 'darkorange': {red: 0xFF, green: 0x8C, blue: 0x00}, + 'darkorchid': {red: 0x99, green: 0x32, blue: 0xCC}, + 'darkred': {red: 0x8B, green: 0x00, blue: 0x00}, + 'darksalmon': {red: 0xE9, green: 0x96, blue: 0x7A}, + 'darkseagreen': {red: 0x8F, green: 0xBC, blue: 0x8F}, + 'darkslateblue': {red: 0x48, green: 0x3D, blue: 0x8B}, + 'darkslategray': {red: 0x2F, green: 0x4F, blue: 0x4F}, + 'darkslategrey': {red: 0x2F, green: 0x4F, blue: 0x4F}, + 'darkturquoise': {red: 0x00, green: 0xCE, blue: 0xD1}, + 'darkviolet': {red: 0x94, green: 0x00, blue: 0xD3}, + 'deeppink': {red: 0xFF, green: 0x14, blue: 0x93}, + 'deepskyblue': {red: 0x00, green: 0xBF, blue: 0xFF}, + 'dimgray': {red: 0x69, green: 0x69, blue: 0x69}, + 'dimgrey': {red: 0x69, green: 0x69, blue: 0x69}, + 'dodgerblue': {red: 0x1E, green: 0x90, blue: 0xFF}, + 'firebrick': {red: 0xB2, green: 0x22, blue: 0x22}, + 'floralwhite': {red: 0xFF, green: 0xFA, blue: 0xF0}, + 'forestgreen': {red: 0x22, green: 0x8B, blue: 0x22}, + 'fuchsia': {red: 0xFF, green: 0x00, blue: 0xFF}, + 'gainsboro': {red: 0xDC, green: 0xDC, blue: 0xDC}, + 'ghostwhite': {red: 0xF8, green: 0xF8, blue: 0xFF}, + 'gold': {red: 0xFF, green: 0xD7, blue: 0x00}, + 'goldenrod': {red: 0xDA, green: 0xA5, blue: 0x20}, + 'gray': {red: 0x80, green: 0x80, blue: 0x80}, + 'green': {red: 0x00, green: 0x80, blue: 0x00}, + 'greenyellow': {red: 0xAD, green: 0xFF, blue: 0x2F}, + 'grey': {red: 0x80, green: 0x80, blue: 0x80}, + 'honeydew': {red: 0xF0, green: 0xFF, blue: 0xF0}, + 'hotpink': {red: 0xFF, green: 0x69, blue: 0xB4}, + 'indianred': {red: 0xCD, green: 0x5C, blue: 0x5C}, + 'indigo': {red: 0x4B, green: 0x00, blue: 0x82}, + 'ivory': {red: 0xFF, green: 0xFF, blue: 0xF0}, + 'khaki': {red: 0xF0, green: 0xE6, blue: 0x8C}, + 'lavender': {red: 0xE6, green: 0xE6, blue: 0xFA}, + 'lavenderblush': {red: 0xFF, green: 0xF0, blue: 0xF5}, + 'lawngreen': {red: 0x7C, green: 0xFC, blue: 0x00}, + 'lemonchiffon': {red: 0xFF, green: 0xFA, blue: 0xCD}, + 'lightblue': {red: 0xAD, green: 0xD8, blue: 0xE6}, + 'lightcoral': {red: 0xF0, green: 0x80, blue: 0x80}, + 'lightcyan': {red: 0xE0, green: 0xFF, blue: 0xFF}, + 'lightgoldenrodyellow': {red: 0xFA, green: 0xFA, blue: 0xD2}, + 'lightgray': {red: 0xD3, green: 0xD3, blue: 0xD3}, + 'lightgreen': {red: 0x90, green: 0xEE, blue: 0x90}, + 'lightgrey': {red: 0xD3, green: 0xD3, blue: 0xD3}, + 'lightpink': {red: 0xFF, green: 0xB6, blue: 0xC1}, + 'lightsalmon': {red: 0xFF, green: 0xA0, blue: 0x7A}, + 'lightseagreen': {red: 0x20, green: 0xB2, blue: 0xAA}, + 'lightskyblue': {red: 0x87, green: 0xCE, blue: 0xFA}, + 'lightslategray': {red: 0x77, green: 0x88, blue: 0x99}, + 'lightslategrey': {red: 0x77, green: 0x88, blue: 0x99}, + 'lightsteelblue': {red: 0xB0, green: 0xC4, blue: 0xDE}, + 'lightyellow': {red: 0xFF, green: 0xFF, blue: 0xE0}, + 'lime': {red: 0x00, green: 0xFF, blue: 0x00}, + 'limegreen': {red: 0x32, green: 0xCD, blue: 0x32}, + 'linen': {red: 0xFA, green: 0xF0, blue: 0xE6}, + 'magenta': {red: 0xFF, green: 0x00, blue: 0xFF}, + 'maroon': {red: 0x80, green: 0x00, blue: 0x00}, + 'mediumaquamarine': {red: 0x66, green: 0xCD, blue: 0xAA}, + 'mediumblue': {red: 0x00, green: 0x00, blue: 0xCD}, + 'mediumorchid': {red: 0xBA, green: 0x55, blue: 0xD3}, + 'mediumpurple': {red: 0x93, green: 0x70, blue: 0xDB}, + 'mediumseagreen': {red: 0x3C, green: 0xB3, blue: 0x71}, + 'mediumslateblue': {red: 0x7B, green: 0x68, blue: 0xEE}, + 'mediumspringgreen': {red: 0x00, green: 0xFA, blue: 0x9A}, + 'mediumturquoise': {red: 0x48, green: 0xD1, blue: 0xCC}, + 'mediumvioletred': {red: 0xC7, green: 0x15, blue: 0x85}, + 'midnightblue': {red: 0x19, green: 0x19, blue: 0x70}, + 'mintcream': {red: 0xF5, green: 0xFF, blue: 0xFA}, + 'mistyrose': {red: 0xFF, green: 0xE4, blue: 0xE1}, + 'moccasin': {red: 0xFF, green: 0xE4, blue: 0xB5}, + 'navajowhite': {red: 0xFF, green: 0xDE, blue: 0xAD}, + 'navy': {red: 0x00, green: 0x00, blue: 0x80}, + 'oldlace': {red: 0xFD, green: 0xF5, blue: 0xE6}, + 'olive': {red: 0x80, green: 0x80, blue: 0x00}, + 'olivedrab': {red: 0x6B, green: 0x8E, blue: 0x23}, + 'orange': {red: 0xFF, green: 0xA5, blue: 0x00}, + 'orangered': {red: 0xFF, green: 0x45, blue: 0x00}, + 'orchid': {red: 0xDA, green: 0x70, blue: 0xD6}, + 'palegoldenrod': {red: 0xEE, green: 0xE8, blue: 0xAA}, + 'palegreen': {red: 0x98, green: 0xFB, blue: 0x98}, + 'paleturquoise': {red: 0xAF, green: 0xEE, blue: 0xEE}, + 'palevioletred': {red: 0xDB, green: 0x70, blue: 0x93}, + 'papayawhip': {red: 0xFF, green: 0xEF, blue: 0xD5}, + 'peachpuff': {red: 0xFF, green: 0xDA, blue: 0xB9}, + 'peru': {red: 0xCD, green: 0x85, blue: 0x3F}, + 'pink': {red: 0xFF, green: 0xC0, blue: 0xCB}, + 'plum': {red: 0xDD, green: 0xA0, blue: 0xDD}, + 'powderblue': {red: 0xB0, green: 0xE0, blue: 0xE6}, + 'purple': {red: 0x80, green: 0x00, blue: 0x80}, + 'red': {red: 0xFF, green: 0x00, blue: 0x00}, + 'rosybrown': {red: 0xBC, green: 0x8F, blue: 0x8F}, + 'royalblue': {red: 0x41, green: 0x69, blue: 0xE1}, + 'saddlebrown': {red: 0x8B, green: 0x45, blue: 0x13}, + 'salmon': {red: 0xFA, green: 0x80, blue: 0x72}, + 'sandybrown': {red: 0xF4, green: 0xA4, blue: 0x60}, + 'seagreen': {red: 0x2E, green: 0x8B, blue: 0x57}, + 'seashell': {red: 0xFF, green: 0xF5, blue: 0xEE}, + 'sienna': {red: 0xA0, green: 0x52, blue: 0x2D}, + 'silver': {red: 0xC0, green: 0xC0, blue: 0xC0}, + 'skyblue': {red: 0x87, green: 0xCE, blue: 0xEB}, + 'slateblue': {red: 0x6A, green: 0x5A, blue: 0xCD}, + 'slategray': {red: 0x70, green: 0x80, blue: 0x90}, + 'slategrey': {red: 0x70, green: 0x80, blue: 0x90}, + 'snow': {red: 0xFF, green: 0xFA, blue: 0xFA}, + 'springgreen': {red: 0x00, green: 0xFF, blue: 0x7F}, + 'steelblue': {red: 0x46, green: 0x82, blue: 0xB4}, + 'tan': {red: 0xD2, green: 0xB4, blue: 0x8C}, + 'teal': {red: 0x00, green: 0x80, blue: 0x80}, + 'thistle': {red: 0xD8, green: 0xBF, blue: 0xD8}, + 'tomato': {red: 0xFF, green: 0x63, blue: 0x47}, + 'turquoise': {red: 0x40, green: 0xE0, blue: 0xD0}, + 'violet': {red: 0xEE, green: 0x82, blue: 0xEE}, + 'wheat': {red: 0xF5, green: 0xDE, blue: 0xB3}, + 'white': {red: 0xFF, green: 0xFF, blue: 0xFF}, + 'whitesmoke': {red: 0xF5, green: 0xF5, blue: 0xF5}, + 'yellow': {red: 0xFF, green: 0xFF, blue: 0x00}, + 'yellowgreen': {red: 0x9A, green: 0xCD, blue: 0x32}, + + 'transparent': {red: 0x00, green: 0x00, blue: 0x00, alpha: 0.0} +}; + +/** + * Color class allows cross-browser comparison of values, which can + * be returned from queryCommandValue in several formats: + * #ff00ff + * #f0f + * rgb(255, 0, 0) + * rgb(100%, 0%, 28%) // disabled for the time being (see below) + * rgba(127, 0, 64, 0.25) + * rgba(50%, 0%, 10%, 0.65) // disabled for the time being (see below) + * palegoldenrod + * transparent + * + * @constructor + * @param value {String} original value + */ +function Color(value) { + this.compare = function(other) { + if (!this.valid || !other.valid) { + return false; + } + if (this.alpha != other.alpha) { + return false; + } + if (this.alpha == 0.0) { + // both are fully transparent -> ignore the specific color information + return true; + } + // TODO(rolandsteiner): handle hsl/hsla values + return this.red == other.red && this.green == other.green && this.blue == other.blue; + } + this.parse = function(value) { + if (!value) + return false; + value = String(value).toLowerCase(); + var match; + // '#' + 6 hex digits, e.g., #ff3300 + match = value.match(/#([0-9a-f]{6})/i); + if (match) { + this.red = parseInt(match[1].substring(0, 2), 16); + this.green = parseInt(match[1].substring(2, 4), 16); + this.blue = parseInt(match[1].substring(4, 6), 16); + this.alpha = 1.0; + return true; + } + // '#' + 3 hex digits, e.g., #f30 + match = value.match(/#([0-9a-f]{3})/i); + if (match) { + this.red = parseInt(match[1].substring(0, 1), 16) * 16; + this.green = parseInt(match[1].substring(1, 2), 16) * 16; + this.blue = parseInt(match[1].substring(2, 3), 16) * 16; + this.alpha = 1.0; + return true; + } + // a color name, e.g., springgreen + match = colorChart[value]; + if (match) { + this.red = match.red; + this.green = match.green; + this.blue = match.blue; + this.alpha = (match.alpha === undefined) ? 1.0 : match.alpha; + return true; + } + // rgb(r, g, b), e.g., rgb(128, 12, 217) + match = value.match(/rgb\(([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/i); + if (match) { + this.red = Number(match[1]); + this.green = Number(match[2]); + this.blue = Number(match[3]); + this.alpha = 1.0; + return true; + } + // rgb(r%, g%, b%), e.g., rgb(100%, 0%, 50%) +// Commented out for the time being, since it seems likely that the resulting +// decimal values will create false negatives when compared with non-% values. +// +// => store as separate percent values and do exact matching when compared with % values +// and fuzzy matching when compared with non-% values? +// +// match = value.match(/rgb\(([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*\)/i); +// if (match) { +// this.red = Number(match[1]) * 255 / 100; +// this.green = Number(match[2]) * 255 / 100; +// this.blue = Number(match[3]) * 255 / 100; +// this.alpha = 1.0; +// return true; +// } + // rgba(r, g, b, a), e.g., rgb(128, 12, 217, 0.2) + match = value.match(/rgba\(([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/i); + if (match) { + this.red = Number(match[1]); + this.green = Number(match[2]); + this.blue = Number(match[3]); + this.alpha = Number(match[4]); + return true; + } + // rgba(r%, g%, b%, a), e.g., rgb(100%, 0%, 50%, 0.3) +// Commented out for the time being (cf. rgb() matching above) +// match = value.match(/rgba\(([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/i); +// if (match) { +// this.red = Number(match[1]) * 255 / 100; +// this.green = Number(match[2]) * 255 / 100; +// this.blue = Number(match[3]) * 255 / 100; +// this.alpha = Number(match[4]); +// return true; +// } + // TODO(rolandsteiner): handle "hsl(h, s, l)" and "hsla(h, s, l, a)" notation + return false; + } + this.toString = function() { + return this.valid ? this.red + ',' + this.green + ',' + this.blue : '(invalid)'; + } + this.toHexString = function() { + if (!this.valid) + return '(invalid)'; + return ((this.red < 16) ? '0' : '') + this.red.toString(16) + + ((this.green < 16) ? '0' : '') + this.green.toString(16) + + ((this.blue < 16) ? '0' : '') + this.blue.toString(16); + } + this.valid = this.parse(value); +} + +/** + * Utility class for converting font sizes to the size + * attribute in a font tag. Currently only converts px because + * only the sizes and px ever come from queryCommandValue. + * + * @constructor + * @param value {String} original value + */ +function FontSize(value) { + this.parse = function(str) { + if (!str) + this.valid = false; + var match; + if (match = String(str).match(/([0-9]+)px/)) { + var px = Number(match[1]); + if (px <= 0 || px > 47) + return false; + if (px <= 10) { + this.size = '1'; + } else if (px <= 13) { + this.size = '2'; + } else if (px <= 16) { + this.size = '3'; + } else if (px <= 18) { + this.size = '4'; + } else if (px <= 24) { + this.size = '5'; + } else if (px <= 32) { + this.size = '6'; + } else { + this.size = '7'; + } + return true; + } + if (match = String(str).match(/([+-][0-9]+)/)) { + this.size = match[1]; + return this.size >= 1 && this.size <= 7; + } + if (Number(str)) { + this.size = String(Number(str)); + return this.size >= 1 && this.size <= 7; + } + switch (str) { + case 'x-small': + this.size = '1'; + return true; + case 'small': + this.size = '2'; + return true; + case 'medium': + this.size = '3'; + return true; + case 'large': + this.size = '4'; + return true; + case 'x-large': + this.size = '5'; + return true; + case 'xx-large': + this.size = '6'; + return true; + case 'xxx-large': + this.size = '7'; + return true; + case '-webkit-xxx-large': + this.size = '7'; + return true; + case 'larger': + this.size = '+1'; + return true; + case 'smaller': + this.size = '-1'; + return true; + } + return false; + } + this.compare = function(other) { + return this.valid && other.valid && this.size === other.size; + } + this.toString = function() { + return this.valid ? this.size : '(invalid)'; + } + this.valid = this.parse(value); +} + +/** + * Utility class for converting & canonicalizing font names. + * + * @constructor + * @param value {String} original value + */ +function FontName(value) { + this.parse = function(str) { + if (!str) + return false; + str = String(str).toLowerCase(); + switch (str) { + case 'arial new': + this.fontname = 'arial'; + return true; + case 'courier new': + this.fontname = 'courier'; + return true; + case 'times new': + case 'times roman': + case 'times new roman': + this.fontname = 'times'; + return true; + } + this.fontname = value; + return true; + } + this.compare = function(other) { + return this.valid && other.valid && this.fontname === other.fontname; + } + this.toString = function() { + return this.valid ? this.fontname : '(invalid)'; + } + this.valid = this.parse(value); +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js new file mode 100644 index 000000000..cdc6f1e92 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js @@ -0,0 +1,227 @@ +/** + * @fileoverview + * Common constants and variables used in the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +// Constant for indicating a test setup is unsupported or incorrect +// (threw exception). +var INTERNAL_ERR = 'INTERNAL ERROR: '; +var SETUP_EXCEPTION = 'SETUP EXCEPTION: '; +var EXECUTION_EXCEPTION = 'EXECUTION EXCEPTION: '; +var VERIFICATION_EXCEPTION = 'VERIFICATION EXCEPTION: '; + +var SETUP_CONTAINER = 'WHEN INITIALIZING TEST CONTAINER'; +var SETUP_BAD_SELECTION_SPEC = 'BAD SELECTION SPECIFICATION IN TEST OR EXPECTATION STRING'; +var SETUP_HTML = 'WHEN SETTING TEST HTML'; +var SETUP_SELECTION = 'WHEN SETTING SELECTION'; +var SETUP_NOCOMMAND = 'NO COMMAND, GENERAL FUNCTION OR QUERY FUNCTION GIVEN'; +var HTML_COMPARISON = 'WHEN COMPARING OUTPUT HTML'; + +// Exceptiona to be thrown on unsupported selection operations +var SELMODIFY_UNSUPPORTED = 'UNSUPPORTED selection.modify()'; +var SELALLCHILDREN_UNSUPPORTED = 'UNSUPPORTED selection.selectAllChildren()'; + +// Output string for unsupported functions +// (returning bool 'false' as opposed to throwing an exception) +var UNSUPPORTED = '<i>false</i> (UNSUPPORTED)'; + +// HTML comparison result contants. +var VALRESULT_NOT_RUN = 0; // test hasn't been run yet +var VALRESULT_SETUP_EXCEPTION = 1; +var VALRESULT_EXECUTION_EXCEPTION = 2; +var VALRESULT_VERIFICATION_EXCEPTION = 3; +var VALRESULT_UNSUPPORTED = 4; +var VALRESULT_CANARY = 5; // HTML changes bled into the canary. +var VALRESULT_DIFF = 6; +var VALRESULT_ACCEPT = 7; // HTML technically correct, but not ideal. +var VALRESULT_EQUAL = 8; + +var VALOUTPUT = [ // IMPORTANT: this array MUST be coordinated with the values above!! + {css: 'grey', output: '???', title: 'The test has not been run yet.'}, // VALRESULT_NOT_RUN + {css: 'exception', output: 'EXC.', title: 'Exception was thrown during setup.'}, // VALRESULT_SETUP_EXCEPTION + {css: 'exception', output: 'EXC.', title: 'Exception was thrown during execution.'}, // VALRESULT_EXECUTION_EXCEPTION + {css: 'exception', output: 'EXC.', title: 'Exception was thrown during result verification.'}, // VALRESULT_VERIFICATION_EXCEPTION + {css: 'unsupported', output: 'UNS.', title: 'Unsupported command or value'}, // VALRESULT_UNSUPPORTED + {css: 'canary', output: 'CANARY', title: 'The command affected the contentEditable root element, or outside HTML.'}, // VALRESULT_CANARY + {css: 'fail', output: 'FAIL', title: 'The result differs from the expectation(s).'}, // VALRESULT_DIFF + {css: 'accept', output: 'ACC.', title: 'The result is technically correct, but sub-optimal.'}, // VALRESULT_ACCEPT + {css: 'pass', output: 'PASS', title: 'The test result matches the expectation.'} // VALRESULT_EQUAL +] + +// Selection comparison result contants. +var SELRESULT_NOT_RUN = 0; // test hasn't been run yet +var SELRESULT_CANARY = 1; // selection escapes the contentEditable element +var SELRESULT_DIFF = 2; +var SELRESULT_NA = 3; +var SELRESULT_ACCEPT = 4; // Selection is acceptable, but not ideal. +var SELRESULT_EQUAL = 5; + +var SELOUTPUT = [ // IMPORTANT: this array MUST be coordinated with the values above!! + {css: 'grey', output: 'grey', title: 'The test has not been run yet.'}, // SELRESULT_NOT_RUN + {css: 'canary', output: 'CANARY', title: 'The selection escaped the contentEditable boundary!'}, // SELRESULT_CANARY + {css: 'fail', output: 'FAIL', title: 'The selection differs from the expectation(s).'}, // SELRESULT_DIFF + {css: 'na', output: 'N/A', title: 'The correctness of the selection could not be verified.'}, // SELRESULT_NA + {css: 'accept', output: 'ACC.', title: 'The selection is technically correct, but sub-optimal.'}, // SELRESULT_ACCEPT + {css: 'pass', output: 'PASS', title: 'The selection matches the expectation.'} // SELRESULT_EQUAL +]; + +// RegExp for selection markers +var SELECTION_MARKERS = /[\[\]\{\}\|\^]/; + +// Special attributes used to mark selections within elements that otherwise +// have no children. Important: attribute name MUST be lower case! +var ATTRNAME_SEL_START = 'bsselstart'; +var ATTRNAME_SEL_END = 'bsselend'; + +// DOM node type constants. +var DOM_NODE_TYPE_ELEMENT = 1; +var DOM_NODE_TYPE_TEXT = 3; +var DOM_NODE_TYPE_COMMENT = 8; + +// Test parameter names +var PARAM_DESCRIPTION = 'desc'; +var PARAM_PAD = 'pad'; +var PARAM_EXECCOMMAND = 'command'; +var PARAM_FUNCTION = 'function'; +var PARAM_QUERYCOMMANDSUPPORTED = 'qcsupported'; +var PARAM_QUERYCOMMANDENABLED = 'qcenabled'; +var PARAM_QUERYCOMMANDINDETERM = 'qcindeterm'; +var PARAM_QUERYCOMMANDSTATE = 'qcstate'; +var PARAM_QUERYCOMMANDVALUE = 'qcvalue'; +var PARAM_VALUE = 'value'; +var PARAM_EXPECTED = 'expected'; +var PARAM_EXPECTED_OUTER = 'expOuter'; +var PARAM_ACCEPT = 'accept'; +var PARAM_ACCEPT_OUTER = 'accOuter'; +var PARAM_CHECK_ATTRIBUTES = 'checkAttrs'; +var PARAM_CHECK_STYLE = 'checkStyle'; +var PARAM_CHECK_CLASS = 'checkClass'; +var PARAM_CHECK_ID = 'checkID'; +var PARAM_STYLE_WITH_CSS = 'styleWithCSS'; + +// ID suffixes for the output columns +var IDOUT_TR = '_:TR:'; // per container +var IDOUT_TESTID = '_:tid'; // per test +var IDOUT_COMMAND = '_:cmd'; // per test +var IDOUT_VALUE = '_:val'; // per test +var IDOUT_CHECKATTRS = '_:att'; // per test +var IDOUT_CHECKSTYLE = '_:sty'; // per test +var IDOUT_CONTAINER = '_:cnt:'; // per container +var IDOUT_STATUSVAL = '_:sta:'; // per container +var IDOUT_STATUSSEL = '_:sel:'; // per container +var IDOUT_PAD = '_:pad'; // per test +var IDOUT_EXPECTED = '_:exp'; // per test +var IDOUT_ACTUAL = '_:act:'; // per container + +// Output strings to use for yes/no/NA +var OUTSTR_YES = '●'; +var OUTSTR_NO = '○'; +var OUTSTR_NA = '-'; + +// Tags at the start of HTML strings where they were taken from +var HTMLTAG_BODY = 'B:'; +var HTMLTAG_OUTER = 'O:'; +var HTMLTAG_INNER = 'I:'; + +// What to use for the canary +var CANARY = 'CAN<br>ARY'; + +// Containers for tests, and their associated DOM elements: +// iframe, win, doc, body, elem +var containers = [ + { id: 'dM', + iframe: null, + win: null, + doc: null, + body: null, + editor: null, + tagOpen: '<body>', + tagClose: '</body>', + editorID: null, + canary: '', + }, + { id: 'body', + iframe: null, + win: null, + doc: null, + body: null, + editor: null, + tagOpen: '<body contenteditable="true">', + tagClose: '</body>', + editorID: null, + canary: '' + }, + { id: 'div', + iframe: null, + win: null, + doc: null, + body: null, + editor: null, + tagOpen: '<div contenteditable="true" id="editor-div">', + tagClose: '</div>', + editorID: 'editor-div', + canary: CANARY + } +]; + +// Helper variables to use in test functions +var win = null; // window object to use for test functions +var doc = null; // document object to use for test functions +var body = null; // The <body> element of the current document +var editor = null; // The contentEditable element (i.e., the <body> or <div>) +var sel = null; // The current selection after the pad is set up + +// Canonicalization emit flags for various purposes +var emitFlagsForCanary = { + emitAttrs: true, + emitStyle: true, + emitClass: true, + emitID: true, + lowercase: true, + canonicalizeUnits: true +}; +var emitFlagsForOutput = { + emitAttrs: true, + emitStyle: true, + emitClass: true, + emitID: true, + lowercase: false, + canonicalizeUnits: false +}; + +// Shades of output colors +var colorShades = ['Lo', 'Hi']; + +// Classes of tests +var testClassIDs = ['Finalized', 'RFC', 'Proposed']; +var testClassCount = testClassIDs.length; + +// Dictionary storing the detailed test results. +var results = { + count: 0, + score: 0 +}; + +// Results - populated by the fillResults() function. +var beacon = []; + +// "compatibility" between Python and JS for test quines +var True = true; +var False = false; diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html new file mode 100644 index 000000000..62d917d69 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html @@ -0,0 +1,138 @@ +<!-- Legend --> +<TABLE CLASS="legend framed"> + <THEAD> + <TR><TH COLSPAN=3 CLASS="legendHdr">Result Description</TH></TR> + <TR><TH>Status</TH><TH ALIGN="LEFT">Meaning</TH><TH ALIGN="LEFT">Explanation</TH><TH>Scoring</TH></TR> + </THEAD> + <TBODY> + <TR CLASS="lo"><TD CLASS="pass" ALIGN="CENTER"> PASS </TD><TD CLASS="legend" ROWSPAN=2>Passed</TD><TD CLASS="legend" ROWSPAN=2>The result matches the expectation.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="pass">PASS (+1)</TD></TR> + <TR CLASS="hi"><TD CLASS="pass" ALIGN="CENTER"> PASS </TD></TR> + <TR CLASS="lo"><TD CLASS="accept" ALIGN="CENTER"> ACC. </TD><TD CLASS="legend" ROWSPAN=2>Acceptable</TD><TD CLASS="legend" ROWSPAN=2>The result is technically correct, but not ideal (too verbose, deprecated usage, etc.) - for informative purposes only.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="accept" ALIGN="CENTER"> ACC. </TD></TR> + <TR CLASS="lo"><TD CLASS="fail" ALIGN="CENTER"> FAIL </TD><TD CLASS="legend" ROWSPAN=2>Failure</TD><TD CLASS="legend" ROWSPAN=2>The result does not match any given expectation.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="fail" ALIGN="CENTER"> FAIL </TD></TR> + <TR CLASS="lo"><TD CLASS="canary" ALIGN="CENTER"> CANARY </TD><TD CLASS="legend" ROWSPAN=2>Canary</TD><TD CLASS="legend" ROWSPAN=2>The result changes HTML other than children of the contentEditable element.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="canary" ALIGN="CENTER"> CANARY </TD></TR> + <TR CLASS="lo"><TD CLASS="unsupported" ALIGN="CENTER"> UNS. </TD><TD CLASS="legend" ROWSPAN=2>Unsupported</TD><TD CLASS="legend" ROWSPAN=2>The specific function or value is unsupported (returned boolean 'false').</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="unsupported" ALIGN="CENTER"> UNS. </TD></TR> + <TR CLASS="lo"><TD CLASS="exception" ALIGN="CENTER"> EXC. </TD><TD CLASS="legend" ROWSPAN=2>Exception</TD><TD CLASS="legend" ROWSPAN=2>An unexpected exception was thrown during the execution of the test.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="exception" ALIGN="CENTER"> EXC. </TD></TR> + <TR CLASS="lo"><TD CLASS="na" ALIGN="CENTER"> N/A </TD><TD CLASS="legend" ROWSPAN=2>Not Applicable</TD><TD CLASS="legend" ROWSPAN=2>The selection could not be tested, because the tested function failed to return a known result.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="na" ALIGN="CENTER"> N/A </TD></TR> + </TBODY> +</TABLE> +<TABLE CLASS="legend framed"> + <THEAD> + <TR><TH COLSPAN=2 CLASS="legendHdr">Selection and Result Display</TH></TR> + <TR><TH>Character</TH><TH ALIGN="LEFT">Explanation</TH></TR> + </THEAD> + <TBODY> + <TR><TD CLASS="sel" ALIGN="CENTER">[</TD><TD>Start of selection - selection point is within a text node.</TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">]</TD><TD>End of selection - selection point is within a text node.</TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">^</TD><TD>Collapsed selection - selection point is within a text node.</TD></TR> + <TR><TD COLSPAN=2> </TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">{</TD><TD>Start of selection - selection point is within an element node.</TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">}</TD><TD>End of selection - selection point is within an element node.</TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">|</TD><TD>Collapsed selection - selection point is within an element node.</TD></TR> + <TR><TD COLSPAN=2> </TD></TR> + <TR><TD ALIGN="CENTER"><SPAN CLASS="fade">foo</SPAN></TD><TD>Greyed text indicates parts of the output that are ignored for the purposes of checking the result.</TD></TR> + <TR><TD ALIGN="CENTER"><SPAN CLASS="txt">foo</SPAN></TD><TD>Grey border indicates extent of text nodes in the result.</TD></TR> + </TBODY> +</TABLE> +<!-- progress meter --> +<HR ID="divider"> +<H1>Running Test Suites: {% for s in suites %}<A HREF="#{{ s.id }}" ID="{{ s.id }}-progress" STYLE="color: #eeeeee">{{ s.id }}</A> {% endfor %}<SPAN ID="done"> </SPAN></H1> +<HR> +<!-- main output --> +{% for s in suites %} + <H1 ID="{{ s.id }}"><A NAME="{{ s.id }}" HREF="#{{ s.id }}">{{ s.id }}</A> - {{ s.caption }}: + <SPAN ID="{{ s.id }}-{% ifequal s.id.0 'S' %}sel{% endifequal %}score">?/?</SPAN> + {% ifnotequal s.id.0 "Q" %}{% ifnotequal s.id.0 "S" %} + (Selection: <SPAN ID="{{ s.id }}-selscore">?/?</SPAN>) + {% endifnotequal %}{% endifnotequal %} + (time: <SPAN ID="{{ s.id }}-time">?</SPAN> ms) + </H1> + {% if s.comment %} + <DIV CLASS="comment">{{ s.comment|safe }}</DIV> + {% endif %} + {% for cls in classes %}{% for pk, pv in s.items %}{% ifequal pk cls %} + <H2 ID="{{ s.id }}-{{ cls }}"><A NAME="{{ s.id }}-{{ cls }}" HREF="#{{ s.id }}-{{ cls }}">{{ cls }} Tests</A>: + <SPAN ID="{{ s.id }}-{{ cls }}-{% ifequal s.id.0 'S' %}sel{% endifequal %}score">?/?</SPAN> + {% ifnotequal s.id.0 "Q" %}{% ifnotequal s.id.0 "S" %} + (Selection: <SPAN ID="{{ s.id }}-{{ cls }}-selscore">?/?</SPAN>) + {% endifnotequal %}{% endifnotequal %} + </H2> + <TABLE WIDTH=100%> + <THEAD> + <TR> + <TH TITLE="Unique ID of the test" ALIGN="LEFT">ID</TH> + <TH TITLE="Command or function used in the test" ALIGN="LEFT">Command</TH> + <TH TITLE="Value field for commands" ALIGN="LEFT">Value</TH> + {% ifnotequal s.id.0 "S" %}{% ifnotequal s.id.0 "Q" %}{% comment %} Don't output attribute and style columns for selection and "queryCommand..." tests. {% endcomment %} + <TH TITLE="check Atributes?">A</TH> + <TH TITLE="check Style">S</TH> + {% endifnotequal %}{% endifnotequal %} + <TH TITLE="Testing HTML Element">Env.</TH> + {% ifnotequal s.id.0 "S" %}{% comment %} Don't output HTML status column for selection tests. {% endcomment %} + <TH TITLE="State of the test">Status</TH> + {% endifnotequal %} + {% ifnotequal s.id.0 "Q" %}{% comment %} Don't output selection result column for "queryCommand..." tests. {% endcomment %} + <TH TITLE="State of the test regarding the selection">Selection</TH> + {% endifnotequal %} + <TH TITLE="Initial HTML and selection" ALIGN="LEFT">Initial</TH> + <TH TITLE="Expected HTML and selection" ALIGN="LEFT">Expected</TH> + <TH TITLE="Actual result HTML and selection" ALIGN="LEFT">Actual (lower case, canonicalized, selection marks)</TH> + <TH TITLE="Short description of the test" ALIGN="LEFT">Description</TH> + </TR> + </THEAD> + <TBODY> + {% for g in pv %}{% for t in g.tests %} + <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:dM" CLASS="{% cycle 'lo' 'lo' 'lo' 'hi' 'hi' 'hi' as shade %}"> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:tid"><A CLASS="idLabel" NAME="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}" HREF="#{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}">{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}</A></TD> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cmd"> </TD> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:val"> </TD> + {% ifnotequal s.id.0 "S" %}{% ifnotequal s.id.0 "Q" %}{% comment %} Don't output attribute and style columns for selection and "queryCommand..." tests. {% endcomment %} + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:att" ALIGN="CENTER"> </TD> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sty" ALIGN="CENTER"> </TD> + {% endifnotequal %}{% endifnotequal %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:dM" TITLE="designMode="on"" ALIGN="CENTER">dM</TD> + {% ifnotequal s.id.0 "S" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:dM" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + {% ifnotequal s.id.0 "Q" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:dM" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:pad"> </TD> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:exp"> </TD> + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:dM"><I>Processing...</I></TD> + <TD ROWSPAN=3>{{ t.desc|default:" " }}</TD> + </TR> + <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:body" CLASS="{% cycle shade %}"> + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:body" TITLE="<body contentEditable="true">" ALIGN="CENTER">body</TD> + {% ifnotequal s.id.0 "S" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:body" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + {% ifnotequal s.id.0 "Q" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:body" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:body"><I>Processing...</I></TD> + </TR> + <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:div" CLASS="{% cycle shade %}"> + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:div" TITLE="<div contentEditable="true">" ALIGN="CENTER">div</TD> + {% ifnotequal s.id.0 "S" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:div" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + {% ifnotequal s.id.0 "Q" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:div" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:div"><I>Processing...</I></TD> + </TR> + {% endfor %}{% endfor %} + </TBODY> + </TABLE> + {% endifequal %}{% endfor %}{% endfor %} +{% endfor %} + + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html new file mode 100644 index 000000000..98de8796d --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <title>New Rich Text Tests</title> + + <link rel="stylesheet" href="static/common.css" type="text/css"> + <link rel="stylesheet" href="static/editable.css" type="text/css"> + + <!-- utility scripts --> + <script src="static/js/variables.js"></script> + + <script src="static/js/canonicalize.js"></script> + <script src="static/js/compare.js"></script> + <script src="static/js/output.js"></script> + <script src="static/js/pad.js"></script> + <script src="static/js/range.js"></script> + <script src="static/js/units.js"></script> + + <script src="static/js/run.js"></script> + + <!-- new tests --> + <script type="text/javascript"> + {% autoescape off %} + + var commonIDPrefix = '{{ commonIDPrefix }}'; + {% for s in suites %} + var {{ s.id }}_TESTS = {{ s }}; + {% endfor %} + + /** + * Stuff to do after all tests are run: + * - write a nice "DONE!" at the end of the progress meter + * - beacon the results + * - remove the testing <iframe>s + */ + function finish() { + var span = document.getElementById('done'); + if (span) + span.innerHTML = ' ... DONE!'; + + fillResults(); + parent.sendScore(beacon, categoryTotals); + + cleanUp(); + } + + /** + * Run every individual suite, with a a brief timeout in between + * to allow for screen updates. + */ +{% for s in suites %} + {% if not forloop.first %} + setTimeout("runSuite{{ s.id }}()", 100); + } + {% endif %} + + function runSuite{{ s.id }}() { + runAndOutputTestSuite({{ s.id }}_TESTS); +{% endfor %} + finish(); + } + + /** + * Runs all tests in all suites. + */ + function doRunTests() { + initVariables(); + initEditorDocs(); + + // Start with the first test suite + runSuite{{ suites.0.id }}(); + } + + /** + * Runs after allowing for some time to have everything loaded + * (aka. horrible IE9 kludge) + */ + function runTests() { + setTimeout("doRunTests()", 1500); + } + + /** + * Removes the <iframe>s after all tests are finished + */ + function cleanUp() { + var e = document.getElementById('iframe-dM'); + e.parentNode.removeChild(e); + e = document.getElementById('iframe-body'); + e.parentNode.removeChild(e); + e = document.getElementById('iframe-div'); + e.parentNode.removeChild(e); + } + {% endautoescape %} + </script> +</head> + +<body onload="runTests()"> + {% include "richtext2/templates/output.html" %} + <hr> + <iframe name="iframe-dM" id="iframe-dM" src="static/editable-dM.html"></iframe> + <iframe name="iframe-body" id="iframe-body" src="static/editable-body.html"></iframe> + <iframe name="iframe-div" id="iframe-div" src="static/editable-div.html"></iframe> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py new file mode 100644 index 000000000..a1f5279ad --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py @@ -0,0 +1,17 @@ +__all__ = [ + 'apply', + 'applyCSS', + 'change', + 'changeCSS', + 'delete', + 'forwarddelete', + 'insert', + 'queryEnabled', + 'queryIndeterm', + 'queryState', + 'querySupported', + 'queryValue', + 'selection', + 'unapply', + 'unapplyCSS' +]
\ No newline at end of file diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py new file mode 100644 index 000000000..3eb465c84 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py @@ -0,0 +1,364 @@ + +APPLY_TESTS = { + 'id': 'A', + 'caption': 'Apply Formatting Tests', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] bold', + 'command': 'bold', + 'tests': [ + { 'id': 'B_TEXT-1_SI', + 'rte1-id': 'a-bold-0', + 'desc': 'Bold selection', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<b>[bar]</b>baz', + 'foo<strong>[bar]</strong>baz' ] }, + + { 'id': 'B_TEXT-1_SIR', + 'desc': 'Bold reversed selection', + 'pad': 'foo]bar[baz', + 'expected': [ 'foo<b>[bar]</b>baz', + 'foo<strong>[bar]</strong>baz' ] }, + + { 'id': 'B_I-1_SL', + 'desc': 'Bold selection, partially including italic', + 'pad': 'foo[bar<i>baz]qoz</i>quz', + 'expected': [ 'foo<b>[bar</b><i><b>baz]</b>qoz</i>quz', + 'foo<b>[bar<i>baz]</i></b><i>qoz</i>quz', + 'foo<strong>[bar</strong><i><strong>baz]</strong>qoz</i>quz', + 'foo<strong>[bar<i>baz]</i></strong><i>qoz</i>quz' ] } + ] + }, + + { 'desc': '[HTML5] italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_TEXT-1_SI', + 'rte1-id': 'a-italic-0', + 'desc': 'Italicize selection', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<i>[bar]</i>baz', + 'foo<em>[bar]</em>baz' ] } + ] + }, + + { 'desc': '[HTML5] underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_TEXT-1_SI', + 'rte1-id': 'a-underline-0', + 'desc': 'Underline selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<u>[bar]</u>baz' } + ] + }, + + { 'desc': '[HTML5] strikethrough', + 'command': 'strikethrough', + 'tests': [ + { 'id': 'S_TEXT-1_SI', + 'rte1-id': 'a-strikethrough-0', + 'desc': 'Strike-through selection', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<s>[bar]</s>baz', + 'foo<strike>[bar]</strike>baz', + 'foo<del>[bar]</del>baz' ] } + ] + }, + + { 'desc': '[HTML5] subscript', + 'command': 'subscript', + 'tests': [ + { 'id': 'SUB_TEXT-1_SI', + 'rte1-id': 'a-subscript-0', + 'desc': 'Change selection to subscript', + 'pad': 'foo[bar]baz', + 'expected': 'foo<sub>[bar]</sub>baz' } + ] + }, + + { 'desc': '[HTML5] superscript', + 'command': 'superscript', + 'tests': [ + { 'id': 'SUP_TEXT-1_SI', + 'rte1-id': 'a-superscript-0', + 'desc': 'Change selection to superscript', + 'pad': 'foo[bar]baz', + 'expected': 'foo<sup>[bar]</sup>baz' } + ] + }, + + { 'desc': '[HTML5] createlink', + 'command': 'createlink', + 'tests': [ + { 'id': 'CL:url_TEXT-1_SI', + 'rte1-id': 'a-createlink-0', + 'desc': 'create a link around the selection', + 'value': '#foo', + 'pad': 'foo[bar]baz', + 'expected': 'foo<a href="#foo">[bar]</a>baz' } + ] + }, + + { 'desc': '[HTML5] formatBlock', + 'command': 'formatblock', + 'tests': [ + { 'id': 'FB:H1_TEXT-1_SI', + 'rte1-id': 'a-formatblock-0', + 'desc': 'format the selection into a block: use <h1>', + 'value': 'h1', + 'pad': 'foo[bar]baz', + 'expected': '<h1>foo[bar]baz</h1>' }, + + { 'id': 'FB:P_TEXT-1_SI', + 'desc': 'format the selection into a block: use <p>', + 'value': 'p', + 'pad': 'foo[bar]baz', + 'expected': '<p>foo[bar]baz</p>' }, + + { 'id': 'FB:PRE_TEXT-1_SI', + 'desc': 'format the selection into a block: use <pre>', + 'value': 'pre', + 'pad': 'foo[bar]baz', + 'expected': '<pre>foo[bar]baz</pre>' }, + + { 'id': 'FB:ADDRESS_TEXT-1_SI', + 'desc': 'format the selection into a block: use <address>', + 'value': 'address', + 'pad': 'foo[bar]baz', + 'expected': '<address>foo[bar]baz</address>' }, + + { 'id': 'FB:BQ_TEXT-1_SI', + 'desc': 'format the selection into a block: use <blockquote>', + 'value': 'blockquote', + 'pad': 'foo[bar]baz', + 'expected': '<blockquote>foo[bar]baz</blockquote>' }, + + { 'id': 'FB:BQ_BR.BR-1_SM', + 'desc': 'format a multi-line selection into a block: use <blockquote>', + 'command': 'formatblock', + 'value': 'blockquote', + 'pad': 'fo[o<br>bar<br>b]az', + 'expected': '<blockquote>fo[o<br>bar<br>b]az</blockquote>' } + ] + }, + + + { 'desc': '[MIDAS] backcolor', + 'command': 'backcolor', + 'tests': [ + { 'id': 'BC:blue_TEXT-1_SI', + 'rte1-id': 'a-backcolor-0', + 'desc': 'Change background color (note: no non-CSS variant available)', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz', + 'foo<font style="background-color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] forecolor', + 'command': 'forecolor', + 'tests': [ + { 'id': 'FC:blue_TEXT-1_SI', + 'rte1-id': 'a-forecolor-0', + 'desc': 'Change the text color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font color="blue">[bar]</font>baz' } + ] + }, + + { 'desc': '[MIDAS] hilitecolor', + 'command': 'hilitecolor', + 'tests': [ + { 'id': 'HC:blue_TEXT-1_SI', + 'rte1-id': 'a-hilitecolor-0', + 'desc': 'Change the hilite color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz', + 'foo<font style="background-color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] fontname', + 'command': 'fontname', + 'tests': [ + { 'id': 'FN:a_TEXT-1_SI', + 'rte1-id': 'a-fontname-0', + 'desc': 'Change the font name', + 'value': 'arial', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font face="arial">[bar]</font>baz' } + ] + }, + + { 'desc': '[MIDAS] fontsize', + 'command': 'fontsize', + 'tests': [ + { 'id': 'FS:2_TEXT-1_SI', + 'rte1-id': 'a-fontsize-0', + 'desc': 'Change the font size to "2"', + 'value': '2', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font size="2">[bar]</font>baz' }, + + { 'id': 'FS:18px_TEXT-1_SI', + 'desc': 'Change the font size to "18px"', + 'value': '18px', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font size="18px">[bar]</font>baz' }, + + { 'id': 'FS:large_TEXT-1_SI', + 'desc': 'Change the font size to "large"', + 'value': 'large', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font size="large">[bar]</font>baz' } + ] + }, + + { 'desc': '[MIDAS] increasefontsize', + 'command': 'increasefontsize', + 'tests': [ + { 'id': 'INCFS:2_TEXT-1_SI', + 'desc': 'Decrease the font size (to small)', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<font size="4">[bar]</font>baz', + 'foo<font size="+1">[bar]</font>baz', + 'foo<big>[bar]</big>baz' ] } + ] + }, + + { 'desc': '[MIDAS] decreasefontsize', + 'command': 'decreasefontsize', + 'tests': [ + { 'id': 'DECFS:2_TEXT-1_SI', + 'rte1-id': 'a-decreasefontsize-0', + 'desc': 'Decrease the font size (to small)', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<font size="2">[bar]</font>baz', + 'foo<font size="-1">[bar]</font>baz', + 'foo<small>[bar]</small>baz' ] } + ] + }, + + { 'desc': '[MIDAS] indent (note: accept the de-facto standard indent of 40px)', + 'command': 'indent', + 'tests': [ + { 'id': 'IND_TEXT-1_SI', + 'rte1-id': 'a-indent-0', + 'desc': 'Indent the text (accept the de-facto standard of 40px indent)', + 'pad': 'foo[bar]baz', + 'checkAttrs': False, + 'expected': [ '<blockquote>foo[bar]baz</blockquote>', + '<div style="margin-left: 40px">foo[bar]baz</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="margin-left: 40px">foo[bar]baz</div>' } } + ] + }, + + { 'desc': '[MIDAS] outdent (-> unapply tests)', + 'command': 'outdent', + 'tests': [ + ] + }, + + { 'desc': '[MIDAS] justifycenter', + 'command': 'justifycenter', + 'tests': [ + { 'id': 'JC_TEXT-1_SC', + 'rte1-id': 'a-justifycenter-0', + 'desc': 'justify the text centrally', + 'pad': 'foo^bar', + 'expected': [ '<center>foo^bar</center>', + '<p align="center">foo^bar</p>', + '<p align="middle">foo^bar</p>', + '<div align="center">foo^bar</div>', + '<div align="middle">foo^bar</div>' ], + 'div': { + 'accOuter': [ '<div align="center" contenteditable="true">foo^bar</div>', + '<div align="middle" contenteditable="true">foo^bar</div>' ] } } + ] + }, + + { 'desc': '[MIDAS] justifyfull', + 'command': 'justifyfull', + 'tests': [ + { 'id': 'JF_TEXT-1_SC', + 'rte1-id': 'a-justifyfull-0', + 'desc': 'justify the text fully', + 'pad': 'foo^bar', + 'expected': [ '<p align="justify">foo^bar</p>', + '<div align="justify">foo^bar</div>' ], + 'div': { + 'accOuter': '<div align="justify" contenteditable="true">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyleft', + 'command': 'justifyleft', + 'tests': [ + { 'id': 'JL_TEXT-1_SC', + 'rte1-id': 'a-justifyleft-0', + 'desc': 'justify the text left', + 'pad': 'foo^bar', + 'expected': [ '<p align="left">foo^bar</p>', + '<div align="left">foo^bar</div>' ], + 'div': { + 'accOuter': '<div align="left" contenteditable="true">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyright', + 'command': 'justifyright', + 'tests': [ + { 'id': 'JR_TEXT-1_SC', + 'rte1-id': 'a-justifyright-0', + 'desc': 'justify the text right', + 'pad': 'foo^bar', + 'expected': [ '<p align="right">foo^bar</p>', + '<div align="right">foo^bar</div>' ], + 'div': { + 'accOuter': '<div align="right" contenteditable="true">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] heading', + 'command': 'heading', + 'tests': [ + { 'id': 'H:H1_TEXT-1_SC', + 'desc': 'create a heading from the paragraph that contains the selection', + 'value': 'h1', + 'pad': 'foo[bar]baz', + 'expected': '<h1>foo[bar]baz</h1>' } + ] + }, + + + { 'desc': '[Other] createbookmark', + 'command': 'createbookmark', + 'tests': [ + { 'id': 'CB:name_TEXT-1_SI', + 'rte1-id': 'a-createbookmark-0', + 'desc': 'create a bookmark (named link) around selection', + 'value': 'created', + 'pad': 'foo[bar]baz', + 'expected': 'foo<a name="created">[bar]</a>baz' } + ] + } + ] +} + + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py new file mode 100644 index 000000000..94cdad83f --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py @@ -0,0 +1,244 @@ + +APPLY_TESTS_CSS = { + 'id': 'AC', + 'caption': 'Apply Formatting Tests, using styleWithCSS', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': True, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] bold', + 'command': 'bold', + 'tests': [ + { 'id': 'B_TEXT-1_SI', + 'rte1-id': 'a-bold-1', + 'desc': 'Bold selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="font-weight: bold">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_TEXT-1_SI', + 'rte1-id': 'a-italic-1', + 'desc': 'Italicize selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="font-style: italic">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_TEXT-1_SI', + 'rte1-id': 'a-underline-1', + 'desc': 'Underline selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="text-decoration: underline">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] strikethrough', + 'command': 'strikethrough', + 'tests': [ + { 'id': 'S_TEXT-1_SI', + 'rte1-id': 'a-strikethrough-1', + 'desc': 'Strike-through selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="text-decoration: line-through">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] subscript', + 'command': 'subscript', + 'tests': [ + { 'id': 'SUB_TEXT-1_SI', + 'rte1-id': 'a-subscript-1', + 'desc': 'Change selection to subscript', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="vertical-align: sub">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] superscript', + 'command': 'superscript', + 'tests': [ + { 'id': 'SUP_TEXT-1_SI', + 'rte1-id': 'a-superscript-1', + 'desc': 'Change selection to superscript', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="vertical-align: super">[bar]</span>baz' } + ] + }, + + + { 'desc': '[MIDAS] backcolor', + 'command': 'backcolor', + 'tests': [ + { 'id': 'BC:blue_TEXT-1_SI', + 'rte1-id': 'a-backcolor-1', + 'desc': 'Change background color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz', + 'foo<font style="background-color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] forecolor', + 'command': 'forecolor', + 'tests': [ + { 'id': 'FC:blue_TEXT-1_SI', + 'rte1-id': 'a-forecolor-1', + 'desc': 'Change the text color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="color: blue">[bar]</span>baz', + 'foo<font style="color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] hilitecolor', + 'command': 'hilitecolor', + 'tests': [ + { 'id': 'HC:blue_TEXT-1_SI', + 'rte1-id': 'a-hilitecolor-1', + 'desc': 'Change the hilite color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz', + 'foo<font style="background-color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] fontname', + 'command': 'fontname', + 'tests': [ + { 'id': 'FN:a_TEXT-1_SI', + 'rte1-id': 'a-fontname-1', + 'desc': 'Change the font name', + 'value': 'arial', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="font-family: arial">[bar]</span>baz', + 'foo<font style="font-family: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] fontsize', + 'command': 'fontsize', + 'tests': [ + { 'id': 'FS:2_TEXT-1_SI', + 'rte1-id': 'a-fontsize-1', + 'desc': 'Change the font size to "2"', + 'value': '2', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="font-size: small">[bar]</span>baz', + 'foo<font style="font-size: small">[bar]</font>baz' ] }, + + { 'id': 'FS:18px_TEXT-1_SI', + 'desc': 'Change the font size to "18px"', + 'value': '18px', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="font-size: 18px">[bar]</span>baz', + 'foo<font style="font-size: 18px">[bar]</font>baz' ] }, + + { 'id': 'FS:large_TEXT-1_SI', + 'desc': 'Change the font size to "large"', + 'value': 'large', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="font-size: large">[bar]</span>baz', + 'foo<font style="font-size: large">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] indent', + 'command': 'indent', + 'tests': [ + { 'id': 'IND_TEXT-1_SI', + 'rte1-id': 'a-indent-1', + 'desc': 'Indent the text (assume "standard" 40px)', + 'pad': 'foo[bar]baz', + 'expected': [ '<div style="margin-left: 40px">foo[bar]baz</div>', + '<div style="margin: 0 0 0 40px">foo[bar]baz</div>', + '<blockquote style="margin-left: 40px">foo[bar]baz</blockquote>', + '<blockquote style="margin: 0 0 0 40px">foo[bar]baz</blockquote>' ], + 'div': { + 'accOuter': [ '<div contenteditable="true" style="margin-left: 40px">foo[bar]baz</div>', + '<div contenteditable="true" style="margin: 0 0 0 40px">foo[bar]baz</div>' ] } } + ] + }, + + { 'desc': '[MIDAS] outdent (-> unapply tests)', + 'command': 'outdent', + 'tests': [ + ] + }, + + { 'desc': '[MIDAS] justifycenter', + 'command': 'justifycenter', + 'tests': [ + { 'id': 'JC_TEXT-1_SC', + 'rte1-id': 'a-justifycenter-1', + 'desc': 'justify the text centrally', + 'pad': 'foo^bar', + 'expected': [ '<p style="text-align: center">foo^bar</p>', + '<div style="text-align: center">foo^bar</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="text-align: center">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyfull', + 'command': 'justifyfull', + 'tests': [ + { 'id': 'JF_TEXT-1_SC', + 'rte1-id': 'a-justifyfull-1', + 'desc': 'justify the text fully', + 'pad': 'foo^bar', + 'expected': [ '<p style="text-align: justify">foo^bar</p>', + '<div style="text-align: justify">foo^bar</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="text-align: justify">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyleft', + 'command': 'justifyleft', + 'tests': [ + { 'id': 'JL_TEXT-1_SC', + 'rte1-id': 'a-justifyleft-1', + 'desc': 'justify the text left', + 'pad': 'foo^bar', + 'expected': [ '<p style="text-align: left">foo^bar</p>', + '<div style="text-align: left">foo^bar</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="text-align: left">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyright', + 'command': 'justifyright', + 'tests': [ + { 'id': 'JR_TEXT-1_SC', + 'rte1-id': 'a-justifyright-1', + 'desc': 'justify the text right', + 'pad': 'foo^bar', + 'expected': [ '<p style="text-align: right">foo^bar</p>', + '<div style="text-align: right">foo^bar</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="text-align: right">foo^bar</div>' } } + ] + } + ] +} + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py new file mode 100644 index 000000000..6a76d3d5f --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py @@ -0,0 +1,273 @@ + +CHANGE_TESTS = { + 'id': 'C', + 'caption': 'Change Existing Format to Different Format Tests', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_I-1_SL', + 'desc': 'Italicize partially italicized text', + 'pad': 'foo[bar<i>baz]</i>qoz', + 'expected': 'foo<i>[barbaz]</i>qoz' }, + + { 'id': 'I_B-I-1_SO', + 'desc': 'Italicize partially italicized text in bold context', + 'pad': '<b>foo[bar<i>baz</i>}</b>', + 'expected': '<b>foo<i>[barbaz]</i></b>' } + ] + }, + + { 'desc': '[HTML5] underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_U-1_SO', + 'desc': 'Underline partially underlined text', + 'pad': 'foo[bar<u>baz</u>qoz]quz', + 'expected': 'foo<u>[barbazqoz]</u>quz' }, + + { 'id': 'U_U-1_SL', + 'desc': 'Underline partially underlined text', + 'pad': 'foo[bar<u>baz]qoz</u>quz', + 'expected': 'foo<u>[barbaz]qoz</u>quz' }, + + { 'id': 'U_S-U-1_SO', + 'desc': 'Underline partially underlined text in striked context', + 'pad': '<s>foo[bar<u>baz</u>}</s>', + 'expected': '<s>foo<u>[barbaz]</u></s>' } + ] + }, + + + { 'desc': '[MIDAS] backcolor', + 'command': 'backcolor', + 'tests': [ + { 'id': 'BC:842_FONTs:bc:fca-1_SW', + 'rte1-id': 'c-backcolor-0', + 'desc': 'Change background color to new color', + 'value': '#884422', + 'pad': '<font style="background-color: #ffccaa">[foobarbaz]</font>', + 'expected': [ '<font style="background-color: #884422">[foobarbaz]</font>', + '<span style="background-color: #884422">[foobarbaz]</span>' ] }, + + { 'id': 'BC:00f_SPANs:bc:f00-1_SW', + 'rte1-id': 'c-backcolor-2', + 'desc': 'Change background color to new color', + 'value': '#0000ff', + 'pad': '<span style="background-color: #ff0000">[foobarbaz]</span>', + 'expected': [ '<font style="background-color: #0000ff">[foobarbaz]</font>', + '<span style="background-color: #0000ff">[foobarbaz]</span>' ] }, + + { 'id': 'BC:ace_FONT.ass.s:bc:rgb-1_SW', + 'rte1-id': 'c-backcolor-1', + 'desc': 'Change background color in styled span to new color', + 'value': '#aaccee', + 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">[foobarbaz]</span>', + 'expected': [ '<font style="background-color: #aaccee">[foobarbaz]</font>', + '<span style="background-color: #aaccee">[foobarbaz]</span>' ] } + ] + }, + + { 'desc': '[MIDAS] forecolor', + 'command': 'forecolor', + 'tests': [ + { 'id': 'FC:g_FONTc:b-1_SW', + 'rte1-id': 'c-forecolor-0', + 'desc': 'Change the text color (without CSS)', + 'value': 'green', + 'pad': '<font color="blue">[foobarbaz]</font>', + 'expected': '<font color="green">[foobarbaz]</font>' }, + + { 'id': 'FC:g_SPANs:c:g-1_SW', + 'rte1-id': 'c-forecolor-1', + 'desc': 'Change the text color from a styled span (without CSS)', + 'value': 'green', + 'pad': '<span style="color: blue">[foobarbaz]</span>', + 'expected': '<font color="green">[foobarbaz]</font>' }, + + { 'id': 'FC:g_FONTc:b.s:c:r-1_SW', + 'rte1-id': 'c-forecolor-2', + 'desc': 'Change the text color from conflicting color and style (without CSS)', + 'value': 'green', + 'pad': '<font color="blue" style="color: red">[foobarbaz]</font>', + 'expected': '<font color="green">[foobarbaz]</font>' }, + + { 'id': 'FC:g_FONTc:b.sz:6-1_SI', + 'desc': 'Change the font color in content with a different font size and font color', + 'value': 'green', + 'pad': '<font color="blue" size="6">foo[bar]baz</font>', + 'expected': [ '<font color="blue" size="6">foo<font color="green">[bar]</font>baz</font>', + '<font size="6"><font color="blue">foo<font color="green">[bar]</font><font color="blue">baz</font></font>' ] } + ] + }, + + { 'desc': '[MIDAS] hilitecolor', + 'command': 'hilitecolor', + 'tests': [ + { 'id': 'HC:g_FONTs:c:b-1_SW', + 'rte1-id': 'c-hilitecolor-0', + 'desc': 'Change the hilite color (without CSS)', + 'value': 'green', + 'pad': '<font style="background-color: blue">[foobarbaz]</font>', + 'expected': [ '<font style="background-color: green">[foobarbaz]</font>', + '<span style="background-color: green">[foobarbaz]</span>' ] }, + + { 'id': 'HC:g_SPANs:c:g-1_SW', + 'rte1-id': 'c-hilitecolor-2', + 'desc': 'Change the hilite color from a styled span (without CSS)', + 'value': 'green', + 'pad': '<span style="background-color: blue">[foobarbaz]</span>', + 'expected': '<span style="background-color: green">[foobarbaz]</span>' }, + + { 'id': 'HC:g_SPAN.ass.s:c:rgb-1_SW', + 'rte1-id': 'c-hilitecolor-1', + 'desc': 'Change the hilite color from a styled span (without CSS)', + 'value': 'green', + 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">[foobarbaz]</span>', + 'expected': '<span style="background-color: green">[foobarbaz]</span>' } + ] + }, + + { 'desc': '[MIDAS] fontname', + 'command': 'fontname', + 'tests': [ + { 'id': 'FN:c_FONTf:a-1_SW', + 'rte1-id': 'c-fontname-0', + 'desc': 'Change existing font name to new font name (without CSS)', + 'value': 'courier', + 'pad': '<font face="arial">[foobarbaz]</font>', + 'expected': '<font face="courier">[foobarbaz]</font>' }, + + { 'id': 'FN:c_SPANs:ff:a-1_SW', + 'rte1-id': 'c-fontname-1', + 'desc': 'Change existing font name from style to new font name (without CSS)', + 'value': 'courier', + 'pad': '<span style="font-family: arial">[foobarbaz]</span>', + 'expected': '<font face="courier">[foobarbaz]</font>' }, + + { 'id': 'FN:c_FONTf:a.s:ff:v-1_SW', + 'rte1-id': 'c-fontname-2', + 'desc': 'Change existing font name with conflicting face and style to new font name (without CSS)', + 'value': 'courier', + 'pad': '<font face="arial" style="font-family: verdana">[foobarbaz]</font>', + 'expected': '<font face="courier">[foobarbaz]</font>' }, + + { 'id': 'FN:c_FONTf:a-1_SI', + 'desc': 'Change existing font name to new font name, text partially selected', + 'value': 'courier', + 'pad': '<font face="arial">foo[bar]baz</font>', + 'expected': '<font face="arial">foo</font><font face="courier">[bar]</font><font face="arial">baz</font>', + 'accept': '<font face="arial">foo<font face="courier">[bar]</font>baz</font>' }, + + { 'id': 'FN:c_FONTf:a-2_SL', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': 'foo[bar<font face="arial">baz]qoz</font>', + 'expected': 'foo<font face="courier">[barbaz]</font><font face="arial">qoz</font>' }, + + { 'id': 'FN:c_FONTf:v-FONTf:a-1_SW', + 'rte1-id': 'c-fontname-3', + 'desc': 'Change existing font name in nested <font> tags to new font name (without CSS)', + 'value': 'courier', + 'pad': '<font face="verdana"><font face="arial">[foobarbaz]</font></font>', + 'expected': '<font face="courier">[foobarbaz]</font>', + 'accept': '<font face="verdana"><font face="courier">[foobarbaz]</font></font>' }, + + { 'id': 'FN:c_SPANs:ff:v-FONTf:a-1_SW', + 'rte1-id': 'c-fontname-4', + 'desc': 'Change existing font name in nested mixed tags to new font name (without CSS)', + 'value': 'courier', + 'pad': '<span style="font-family: verdana"><font face="arial">[foobarbaz]</font></span>', + 'expected': '<font face="courier">[foobarbaz]</font>', + 'accept': '<span style="font-family: verdana"><font face="courier">[foobarbaz]</font></span>' } + ] + }, + + { 'desc': '[MIDAS] fontsize', + 'command': 'fontsize', + 'tests': [ + { 'id': 'FS:1_FONTsz:4-1_SW', + 'rte1-id': 'c-fontsize-0', + 'desc': 'Change existing font size to new size (without CSS)', + 'value': '1', + 'pad': '<font size="4">[foobarbaz]</font>', + 'expected': '<font size="1">[foobarbaz]</font>' }, + + { 'id': 'FS:1_SPAN.ass.s:fs:large-1_SW', + 'rte1-id': 'c-fontsize-1', + 'desc': 'Change existing font size from styled span to new size (without CSS)', + 'value': '1', + 'pad': '<span class="Apple-style-span" style="font-size: large">[foobarbaz]</span>', + 'expected': '<font size="1">[foobarbaz]</font>' }, + + { 'id': 'FS:5_FONTsz:1.s:fs:xs-1_SW', + 'rte1-id': 'c-fontsize-2', + 'desc': 'Change existing font size from tag with conflicting size and style to new size (without CSS)', + 'value': '5', + 'pad': '<font size="1" style="font-size:x-small">[foobarbaz]</font>', + 'expected': '<font size="5">[foobarbaz]</font>' }, + + { 'id': 'FS:2_FONTc:b.sz:6-1_SI', + 'desc': 'Change the font size in content with a different font size and font color', + 'value': '2', + 'pad': '<font color="blue" size="6">foo[bar]baz</font>', + 'expected': [ '<font color="blue" size="6">foo<font size="2">[bar]</font>baz</font>', + '<font color="blue"><font size="6">foo</font><font size="2">[bar]</font><font size="6">baz</font></font>' ] }, + + { 'id': 'FS:larger_FONTsz:4', + 'desc': 'Change selection to use next larger font', + 'value': 'larger', + 'pad': '<font size="4">foo[bar]baz</font>', + 'expected': '<font size="4">foo<font size="larger">[bar]</font>baz</font>', + 'accept': '<font size="4">foo</font><font size="5">[bar]</font><font size="4">baz</font>' }, + + { 'id': 'FS:smaller_FONTsz:4', + 'desc': 'Change selection to use next smaller font', + 'value': 'smaller', + 'pad': '<font size="4">foo[bar]baz</font>', + 'expected': '<font size="4">foo<font size="smaller">[bar]</font>baz</font>', + 'accept': '<font size="4">foo</font><font size="3">[bar]</font><font size="4">baz</font>' } + ] + }, + + { 'desc': '[MIDAS] formatblock', + 'command': 'formatblock', + 'tests': [ + { 'id': 'FB:h1_ADDRESS-1_SW', + 'desc': 'change block from <address> to <h1>', + 'value': 'h1', + 'pad': '<address>foo [bar] baz</address>', + 'expected': '<h1>foo [bar] baz</h1>' }, + + { 'id': 'FB:h1_ADDRESS-FONTsz:4-1_SO', + 'desc': 'change block from <address> with partially formatted content to <h1>', + 'value': 'h1', + 'pad': '<address>foo [<font size="4">bar</font>] baz</address>', + 'expected': '<h1>foo [bar] baz</h1>' }, + + { 'id': 'FB:h1_ADDRESS-FONTsz:4-1_SW', + 'desc': 'change block from <address> with partially formatted content to <h1>', + 'value': 'h1', + 'pad': '<address>foo <font size="4">[bar]</font> baz</address>', + 'expected': '<h1>foo [bar] baz</h1>' }, + + { 'id': 'FB:h1_ADDRESS-FONT.ass.sz:4-1_SW', + 'desc': 'change block from <address> with partially formatted content to <h1>', + 'value': 'h1', + 'pad': '<address>foo <font class="Apple-style-span" size="4">[bar]</font> baz</address>', + 'expected': '<h1>foo [bar] baz</h1>' } + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py new file mode 100644 index 000000000..4862b9b73 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py @@ -0,0 +1,210 @@ + +CHANGE_TESTS_CSS = { + 'id': 'CC', + 'caption': 'Change Existing Format to Different Format Tests, using styleWithCSS', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': True, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_I-1_SL', + 'desc': 'Italicize partially italicized text', + 'pad': 'foo[bar<i>baz]</i>qoz', + 'expected': 'foo<span style="font-style: italic">[barbaz]</span>qoz' }, + + { 'id': 'I_B-1_SL', + 'desc': 'Italicize partially bolded text', + 'pad': 'foo[bar<b>baz]</b>qoz', + 'expected': 'foo<span style="font-style: italic">[bar<b>baz]</b></span>qoz', + 'accept': 'foo<span style="font-style: italic">[bar<b>baz</b>}</span>qoz' }, + + { 'id': 'I_B-1_SW', + 'desc': 'Italicize bold text, ideally combining both', + 'pad': 'foobar<b>[baz]</b>qoz', + 'expected': 'foobar<span style="font-style: italic; font-weight: bold">[baz]</span>qoz', + 'accept': 'foobar<b><span style="font-style: italic">[baz]</span></b>qoz' } + ] + }, + + { 'desc': '[MIDAS] backcolor', + 'command': 'backcolor', + 'tests': [ + { 'id': 'BC:gray_SPANs:bc:b-1_SW', + 'desc': 'Change background color from blue to gray', + 'value': 'gray', + 'pad': '<span style="background-color: blue">[foobarbaz]</span>', + 'expected': '<span style="background-color: gray">[foobarbaz]</span>' }, + + { 'id': 'BC:gray_SPANs:bc:b-1_SO', + 'desc': 'Change background color from blue to gray', + 'value': 'gray', + 'pad': '{<span style="background-color: blue">foobarbaz</span>}', + 'expected': [ '{<span style="background-color: gray">foobarbaz</span>}', + '<span style="background-color: gray">[foobarbaz]</span>' ] }, + + { 'id': 'BC:gray_SPANs:bc:b-1_SI', + 'desc': 'Change background color from blue to gray', + 'value': 'gray', + 'pad': '<span style="background-color: blue">foo[bar]baz</span>', + 'expected': '<span style="background-color: blue">foo</span><span style="background-color: gray">[bar]</span><span style="background-color: blue">baz</span>', + 'accept': '<span style="background-color: blue">foo<span style="background-color: gray">[bar]</span>baz</span>' }, + + { 'id': 'BC:gray_P-SPANs:bc:b-1_SW', + 'desc': 'Change background color within a paragraph from blue to gray', + 'value': 'gray', + 'pad': '<p><span style="background-color: blue">[foobarbaz]</span></p>', + 'expected': [ '<p><span style="background-color: gray">[foobarbaz]</span></p>', + '<p style="background-color: gray">[foobarbaz]</p>' ] }, + + { 'id': 'BC:gray_P-SPANs:bc:b-2_SW', + 'desc': 'Change background color within a paragraph from blue to gray', + 'value': 'gray', + 'pad': '<p>foo<span style="background-color: blue">[bar]</span>baz</p>', + 'expected': '<p>foo<span style="background-color: gray">[bar]</span>baz</p>' }, + + { 'id': 'BC:gray_P-SPANs:bc:b-3_SO', + 'desc': 'Change background color within a paragraph from blue to gray (selection encloses more than previous span)', + 'value': 'gray', + 'pad': '<p>[foo<span style="background-color: blue">barbaz</span>qoz]quz</p>', + 'expected': '<p><span style="background-color: gray">[foobarbazqoz]</span>quz</p>' }, + + { 'id': 'BC:gray_P-SPANs:bc:b-3_SL', + 'desc': 'Change background color within a paragraph from blue to gray (previous span partially selected)', + 'value': 'gray', + 'pad': '<p>[foo<span style="background-color: blue">bar]baz</span>qozquz</p>', + 'expected': '<p><span style="background-color: gray">[foobar]</span><span style="background-color: blue">baz</span>qozquz</p>' }, + + { 'id': 'BC:gray_SPANs:bc:b-2_SL', + 'desc': 'Change background color from blue to gray on partially covered span, selection extends left', + 'value': 'gray', + 'pad': 'foo [bar <span style="background-color: blue">baz] qoz</span> quz sic', + 'expected': 'foo <span style="background-color: gray">[bar baz]</span><span style="background-color: blue"> qoz</span> quz sic' }, + + { 'id': 'BC:gray_SPANs:bc:b-2_SR', + 'desc': 'Change background color from blue to gray on partially covered span, selection extends right', + 'value': 'gray', + 'pad': 'foo bar <span style="background-color: blue">baz [qoz</span> quz] sic', + 'expected': 'foo bar <span style="background-color: blue">baz </span><span style="background-color: gray">[qoz quz]</span> sic' } + ] + }, + + { 'desc': '[MIDAS] fontname', + 'command': 'fontname', + 'tests': [ + { 'id': 'FN:c_SPANs:ff:a-1_SW', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': '<span style="font-family: arial">[foobarbaz]</span>', + 'expected': '<span style="font-family: courier">[foobarbaz]</span>' }, + + { 'id': 'FN:c_FONTf:a-1_SW', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': '<font face="arial">[foobarbaz]</font>', + 'expected': [ '<font style="font-family: courier">[foobarbaz]</font>', + '<span style="font-family: courier">[foobarbaz]</span>' ] }, + + { 'id': 'FN:c_FONTf:a-1_SI', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': '<font face="arial">foo[bar]baz</font>', + 'expected': '<font face="arial">foo</font><span style="font-family: courier">[bar]</span><font face="arial">baz</font>' }, + + { 'id': 'FN:a_FONTf:a-1_SI', + 'desc': 'Change existing font name to same font name, using CSS styling (should be noop)', + 'value': 'arial', + 'pad': '<font face="arial">foo[bar]baz</font>', + 'expected': '<font face="arial">foo[bar]baz</font>' }, + + { 'id': 'FN:a_FONTf:a-1_SW', + 'desc': 'Change existing font name to same font name, using CSS styling (should be noop or perhaps change tag)', + 'value': 'arial', + 'pad': '<font face="arial">[foobarbaz]</font>', + 'expected': [ '<font face="arial">[foobarbaz]</font>', + '<span style="font-family: arial">[foobarbaz]</span>' ] }, + + { 'id': 'FN:a_FONTf:a-1_SO', + 'desc': 'Change existing font name to same font name, using CSS styling (should be noop or perhaps change tag)', + 'value': 'arial', + 'pad': '{<font face="arial">foobarbaz</font>}', + 'expected': [ '{<font face="arial">foobarbaz</font>}', + '<font face="arial">[foobarbaz]</font>', + '{<span style="font-family: arial">foobarbaz</span>}', + '<span style="font-family: arial">[foobarbaz]</span>' ] }, + + { 'id': 'FN:a_SPANs:ff:a-1_SI', + 'desc': 'Change existing font name to same font name, using CSS styling (should be noop)', + 'value': 'arial', + 'pad': '<span style="font-family: arial">[foobarbaz]</span>', + 'expected': '<span style="font-family: arial">[foobarbaz]</span>' }, + + { 'id': 'FN:c_FONTf:a-2_SL', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': 'foo[bar<font face="arial">baz]qoz</font>', + 'expected': 'foo<span style="font-family: courier">[barbaz]</span><font face="arial">qoz</font>' } + ] + }, + + { 'desc': '[MIDAS] fontsize', + 'command': 'fontsize', + 'tests': [ + { 'id': 'FS:1_SPANs:fs:l-1_SW', + 'desc': 'Change existing font size to new size, using CSS styling', + 'value': '1', + 'pad': '<span style="font-size: large">[foobarbaz]</span>', + 'expected': '<span style="font-size: x-small">[foobarbaz]</span>' }, + + { 'id': 'FS:large_SPANs:fs:l-1_SW', + 'desc': 'Change existing font size to same size (should be noop)', + 'value': 'large', + 'pad': '<span style="font-size: large">[foobarbaz]</span>', + 'expected': '<span style="font-size: large">[foobarbaz]</span>' }, + + { 'id': 'FS:18px_SPANs:fs:l-1_SW', + 'desc': 'Change existing font size to equivalent px size (should be noop, or change unit)', + 'value': '18px', + 'pad': '<span style="font-size: large">[foobarbaz]</span>', + 'expected': [ '<span style="font-size: 18px">[foobarbaz]</span>', + '<span style="font-size: large">[foobarbaz]</span>' ] }, + + { 'id': 'FS:4_SPANs:fs:l-1_SW', + 'desc': 'Change existing font size to equivalent numeric size (should be noop)', + 'value': '4', + 'pad': '<span style="font-size: large">[foobarbaz]</span>', + 'expected': '<span style="font-size: large">[foobarbaz]</span>' }, + + { 'id': 'FS:4_SPANs:fs:18px-1_SW', + 'desc': 'Change existing font size to equivalent numeric size (should be noop)', + 'value': '4', + 'pad': '<span style="font-size: 18px">[foobarbaz]</span>', + 'expected': '<span style="font-size: 18px">[foobarbaz]</span>' }, + + { 'id': 'FS:larger_SPANs:fs:l-1_SI', + 'desc': 'Change selection to use next larger font', + 'value': 'larger', + 'pad': '<span style="font-size: large">foo[bar]baz</span>', + 'expected': [ '<span style="font-size: large">foo<span style="font-size: x-large">[bar]</span>baz</span>', + '<span style="font-size: large">foo</span><span style="font-size: x-large">[bar]</span><span style="font-size: large">baz</span>' ], + 'accept': '<span style="font-size: large">foo<font size="larger">[bar]</font>baz</span>' }, + + { 'id': 'FS:smaller_SPANs:fs:l-1_SI', + 'desc': 'Change selection to use next smaller font', + 'value': 'smaller', + 'pad': '<span style="font-size: large">foo[bar]baz</span>', + 'expected': [ '<span style="font-size: large">foo<span style="font-size: medium">[bar]</span>baz</span>', + '<span style="font-size: large">foo</span><span style="font-size: medium">[bar]</span><span style="font-size: large">baz</span>' ], + 'accept': '<span style="font-size: large">foo<font size="smaller">[bar]</font>baz</span>' } + ] + } + ] +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py new file mode 100644 index 000000000..0cc659225 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py @@ -0,0 +1,330 @@ + +DELETE_TESTS = { + 'id': 'D', + 'caption': 'Delete Tests', + 'command': 'delete', + 'checkAttrs': True, + 'checkStyle': False, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'delete single characters', + 'tests': [ + { 'id': 'CHAR-1_SC', + 'desc': 'Delete 1 character', + 'pad': 'foo^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-2_SC', + 'desc': 'Delete 1 pre-composed character o with diaeresis', + 'pad': 'foö^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-3_SC', + 'desc': 'Delete 1 character with combining diaeresis above', + 'pad': 'foö^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-4_SC', + 'desc': 'Delete 1 character with combining diaeresis below', + 'pad': 'foo̤^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SC', + 'desc': 'Delete 1 character with combining diaeresis above and below', + 'pad': 'foö̤^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SI-1', + 'desc': 'Delete 1 character with combining diaeresis above and below, selection on diaeresis above', + 'pad': 'foo[̈]̤barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SI-2', + 'desc': 'Delete 1 character with combining diaeresis above and below, selection on diaeresis below', + 'pad': 'foö[̤]barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SR', + 'desc': 'Delete 1 character with combining diaeresis above and below, selection oblique on diaeresis and following text', + 'pad': 'foö[̤bar]baz', + 'expected': 'fo^baz' }, + + { 'id': 'CHAR-6_SC', + 'desc': 'Delete 1 character with enclosing square', + 'pad': 'foo⃞^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-7_SC', + 'desc': 'Delete 1 character with combining long solidus overlay', + 'pad': 'foo̸^barbaz', + 'expected': 'fo^barbaz' } + ] + }, + + { 'desc': 'delete text selection', + 'tests': [ + { 'id': 'TEXT-1_SI', + 'desc': 'Delete text selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SS', + 'desc': 'Delete at start of span', + 'pad': 'foo<b>^bar</b>baz', + 'expected': 'fo^<b>bar</b>baz' }, + + { 'id': 'B-1_SA', + 'desc': 'Delete from position after span', + 'pad': 'foo<b>bar</b>^baz', + 'expected': 'foo<b>ba^</b>baz' }, + + { 'id': 'B-1_SW', + 'desc': 'Delete selection that wraps the whole span content', + 'pad': 'foo<b>[bar]</b>baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SO', + 'desc': 'Delete selection that wraps the whole span', + 'pad': 'foo[<b>bar</b>]baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SL', + 'desc': 'Delete oblique selection that starts before span', + 'pad': 'foo[bar<b>baz]quoz</b>quuz', + 'expected': 'foo^<b>quoz</b>quuz' }, + + { 'id': 'B-1_SR', + 'desc': 'Delete oblique selection that ends after span', + 'pad': 'foo<b>bar[baz</b>quoz]quuz', + 'expected': 'foo<b>bar^</b>quuz' }, + + { 'id': 'B.I-1_SM', + 'desc': 'Delete oblique selection that starts and ends in different spans', + 'pad': 'foo<b>bar[baz</b><i>qoz]quuz</i>quuuz', + 'expected': 'foo<b>bar^</b><i>quuz</i>quuuz' }, + + { 'id': 'GEN-1_SS', + 'desc': 'Delete at start of span with generated content', + 'pad': 'foo<gen>^bar</gen>baz', + 'expected': 'fo^<gen>bar</gen>baz' }, + + { 'id': 'GEN-1_SA', + 'desc': 'Delete from position after span with generated content', + 'pad': 'foo<gen>bar</gen>^baz', + 'expected': 'foo<gen>ba^</gen>baz' } + ] + }, + + { 'desc': 'delete paragraphs', + 'tests': [ + { 'id': 'P2-1_SS2', + 'desc': 'Delete from collapsed selection at start of paragraph - should merge with previous', + 'pad': '<p>foobar</p><p>^bazqoz</p>', + 'expected': '<p>foobar^bazqoz</p>' }, + + { 'id': 'P2-1_SI2', + 'desc': 'Delete non-collapsed selection at start of paragraph - should not merge with previous', + 'pad': '<p>foobar</p><p>[baz]qoz</p>', + 'expected': '<p>foobar</p><p>^qoz</p>' }, + + { 'id': 'P2-1_SM', + 'desc': 'Delete non-collapsed selection spanning 2 paragraphs - should merge them', + 'pad': '<p>foo[bar</p><p>baz]qoz</p>', + 'expected': '<p>foo^qoz</p>' } + ] + }, + + { 'desc': 'delete lists and list items', + 'tests': [ + { 'id': 'OL-LI2-1_SO1', + 'desc': 'Delete fully wrapped list item', + 'pad': 'foo<ol>{<li>bar</li>}<li>baz</li></ol>qoz', + 'expected': ['foo<ol>|<li>baz</li></ol>qoz', + 'foo<ol><li>^baz</li></ol>qoz'] }, + + { 'id': 'OL-LI2-1_SM', + 'desc': 'Delete oblique range between list items within same list', + 'pad': 'foo<ol><li>ba[r</li><li>b]az</li></ol>qoz', + 'expected': 'foo<ol><li>ba^az</li></ol>qoz' }, + + { 'id': 'OL-LI-1_SW', + 'desc': 'Delete contents of last list item (list should remain)', + 'pad': 'foo<ol><li>[foo]</li></ol>qoz', + 'expected': ['foo<ol><li>|</li></ol>qoz', + 'foo<ol><li>^</li></ol>qoz'] }, + + { 'id': 'OL-LI-1_SO', + 'desc': 'Delete last list item of list (should remove entire list)', + 'pad': 'foo<ol>{<li>foo</li>}</ol>qoz', + 'expected': 'foo^qoz' } + ] + }, + + { 'desc': 'delete with strange selections', + 'tests': [ + { 'id': 'HR.BR-1_SM', + 'desc': 'Delete selection that starts and ends within nodes that don\'t have children', + 'pad': 'foo<hr {>bar<br }>baz', + 'expected': 'foo<hr>|<br>baz' } + ] + }, + + { 'desc': 'delete after table', + 'tests': [ + { 'id': 'TABLE-1_SA', + 'desc': 'Delete from position immediately after table (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>bar</td></tr></tbody></table>^baz', + 'expected': 'foo<table><tbody><tr><td>bar</td></tr></tbody></table>^baz' } + ] + }, + + { 'desc': 'delete within table cells', + 'tests': [ + { 'id': 'TD-1_SS', + 'desc': 'Delete from start of first cell (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>^bar</td></tr></tbody></table>baz', + 'expected': 'foo<table><tbody><tr><td>^bar</td></tr></tbody></table>baz' }, + + { 'id': 'TD2-1_SS2', + 'desc': 'Delete from start of inner cell (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>bar</td><td>^baz</td></tr></tbody></table>quoz', + 'expected': 'foo<table><tbody><tr><td>bar</td><td>^baz</td></tr></tbody></table>quoz' }, + + { 'id': 'TD2-1_SM', + 'desc': 'Delete with selection spanning 2 cells', + 'pad': 'foo<table><tbody><tr><td>ba[r</td><td>b]az</td></tr></tbody></table>quoz', + 'expected': 'foo<table><tbody><tr><td>ba^</td><td>az</td></tr></tbody></table>quoz' } + ] + }, + + { 'desc': 'delete table rows', + 'tests': [ + { 'id': 'TR3-1_SO1', + 'desc': 'Delete first table row', + 'pad': '<table><tbody>{<tr><td>A</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>^B</td></tr><tr><td>C</td></tr></tbody></table>'] }, + + { 'id': 'TR3-1_SO2', + 'desc': 'Delete middle table row', + 'pad': '<table><tbody><tr><td>A</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td></tr>|<tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>A</td></tr><tr><td>^C</td></tr></tbody></table>'] }, + + { 'id': 'TR3-1_SO3', + 'desc': 'Delete last table row', + 'pad': '<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td></tr><tr><td>B^</td></tr></tbody></table>'] }, + + { 'id': 'TR2rs:2-1_SO1', + 'desc': 'Delete first table row where a cell has rowspan 2', + 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=2>R</td></tr>}<tr><td>B</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>B</td><td>R</td></tr></tbody></table>', + '<table><tbody><tr><td>^B</td><td>R</td></tr></tbody></table>'] }, + + { 'id': 'TR2rs:2-1_SO2', + 'desc': 'Delete second table row where a cell has rowspan 2', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=2>R</td></tr>{<tr><td>B</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td><td>R</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td><td>R^</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO1', + 'desc': 'Delete first table row where a cell has rowspan 3', + 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=3>R</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>^A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO2', + 'desc': 'Delete middle table row where a cell has rowspan 3', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr>|<tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr><tr><td>^C</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO3', + 'desc': 'Delete last table row where a cell has rowspan 3', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B^</td></tr></tbody></table>'] } + ] + }, + + { 'desc': 'delete with non-editable nested content', + 'tests': [ + { 'id': 'DIV:ce:false-1_SO', + 'desc': 'Delete nested non-editable <div>', + 'pad': 'foo[bar<div contenteditable="false">NESTED</div>baz]qoz', + 'expected': 'foo^qoz' }, + + { 'id': 'DIV:ce:false-1_SB', + 'desc': 'Delete from immediately after a nested non-editable <div> (should be no-op)', + 'pad': 'foobar<div contenteditable="false">NESTED</div>^bazqoz', + 'expected': 'foobar<div contenteditable="false">NESTED</div>^bazqoz' }, + + { 'id': 'DIV:ce:false-1_SL', + 'desc': 'Delete nested non-editable <div> with oblique selection', + 'pad': 'foo[bar<div contenteditable="false">NES]TED</div>bazqoz', + 'expected': [ 'foo^<div contenteditable="false">NESTED</div>bazqoz', + 'foo<div contenteditable="false">[NES]TED</div>bazqoz' ] }, + + { 'id': 'DIV:ce:false-1_SR', + 'desc': 'Delete nested non-editable <div> with oblique selection', + 'pad': 'foobar<div contenteditable="false">NES[TED</div>baz]qoz', + 'expected': [ 'foobar<div contenteditable="false">NESTED</div>^qoz', + 'foobar<div contenteditable="false">NES[TED]</div>qoz' ] }, + + { 'id': 'DIV:ce:false-1_SI', + 'desc': 'Delete inside nested non-editable <div> (should be no-op)', + 'pad': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz', + 'expected': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz' } + ] + }, + + { 'desc': 'Delete with display:inline-block', + 'checkStyle': True, + 'tests': [ + { 'id': 'SPAN:d:ib-1_SC', + 'desc': 'Delete inside an inline-block <span>', + 'pad': 'foo<span style="display: inline-block">bar^baz</span>qoz', + 'expected': 'foo<span style="display: inline-block">ba^baz</span>qoz' }, + + { 'id': 'SPAN:d:ib-1_SA', + 'desc': 'Delete from immediately after an inline-block <span>', + 'pad': 'foo<span style="display: inline-block">barbaz</span>^qoz', + 'expected': 'foo<span style="display: inline-block">barba^</span>qoz' }, + + { 'id': 'SPAN:d:ib-2_SL', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo[DEL<span style="display: inline-block">ETE]bar</span>baz', + 'expected': 'foo^<span style="display: inline-block">bar</span>baz' }, + + { 'id': 'SPAN:d:ib-3_SR', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">bar[DEL</span>ETE]baz', + 'expected': 'foo<span style="display: inline-block">bar^</span>baz' }, + + { 'id': 'SPAN:d:ib-4i_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">bar[DELETE]baz</span>qoz', + 'expected': 'foo<span style="display: inline-block">bar^baz</span>qoz' }, + + { 'id': 'SPAN:d:ib-4l_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">[DELETE]barbaz</span>qoz', + 'expected': 'foo<span style="display: inline-block">^barbaz</span>qoz' }, + + { 'id': 'SPAN:d:ib-4r_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">barbaz[DELETE]</span>qoz', + 'expected': 'foo<span style="display: inline-block">barbaz^</span>qoz' } + ] + } + ] +} + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py new file mode 100644 index 000000000..d625a2a7d --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py @@ -0,0 +1,315 @@ + +FORWARDDELETE_TESTS = { + 'id': 'FD', + 'caption': 'Forward-Delete Tests', + 'command': 'forwardDelete', + 'checkAttrs': True, + 'checkStyle': False, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'forward-delete single characters', + 'tests': [ + { 'id': 'CHAR-1_SC', + 'desc': 'Delete 1 character', + 'pad': 'foo^barbaz', + 'expected': 'foo^arbaz' }, + + { 'id': 'CHAR-2_SC', + 'desc': 'Delete 1 pre-composed character o with diaeresis', + 'pad': 'fo^öbarbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-3_SC', + 'desc': 'Delete 1 character with combining diaeresis above', + 'pad': 'fo^öbarbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-4_SC', + 'desc': 'Delete 1 character with combining diaeresis below', + 'pad': 'fo^o̤barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SC', + 'desc': 'Delete 1 character with combining diaeresis above and below', + 'pad': 'fo^ö̤barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-6_SC', + 'desc': 'Delete 1 character with enclosing square', + 'pad': 'fo^o⃞barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-7_SC', + 'desc': 'Delete 1 character with combining long solidus overlay', + 'pad': 'fo^o̸barbaz', + 'expected': 'fo^barbaz' } + ] + }, + + { 'desc': 'forward-delete text selections', + 'tests': [ + { 'id': 'TEXT-1_SI', + 'desc': 'Delete text selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SE', + 'desc': 'Forward-delete at end of span', + 'pad': 'foo<b>bar^</b>baz', + 'expected': 'foo<b>bar^</b>az' }, + + { 'id': 'B-1_SB', + 'desc': 'Forward-delete from position before span', + 'pad': 'foo^<b>bar</b>baz', + 'expected': 'foo^<b>ar</b>baz' }, + + { 'id': 'B-1_SW', + 'desc': 'Delete selection that wraps the whole span content', + 'pad': 'foo<b>[bar]</b>baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SO', + 'desc': 'Delete selection that wraps the whole span', + 'pad': 'foo[<b>bar</b>]baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SL', + 'desc': 'Delete oblique selection that starts before span', + 'pad': 'foo[bar<b>baz]quoz</b>quuz', + 'expected': 'foo^<b>quoz</b>quuz' }, + + { 'id': 'B-1_SR', + 'desc': 'Delete oblique selection that ends after span', + 'pad': 'foo<b>bar[baz</b>quoz]quuz', + 'expected': 'foo<b>bar^</b>quuz' }, + + { 'id': 'B.I-1_SM', + 'desc': 'Delete oblique selection that starts and ends in different spans', + 'pad': 'foo<b>bar[baz</b><i>qoz]quuz</i>quuuz', + 'expected': 'foo<b>bar^</b><i>quuz</i>quuuz' }, + + { 'id': 'GEN-1_SE', + 'desc': 'Delete at end of span with generated content', + 'pad': 'foo<gen>bar^</gen>baz', + 'expected': 'foo<gen>bar^</gen>az' }, + + { 'id': 'GEN-1_SB', + 'desc': 'Delete from position before span with generated content', + 'pad': 'foo^<gen>bar</gen>baz', + 'expected': 'foo^<gen>ar</gen>baz' } + ] + }, + + { 'desc': 'forward-delete paragraphs', + 'tests': [ + { 'id': 'P2-1_SE1', + 'desc': 'Delete from collapsed selection at end of paragraph - should merge with next', + 'pad': '<p>foobar^</p><p>bazqoz</p>', + 'expected': '<p>foobar^bazqoz</p>' }, + + { 'id': 'P2-1_SI1', + 'desc': 'Delete non-collapsed selection at end of paragraph - should not merge with next', + 'pad': '<p>foo[bar]</p><p>bazqoz</p>', + 'expected': '<p>foo^</p><p>bazqoz</p>' }, + + { 'id': 'P2-1_SM', + 'desc': 'Delete non-collapsed selection spanning 2 paragraphs - should merge them', + 'pad': '<p>foo[bar</p><p>baz]qoz</p>', + 'expected': '<p>foo^qoz</p>' } + ] + }, + + { 'desc': 'forward-delete lists and list items', + 'tests': [ + { 'id': 'OL-LI2-1_SO1', + 'desc': 'Delete fully wrapped list item', + 'pad': 'foo<ol>{<li>bar</li>}<li>baz</li></ol>qoz', + 'expected': ['foo<ol>|<li>baz</li></ol>qoz', + 'foo<ol><li>^baz</li></ol>qoz'] }, + + { 'id': 'OL-LI2-1_SM', + 'desc': 'Delete oblique range between list items within same list', + 'pad': 'foo<ol><li>ba[r</li><li>b]az</li></ol>qoz', + 'expected': 'foo<ol><li>ba^az</li></ol>qoz' }, + + { 'id': 'OL-LI-1_SW', + 'desc': 'Delete contents of last list item (list should remain)', + 'pad': 'foo<ol><li>[foo]</li></ol>qoz', + 'expected': ['foo<ol><li>|</li></ol>qoz', + 'foo<ol><li>^</li></ol>qoz'] }, + + { 'id': 'OL-LI-1_SO', + 'desc': 'Delete last list item of list (should remove entire list)', + 'pad': 'foo<ol>{<li>foo</li>}</ol>qoz', + 'expected': 'foo^qoz' } + ] + }, + + { 'desc': 'forward-delete with strange selections', + 'tests': [ + { 'id': 'HR.BR-1_SM', + 'desc': 'Delete selection that starts and ends within nodes that don\'t have children', + 'pad': 'foo<hr {>bar<br }>baz', + 'expected': 'foo<hr>|<br>baz' } + ] + }, + + { 'desc': 'forward-delete from immediately before a table', + 'tests': [ + { 'id': 'TABLE-1_SB', + 'desc': 'Delete from position immediately before table (should have no effect)', + 'pad': 'foo^<table><tbody><tr><td>bar</td></tr></tbody></table>baz', + 'expected': 'foo^<table><tbody><tr><td>bar</td></tr></tbody></table>baz' } + ] + }, + + { 'desc': 'forward-delete within table cells', + 'tests': [ + { 'id': 'TD-1_SE', + 'desc': 'Delete from end of last cell (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>bar^</td></tr></tbody></table>baz', + 'expected': 'foo<table><tbody><tr><td>bar^</td></tr></tbody></table>baz' }, + + { 'id': 'TD2-1_SE1', + 'desc': 'Delete from end of inner cell (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>bar^</td><td>baz</td></tr></tbody></table>quoz', + 'expected': 'foo<table><tbody><tr><td>bar^</td><td>baz</td></tr></tbody></table>quoz' }, + + { 'id': 'TD2-1_SM', + 'desc': 'Delete with selection spanning 2 cells', + 'pad': 'foo<table><tbody><tr><td>ba[r</td><td>b]az</td></tr></tbody></table>quoz', + 'expected': 'foo<table><tbody><tr><td>ba^</td><td>az</td></tr></tbody></table>quoz' } + ] + }, + + { 'desc': 'forward-delete table rows', + 'tests': [ + { 'id': 'TR3-1_SO1', + 'desc': 'Delete first table row', + 'pad': '<table><tbody>{<tr><td>A</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>^B</td></tr><tr><td>C</td></tr></tbody></table>'] }, + + { 'id': 'TR3-1_SO2', + 'desc': 'Delete middle table row', + 'pad': '<table><tbody><tr><td>A</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td></tr>|<tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>A</td></tr><tr><td>^C</td></tr></tbody></table>'] }, + + { 'id': 'TR3-1_SO3', + 'desc': 'Delete last table row', + 'pad': '<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td></tr><tr><td>B^</td></tr></tbody></table>'] }, + + { 'id': 'TR2rs:2-1_SO1', + 'desc': 'Delete first table row where a cell has rowspan 2', + 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=2>R</td></tr>}<tr><td>B</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>B</td><td>R</td></tr></tbody></table>', + '<table><tbody><tr><td>^B</td><td>R</td></tr></tbody></table>'] }, + + { 'id': 'TR2rs:2-1_SO2', + 'desc': 'Delete second table row where a cell has rowspan 2', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=2>R</td></tr>{<tr><td>B</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td><td>R</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td><td>R^</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO1', + 'desc': 'Delete first table row where a cell has rowspan 3', + 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=3>R</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>^A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO2', + 'desc': 'Delete middle table row where a cell has rowspan 3', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr>|<tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr><tr><td>^C</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO3', + 'desc': 'Delete last table row where a cell has rowspan 3', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B^</td></tr></tbody></table>'] } + ] + }, + + { 'desc': 'delete with non-editable nested content', + 'tests': [ + { 'id': 'DIV:ce:false-1_SO', + 'desc': 'Delete nested non-editable <div>', + 'pad': 'foo[bar<div contenteditable="false">NESTED</div>baz]qoz', + 'expected': 'foo^qoz' }, + + { 'id': 'DIV:ce:false-1_SB', + 'desc': 'Delete from immediately before a nested non-editable <div> (should be no-op)', + 'pad': 'foobar^<div contenteditable="false">NESTED</div>bazqoz', + 'expected': 'foobar^<div contenteditable="false">NESTED</div>bazqoz' }, + + { 'id': 'DIV:ce:false-1_SL', + 'desc': 'Delete nested non-editable <div> with oblique selection', + 'pad': 'foo[bar<div contenteditable="false">NES]TED</div>bazqoz', + 'expected': [ 'foo^<div contenteditable="false">NESTED</div>bazqoz', + 'foo<div contenteditable="false">[NES]TED</div>bazqoz' ] }, + + { 'id': 'DIV:ce:false-1_SR', + 'desc': 'Delete nested non-editable <div> with oblique selection', + 'pad': 'foobar<div contenteditable="false">NES[TED</div>baz]qoz', + 'expected': [ 'foobar<div contenteditable="false">NESTED</div>^qoz', + 'foobar<div contenteditable="false">NES[TED]</div>qoz' ] }, + + { 'id': 'DIV:ce:false-1_SI', + 'desc': 'Delete inside nested non-editable <div> (should be no-op)', + 'pad': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz', + 'expected': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz' } + ] + }, + + { 'desc': 'Delete with display:inline-block', + 'checkStyle': True, + 'tests': [ + { 'id': 'SPAN:d:ib-1_SC', + 'desc': 'Delete inside an inline-block <span>', + 'pad': 'foo<span style="display: inline-block">bar^baz</span>qoz', + 'expected': 'foo<span style="display: inline-block">bar^az</span>qoz' }, + + { 'id': 'SPAN:d:ib-1_SA', + 'desc': 'Delete from immediately before an inline-block <span>', + 'pad': 'foo^<span style="display: inline-block">barbaz</span>qoz', + 'expected': 'foo^<span style="display: inline-block">arbaz</span>qoz' }, + + { 'id': 'SPAN:d:ib-2_SL', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo[DEL<span style="display: inline-block">ETE]bar</span>baz', + 'expected': 'foo^<span style="display: inline-block">bar</span>baz' }, + + { 'id': 'SPAN:d:ib-3_SR', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">bar[DEL</span>ETE]baz', + 'expected': 'foo<span style="display: inline-block">bar^</span>baz' }, + + { 'id': 'SPAN:d:ib-4i_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">bar[DELETE]baz</span>qoz', + 'expected': 'foo<span style="display: inline-block">bar^baz</span>qoz' }, + + { 'id': 'SPAN:d:ib-4l_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">[DELETE]barbaz</span>qoz', + 'expected': 'foo<span style="display: inline-block">^barbaz</span>qoz' }, + + { 'id': 'SPAN:d:ib-4r_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">barbaz[DELETE]</span>qoz', + 'expected': 'foo<span style="display: inline-block">barbaz^</span>qoz' } + ] + } + ] +} + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py new file mode 100644 index 000000000..a2e79c27c --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py @@ -0,0 +1,285 @@ + +INSERT_TESTS = { + 'id': 'I', + 'caption': 'Insert Tests', + 'checkAttrs': False, + 'checkStyle': False, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'insert <hr>', + 'command': 'inserthorizontalrule', + 'tests': [ + { 'id': 'IHR_TEXT-1_SC', + 'rte1-id': 'a-inserthorizontalrule-0', + 'desc': 'Insert <hr> into text', + 'pad': 'foo^bar', + 'expected': 'foo<hr>^bar', + 'accept': 'foo<hr>|bar' }, + + { 'id': 'IHR_TEXT-1_SI', + 'desc': 'Insert <hr>, replacing selected text', + 'pad': 'foo[bar]baz', + 'expected': 'foo<hr>^baz', + 'accept': 'foo<hr>|baz' }, + + { 'id': 'IHR_DIV-B-1_SX', + 'desc': 'Insert <hr> between elements', + 'pad': '<div><b>foo</b>|<b>bar</b></div>', + 'expected': '<div><b>foo</b><hr>|<b>bar</b></div>' }, + + { 'id': 'IHR_DIV-B-2_SO', + 'desc': 'Insert <hr>, replacing a fully wrapped element', + 'pad': '<div><b>foo</b>{<b>bar</b>}<b>baz</b></div>', + 'expected': '<div><b>foo</b><hr>|<b>baz</b></div>' }, + + { 'id': 'IHR_B-1_SC', + 'desc': 'Insert <hr> into a span, splitting it', + 'pad': '<b>foo^bar</b>', + 'expected': '<b>foo</b><hr><b>^bar</b>' }, + + { 'id': 'IHR_B-1_SS', + 'desc': 'Insert <hr> into a span at the start (should not create an empty span)', + 'pad': '<b>^foobar</b>', + 'expected': '<hr><b>^foobar</b>' }, + + { 'id': 'IHR_B-1_SE', + 'desc': 'Insert <hr> into a span at the end', + 'pad': '<b>foobar^</b>', + 'expected': [ '<b>foobar</b><hr>|', + '<b>foobar</b><hr><b>^</b>' ] }, + + { 'id': 'IHR_B-2_SL', + 'desc': 'Insert <hr> with oblique selection starting outside of span', + 'pad': 'foo[bar<b>baz]qoz</b>', + 'expected': 'foo<hr>|<b>qoz</b>' }, + + { 'id': 'IHR_B-2_SLR', + 'desc': 'Insert <hr> with oblique reversed selection starting outside of span', + 'pad': 'foo]bar<b>baz[qoz</b>', + 'expected': [ 'foo<hr>|<b>qoz</b>', + 'foo<hr><b>^qoz</b>' ] }, + + { 'id': 'IHR_B-3_SR', + 'desc': 'Insert <hr> with oblique selection ending outside of span', + 'pad': '<b>foo[bar</b>baz]quoz', + 'expected': [ '<b>foo</b><hr>|quoz', + '<b>foo</b><hr><b>^</b>quoz' ] }, + + { 'id': 'IHR_B-3_SRR', + 'desc': 'Insert <hr> with oblique reversed selection starting outside of span', + 'pad': '<b>foo]bar</b>baz[quoz', + 'expected': '<b>foo</b><hr>|quoz' }, + + { 'id': 'IHR_B-I-1_SM', + 'desc': 'Insert <hr> with oblique selection between different spans', + 'pad': '<b>foo[bar</b><i>baz]quoz</i>', + 'expected': [ '<b>foo</b><hr>|<i>quoz</i>', + '<b>foo</b><hr><b>^</b><i>quoz</i>' ] }, + + { 'id': 'IHR_B-I-1_SMR', + 'desc': 'Insert <hr> with reversed oblique selection between different spans', + 'pad': '<b>foo]bar</b><i>baz[quoz</i>', + 'expected': '<b>foo</b><hr><i>^quoz</i>' }, + + { 'id': 'IHR_P-1_SC', + 'desc': 'Insert <hr> into a paragraph, splitting it', + 'pad': '<p>foo^bar</p>', + 'expected': [ '<p>foo</p><hr>|<p>bar</p>', + '<p>foo</p><hr><p>^bar</p>' ] }, + + { 'id': 'IHR_P-1_SS', + 'desc': 'Insert <hr> into a paragraph at the start (should not create an empty span)', + 'pad': '<p>^foobar</p>', + 'expected': [ '<hr>|<p>foobar</p>', + '<hr><p>^foobar</p>' ] }, + + { 'id': 'IHR_P-1_SE', + 'desc': 'Insert <hr> into a paragraph at the end (should not create an empty span)', + 'pad': '<p>foobar^</p>', + 'expected': '<p>foobar</p><hr>|' } + ] + }, + + { 'desc': 'insert <p>', + 'command': 'insertparagraph', + 'tests': [ + { 'id': 'IP_P-1_SC', + 'desc': 'Split paragraph', + 'pad': '<p>foo^bar</p>', + 'expected': '<p>foo</p><p>^bar</p>' }, + + { 'id': 'IP_UL-LI-1_SC', + 'desc': 'Split list item', + 'pad': '<ul><li>foo^bar</li></ul>', + 'expected': '<ul><li>foo</li><li>^bar</li></ul>' } + ] + }, + + { 'desc': 'insert text', + 'command': 'inserttext', + 'tests': [ + { 'id': 'ITEXT:text_TEXT-1_SC', + 'desc': 'Insert text', + 'value': 'text', + 'pad': 'foo^bar', + 'expected': 'footext^bar' }, + + { 'id': 'ITEXT:text_TEXT-1_SI', + 'desc': 'Insert text, replacing selected text', + 'value': 'text', + 'pad': 'foo[bar]baz', + 'expected': 'footext^baz' } + ] + }, + + { 'desc': 'insert <br>', + 'command': 'insertlinebreak', + 'tests': [ + { 'id': 'IBR_TEXT-1_SC', + 'desc': 'Insert <br> into text', + 'pad': 'foo^bar', + 'expected': [ 'foo<br>|bar', + 'foo<br>^bar' ] }, + + { 'id': 'IBR_TEXT-1_SI', + 'desc': 'Insert <br>, replacing selected text', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<br>|baz', + 'foo<br>^baz' ] }, + + { 'id': 'IBR_LI-1_SC', + 'desc': 'Insert <br> within list item', + 'pad': '<ul><li>foo^bar</li></ul>', + 'expected': '<ul><li>foo<br>^bar</li></ul>' } + ] + }, + + { 'desc': 'insert <img>', + 'command': 'insertimage', + 'tests': [ + { 'id': 'IIMG:url_TEXT-1_SC', + 'rte1-id': 'a-insertimage-0', + 'desc': 'Insert image with URL "bar.png"', + 'value': 'bar.png', + 'checkAttrs': True, + 'pad': 'foo^bar', + 'expected': [ 'foo<img src="bar.png">|bar', + 'foo<img src="bar.png">^bar' ] }, + + { 'id': 'IIMG:url_IMG-1_SO', + 'desc': 'Change existing image to new URL, selection on <img>', + 'value': 'quz.png', + 'checkAttrs': True, + 'pad': '<span>foo{<img src="bar.png">}bar</span>', + 'expected': [ '<span>foo<img src="quz.png"/>|bar</span>', + '<span>foo<img src="quz.png"/>^bar</span>' ] }, + + { 'id': 'IIMG:url_SPAN-IMG-1_SO', + 'desc': 'Change existing image to new URL, selection in text surrounding <img>', + 'value': 'quz.png', + 'checkAttrs': True, + 'pad': 'foo[<img src="bar.png">]bar', + 'expected': [ 'foo<img src="quz.png"/>|bar', + 'foo<img src="quz.png"/>^bar' ] }, + + { 'id': 'IIMG:._SPAN-IMG-1_SO', + 'desc': 'Remove existing image or URL, selection on <img>', + 'value': '', + 'checkAttrs': True, + 'pad': '<span>foo{<img src="bar.png">}bar</span>', + 'expected': [ '<span>foo^bar</span>', + '<span>foo<img>|bar</span>', + '<span>foo<img>^bar</span>', + '<span>foo<img src="">|bar</span>', + '<span>foo<img src="">^bar</span>' ] }, + + { 'id': 'IIMG:._IMG-1_SO', + 'desc': 'Remove existing image or URL, selection in text surrounding <img>', + 'value': '', + 'checkAttrs': True, + 'pad': 'foo[<img src="bar.png">]bar', + 'expected': [ 'foo^bar', + 'foo<img>|bar', + 'foo<img>^bar', + 'foo<img src="">|bar', + 'foo<img src="">^bar' ] } + ] + }, + + { 'desc': 'insert <ol>', + 'command': 'insertorderedlist', + 'tests': [ + { 'id': 'IOL_TEXT-1_SC', + 'rte1-id': 'a-insertorderedlist-0', + 'desc': 'Insert ordered list on collapsed selection', + 'pad': 'foo^bar', + 'expected': '<ol><li>foo^bar</li></ol>' }, + + { 'id': 'IOL_TEXT-1_SI', + 'desc': 'Insert ordered list on selected text', + 'pad': 'foo[bar]baz', + 'expected': '<ol><li>foo[bar]baz</li></ol>' } + ] + }, + + { 'desc': 'insert <ul>', + 'command': 'insertunorderedlist', + 'tests': [ + { 'id': 'IUL_TEXT-1_SC', + 'desc': 'Insert unordered list on collapsed selection', + 'pad': 'foo^bar', + 'expected': '<ul><li>foo^bar</li></ul>' }, + + { 'id': 'IUL_TEXT-1_SI', + 'rte1-id': 'a-insertunorderedlist-0', + 'desc': 'Insert unordered list on selected text', + 'pad': 'foo[bar]baz', + 'expected': '<ul><li>foo[bar]baz</li></ul>' } + ] + }, + + { 'desc': 'insert arbitrary HTML', + 'command': 'inserthtml', + 'tests': [ + { 'id': 'IHTML:BR_TEXT-1_SC', + 'rte1-id': 'a-inserthtml-0', + 'desc': 'InsertHTML: <br>', + 'value': '<br>', + 'pad': 'foo^barbaz', + 'expected': 'foo<br>^barbaz' }, + + { 'id': 'IHTML:text_TEXT-1_SI', + 'desc': 'InsertHTML: "NEW"', + 'value': 'NEW', + 'pad': 'foo[bar]baz', + 'expected': 'fooNEW^baz' }, + + { 'id': 'IHTML:S_TEXT-1_SI', + 'desc': 'InsertHTML: "<span>NEW<span>"', + 'value': '<span>NEW</span>', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span>NEW</span>^baz' }, + + { 'id': 'IHTML:H1.H2_TEXT-1_SI', + 'desc': 'InsertHTML: "<h1>NEW</h1><h2>HTML</h2>"', + 'value': '<h1>NEW</h1><h2>HTML</h2>', + 'pad': 'foo[bar]baz', + 'expected': 'foo<h1>NEW</h1><h2>HTML</h2>^baz' }, + + { 'id': 'IHTML:P-B_TEXT-1_SI', + 'desc': 'InsertHTML: "<p>NEW<b>HTML</b>!</p>"', + 'value': '<p>NEW<b>HTML</b>!</p>', + 'pad': 'foo[bar]baz', + 'expected': 'foo<p>NEW<b>HTML</b>!</p>^baz' } + ] + } + ] +} + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py new file mode 100644 index 000000000..eb721923b --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py @@ -0,0 +1,215 @@ + +QUERYENABLED_TESTS = { + 'id': 'QE', + 'caption': 'queryCommandEnabled Tests', + 'pad': 'foo[bar]baz', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + 'expected': True, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'HTML5 commands', + 'tests': [ + { 'id': 'SELECTALL_TEXT-1', + 'desc': 'check whether the "selectall" command is enabled', + 'qcenabled': 'selectall' }, + + { 'id': 'UNSELECT_TEXT-1', + 'desc': 'check whether the "unselect" command is enabled', + 'qcenabled': 'unselect' }, + + { 'id': 'UNDO_TEXT-1', + 'desc': 'check whether the "undo" command is enabled', + 'qcenabled': 'undo' }, + + { 'id': 'REDO_TEXT-1', + 'desc': 'check whether the "redo" command is enabled', + 'qcenabled': 'redo' }, + + { 'id': 'BOLD_TEXT-1', + 'desc': 'check whether the "bold" command is enabled', + 'qcenabled': 'bold' }, + + { 'id': 'ITALIC_TEXT-1', + 'desc': 'check whether the "italic" command is enabled', + 'qcenabled': 'italic' }, + + { 'id': 'UNDERLINE_TEXT-1', + 'desc': 'check whether the "underline" command is enabled', + 'qcenabled': 'underline' }, + + { 'id': 'STRIKETHROUGH_TEXT-1', + 'desc': 'check whether the "strikethrough" command is enabled', + 'qcenabled': 'strikethrough' }, + + { 'id': 'SUBSCRIPT_TEXT-1', + 'desc': 'check whether the "subscript" command is enabled', + 'qcenabled': 'subscript' }, + + { 'id': 'SUPERSCRIPT_TEXT-1', + 'desc': 'check whether the "superscript" command is enabled', + 'qcenabled': 'superscript' }, + + { 'id': 'FORMATBLOCK_TEXT-1', + 'desc': 'check whether the "formatblock" command is enabled', + 'qcenabled': 'formatblock' }, + + { 'id': 'CREATELINK_TEXT-1', + 'desc': 'check whether the "createlink" command is enabled', + 'qcenabled': 'createlink' }, + + { 'id': 'UNLINK_TEXT-1', + 'desc': 'check whether the "unlink" command is enabled', + 'qcenabled': 'unlink' }, + + { 'id': 'INSERTHTML_TEXT-1', + 'desc': 'check whether the "inserthtml" command is enabled', + 'qcenabled': 'inserthtml' }, + + { 'id': 'INSERTHORIZONTALRULE_TEXT-1', + 'desc': 'check whether the "inserthorizontalrule" command is enabled', + 'qcenabled': 'inserthorizontalrule' }, + + { 'id': 'INSERTIMAGE_TEXT-1', + 'desc': 'check whether the "insertimage" command is enabled', + 'qcenabled': 'insertimage' }, + + { 'id': 'INSERTLINEBREAK_TEXT-1', + 'desc': 'check whether the "insertlinebreak" command is enabled', + 'qcenabled': 'insertlinebreak' }, + + { 'id': 'INSERTPARAGRAPH_TEXT-1', + 'desc': 'check whether the "insertparagraph" command is enabled', + 'qcenabled': 'insertparagraph' }, + + { 'id': 'INSERTORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertorderedlist" command is enabled', + 'qcenabled': 'insertorderedlist' }, + + { 'id': 'INSERTUNORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertunorderedlist" command is enabled', + 'qcenabled': 'insertunorderedlist' }, + + { 'id': 'INSERTTEXT_TEXT-1', + 'desc': 'check whether the "inserttext" command is enabled', + 'qcenabled': 'inserttext' }, + + { 'id': 'DELETE_TEXT-1', + 'desc': 'check whether the "delete" command is enabled', + 'qcenabled': 'delete' }, + + { 'id': 'FORWARDDELETE_TEXT-1', + 'desc': 'check whether the "forwarddelete" command is enabled', + 'qcenabled': 'forwarddelete' } + ] + }, + + { 'desc': 'MIDAS commands', + 'tests': [ + { 'id': 'STYLEWITHCSS_TEXT-1', + 'desc': 'check whether the "styleWithCSS" command is enabled', + 'qcenabled': 'styleWithCSS' }, + + { 'id': 'CONTENTREADONLY_TEXT-1', + 'desc': 'check whether the "contentreadonly" command is enabled', + 'qcenabled': 'contentreadonly' }, + + { 'id': 'BACKCOLOR_TEXT-1', + 'desc': 'check whether the "backcolor" command is enabled', + 'qcenabled': 'backcolor' }, + + { 'id': 'FORECOLOR_TEXT-1', + 'desc': 'check whether the "forecolor" command is enabled', + 'qcenabled': 'forecolor' }, + + { 'id': 'HILITECOLOR_TEXT-1', + 'desc': 'check whether the "hilitecolor" command is enabled', + 'qcenabled': 'hilitecolor' }, + + { 'id': 'FONTNAME_TEXT-1', + 'desc': 'check whether the "fontname" command is enabled', + 'qcenabled': 'fontname' }, + + { 'id': 'FONTSIZE_TEXT-1', + 'desc': 'check whether the "fontsize" command is enabled', + 'qcenabled': 'fontsize' }, + + { 'id': 'INCREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "increasefontsize" command is enabled', + 'qcenabled': 'increasefontsize' }, + + { 'id': 'DECREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "decreasefontsize" command is enabled', + 'qcenabled': 'decreasefontsize' }, + + { 'id': 'HEADING_TEXT-1', + 'desc': 'check whether the "heading" command is enabled', + 'qcenabled': 'heading' }, + + { 'id': 'INDENT_TEXT-1', + 'desc': 'check whether the "indent" command is enabled', + 'qcenabled': 'indent' }, + + { 'id': 'OUTDENT_TEXT-1', + 'desc': 'check whether the "outdent" command is enabled', + 'qcenabled': 'outdent' }, + + { 'id': 'CREATEBOOKMARK_TEXT-1', + 'desc': 'check whether the "createbookmark" command is enabled', + 'qcenabled': 'createbookmark' }, + + { 'id': 'UNBOOKMARK_TEXT-1', + 'desc': 'check whether the "unbookmark" command is enabled', + 'qcenabled': 'unbookmark' }, + + { 'id': 'JUSTIFYCENTER_TEXT-1', + 'desc': 'check whether the "justifycenter" command is enabled', + 'qcenabled': 'justifycenter' }, + + { 'id': 'JUSTIFYFULL_TEXT-1', + 'desc': 'check whether the "justifyfull" command is enabled', + 'qcenabled': 'justifyfull' }, + + { 'id': 'JUSTIFYLEFT_TEXT-1', + 'desc': 'check whether the "justifyleft" command is enabled', + 'qcenabled': 'justifyleft' }, + + { 'id': 'JUSTIFYRIGHT_TEXT-1', + 'desc': 'check whether the "justifyright" command is enabled', + 'qcenabled': 'justifyright' }, + + { 'id': 'REMOVEFORMAT_TEXT-1', + 'desc': 'check whether the "removeformat" command is enabled', + 'qcenabled': 'removeformat' }, + + { 'id': 'COPY_TEXT-1', + 'desc': 'check whether the "copy" command is enabled', + 'qcenabled': 'copy' }, + + { 'id': 'CUT_TEXT-1', + 'desc': 'check whether the "cut" command is enabled', + 'qcenabled': 'cut' }, + + { 'id': 'PASTE_TEXT-1', + 'desc': 'check whether the "paste" command is enabled', + 'qcenabled': 'paste' } + ] + }, + + { 'desc': 'Other tests', + 'tests': [ + { 'id': 'garbage-1_TEXT-1', + 'desc': 'check correct return value with garbage input', + 'qcenabled': '#!#@7', + 'expected': False } + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py new file mode 100644 index 000000000..d1ad8debd --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py @@ -0,0 +1,214 @@ + +QUERYINDETERM_TESTS = { + 'id': 'QI', + 'caption': 'queryCommandIndeterm Tests', + 'pad': 'foo[bar]baz', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + 'expected': False, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'HTML5 commands', + 'tests': [ + { 'id': 'SELECTALL_TEXT-1', + 'desc': 'check whether the "selectall" command is indeterminate', + 'qcindeterm': 'selectall' }, + + { 'id': 'UNSELECT_TEXT-1', + 'desc': 'check whether the "unselect" command is indeterminate', + 'qcindeterm': 'unselect' }, + + { 'id': 'UNDO_TEXT-1', + 'desc': 'check whether the "undo" command is indeterminate', + 'qcindeterm': 'undo' }, + + { 'id': 'REDO_TEXT-1', + 'desc': 'check whether the "redo" command is indeterminate', + 'qcindeterm': 'redo' }, + + { 'id': 'BOLD_TEXT-1', + 'desc': 'check whether the "bold" command is indeterminate', + 'qcindeterm': 'bold' }, + + { 'id': 'ITALIC_TEXT-1', + 'desc': 'check whether the "italic" command is indeterminate', + 'qcindeterm': 'italic' }, + + { 'id': 'UNDERLINE_TEXT-1', + 'desc': 'check whether the "underline" command is indeterminate', + 'qcindeterm': 'underline' }, + + { 'id': 'STRIKETHROUGH_TEXT-1', + 'desc': 'check whether the "strikethrough" command is indeterminate', + 'qcindeterm': 'strikethrough' }, + + { 'id': 'SUBSCRIPT_TEXT-1', + 'desc': 'check whether the "subscript" command is indeterminate', + 'qcindeterm': 'subscript' }, + + { 'id': 'SUPERSCRIPT_TEXT-1', + 'desc': 'check whether the "superscript" command is indeterminate', + 'qcindeterm': 'superscript' }, + + { 'id': 'FORMATBLOCK_TEXT-1', + 'desc': 'check whether the "formatblock" command is indeterminate', + 'qcindeterm': 'formatblock' }, + + { 'id': 'CREATELINK_TEXT-1', + 'desc': 'check whether the "createlink" command is indeterminate', + 'qcindeterm': 'createlink' }, + + { 'id': 'UNLINK_TEXT-1', + 'desc': 'check whether the "unlink" command is indeterminate', + 'qcindeterm': 'unlink' }, + + { 'id': 'INSERTHTML_TEXT-1', + 'desc': 'check whether the "inserthtml" command is indeterminate', + 'qcindeterm': 'inserthtml' }, + + { 'id': 'INSERTHORIZONTALRULE_TEXT-1', + 'desc': 'check whether the "inserthorizontalrule" command is indeterminate', + 'qcindeterm': 'inserthorizontalrule' }, + + { 'id': 'INSERTIMAGE_TEXT-1', + 'desc': 'check whether the "insertimage" command is indeterminate', + 'qcindeterm': 'insertimage' }, + + { 'id': 'INSERTLINEBREAK_TEXT-1', + 'desc': 'check whether the "insertlinebreak" command is indeterminate', + 'qcindeterm': 'insertlinebreak' }, + + { 'id': 'INSERTPARAGRAPH_TEXT-1', + 'desc': 'check whether the "insertparagraph" command is indeterminate', + 'qcindeterm': 'insertparagraph' }, + + { 'id': 'INSERTORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertorderedlist" command is indeterminate', + 'qcindeterm': 'insertorderedlist' }, + + { 'id': 'INSERTUNORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertunorderedlist" command is indeterminate', + 'qcindeterm': 'insertunorderedlist' }, + + { 'id': 'INSERTTEXT_TEXT-1', + 'desc': 'check whether the "inserttext" command is indeterminate', + 'qcindeterm': 'inserttext' }, + + { 'id': 'DELETE_TEXT-1', + 'desc': 'check whether the "delete" command is indeterminate', + 'qcindeterm': 'delete' }, + + { 'id': 'FORWARDDELETE_TEXT-1', + 'desc': 'check whether the "forwarddelete" command is indeterminate', + 'qcindeterm': 'forwarddelete' } + ] + }, + + { 'desc': 'MIDAS commands', + 'tests': [ + { 'id': 'STYLEWITHCSS_TEXT-1', + 'desc': 'check whether the "styleWithCSS" command is indeterminate', + 'qcindeterm': 'styleWithCSS' }, + + { 'id': 'CONTENTREADONLY_TEXT-1', + 'desc': 'check whether the "contentreadonly" command is indeterminate', + 'qcindeterm': 'contentreadonly' }, + + { 'id': 'BACKCOLOR_TEXT-1', + 'desc': 'check whether the "backcolor" command is indeterminate', + 'qcindeterm': 'backcolor' }, + + { 'id': 'FORECOLOR_TEXT-1', + 'desc': 'check whether the "forecolor" command is indeterminate', + 'qcindeterm': 'forecolor' }, + + { 'id': 'HILITECOLOR_TEXT-1', + 'desc': 'check whether the "hilitecolor" command is indeterminate', + 'qcindeterm': 'hilitecolor' }, + + { 'id': 'FONTNAME_TEXT-1', + 'desc': 'check whether the "fontname" command is indeterminate', + 'qcindeterm': 'fontname' }, + + { 'id': 'FONTSIZE_TEXT-1', + 'desc': 'check whether the "fontsize" command is indeterminate', + 'qcindeterm': 'fontsize' }, + + { 'id': 'INCREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "increasefontsize" command is indeterminate', + 'qcindeterm': 'increasefontsize' }, + + { 'id': 'DECREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "decreasefontsize" command is indeterminate', + 'qcindeterm': 'decreasefontsize' }, + + { 'id': 'HEADING_TEXT-1', + 'desc': 'check whether the "heading" command is indeterminate', + 'qcindeterm': 'heading' }, + + { 'id': 'INDENT_TEXT-1', + 'desc': 'check whether the "indent" command is indeterminate', + 'qcindeterm': 'indent' }, + + { 'id': 'OUTDENT_TEXT-1', + 'desc': 'check whether the "outdent" command is indeterminate', + 'qcindeterm': 'outdent' }, + + { 'id': 'CREATEBOOKMARK_TEXT-1', + 'desc': 'check whether the "createbookmark" command is indeterminate', + 'qcindeterm': 'createbookmark' }, + + { 'id': 'UNBOOKMARK_TEXT-1', + 'desc': 'check whether the "unbookmark" command is indeterminate', + 'qcindeterm': 'unbookmark' }, + + { 'id': 'JUSTIFYCENTER_TEXT-1', + 'desc': 'check whether the "justifycenter" command is indeterminate', + 'qcindeterm': 'justifycenter' }, + + { 'id': 'JUSTIFYFULL_TEXT-1', + 'desc': 'check whether the "justifyfull" command is indeterminate', + 'qcindeterm': 'justifyfull' }, + + { 'id': 'JUSTIFYLEFT_TEXT-1', + 'desc': 'check whether the "justifyleft" command is indeterminate', + 'qcindeterm': 'justifyleft' }, + + { 'id': 'JUSTIFYRIGHT_TEXT-1', + 'desc': 'check whether the "justifyright" command is indeterminate', + 'qcindeterm': 'justifyright' }, + + { 'id': 'REMOVEFORMAT_TEXT-1', + 'desc': 'check whether the "removeformat" command is indeterminate', + 'qcindeterm': 'removeformat' }, + + { 'id': 'COPY_TEXT-1', + 'desc': 'check whether the "copy" command is indeterminate', + 'qcindeterm': 'copy' }, + + { 'id': 'CUT_TEXT-1', + 'desc': 'check whether the "cut" command is indeterminate', + 'qcindeterm': 'cut' }, + + { 'id': 'PASTE_TEXT-1', + 'desc': 'check whether the "paste" command is indeterminate', + 'qcindeterm': 'paste' } + ] + }, + + { 'desc': 'Other tests', + 'tests': [ + { 'id': 'garbage-1_TEXT-1', + 'desc': 'check correct return value with garbage input', + 'qcindeterm': '#!#@7' } + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py new file mode 100644 index 000000000..297559d62 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py @@ -0,0 +1,575 @@ + +QUERYSTATE_TESTS = { + 'id': 'QS', + 'caption': 'queryCommandState Tests', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'qcstate': '', + 'tests': [ + ] + }, + + { 'desc': 'query bold state', + 'qcstate': 'bold', + 'tests': [ + { 'id': 'B_TEXT_SI', + 'rte1-id': 'q-bold-0', + 'desc': 'query the "bold" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'B_B-1_SI', + 'rte1-id': 'q-bold-1', + 'desc': 'query the "bold" state', + 'pad': '<b>foo[bar]baz</b>', + 'expected': True }, + + { 'id': 'B_STRONG-1_SI', + 'rte1-id': 'q-bold-2', + 'desc': 'query the "bold" state', + 'pad': '<strong>foo[bar]baz</strong>', + 'expected': True }, + + { 'id': 'B_SPANs:fw:b-1_SI', + 'rte1-id': 'q-bold-3', + 'desc': 'query the "bold" state', + 'pad': '<span style="font-weight: bold">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'B_SPANs:fw:n-1_SI', + 'desc': 'query the "bold" state', + 'pad': '<span style="font-weight: normal">foo[bar]baz</span>', + 'expected': False }, + + { 'id': 'B_Bs:fw:n-1_SI', + 'rte1-id': 'q-bold-4', + 'desc': 'query the "bold" state', + 'pad': '<span style="font-weight: normal">foo[bar]baz</span>', + 'expected': False }, + + { 'id': 'B_B-SPANs:fw:n-1_SI', + 'rte1-id': 'q-bold-5', + 'desc': 'query the "bold" state', + 'pad': '<b><span style="font-weight: normal">foo[bar]baz</span></b>', + 'expected': False }, + + { 'id': 'B_SPAN.b-1-SI', + 'desc': 'query the "bold" state', + 'pad': '<span class="b">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'B_MYB-1-SI', + 'desc': 'query the "bold" state', + 'pad': '<myb>foo[bar]baz</myb>', + 'expected': True }, + + { 'id': 'B_B-I-1_SC', + 'desc': 'query the "bold" state, bold tag not immediate parent', + 'pad': '<b>foo<i>ba^r</i>baz</b>', + 'expected': True }, + + { 'id': 'B_B-I-1_SL', + 'desc': 'query the "bold" state, selection partially in child element', + 'pad': '<b>fo[o<i>b]ar</i>baz</b>', + 'expected': True }, + + { 'id': 'B_B-I-1_SR', + 'desc': 'query the "bold" state, selection partially in child element', + 'pad': '<b>foo<i>ba[r</i>b]az</b>', + 'expected': True }, + + { 'id': 'B_STRONG-I-1_SC', + 'desc': 'query the "bold" state, bold tag not immediate parent', + 'pad': '<strong>foo<i>ba^r</i>baz</strong>', + 'expected': True }, + + { 'id': 'B_B-I-U-1_SC', + 'desc': 'query the "bold" state, bold tag not immediate parent', + 'pad': '<b>foo<i>bar<u>b^az</u></i></strong>', + 'expected': True }, + + { 'id': 'B_B-I-U-1_SM', + 'desc': 'query the "bold" state, bold tag not immediate parent', + 'pad': '<b>foo<i>ba[r<u>b]az</u></i></strong>', + 'expected': True }, + + { 'id': 'B_TEXT-B-1_SO-1', + 'desc': 'query the "bold" state, selection wrapping the bold tag', + 'pad': 'foo[<b>bar</b>]baz', + 'expected': True }, + + { 'id': 'B_TEXT-B-1_SO-2', + 'desc': 'query the "bold" state, selection wrapping the bold tag', + 'pad': 'foo{<b>bar</b>}baz', + 'expected': True }, + + { 'id': 'B_TEXT-B-1_SL', + 'desc': 'query the "bold" state, selection containing non-bold text', + 'pad': 'fo[o<b>ba]r</b>baz', + 'expected': False }, + + { 'id': 'B_TEXT-B-1_SR', + 'desc': 'query the "bold" state, selection containing non-bold text', + 'pad': 'foo<b>b[ar</b>b]az', + 'expected': False }, + + { 'id': 'B_TEXT-B-1_SO-3', + 'desc': 'query the "bold" state, selection containing non-bold text', + 'pad': 'fo[o<b>bar</b>b]az', + 'expected': False }, + + { 'id': 'B_B.TEXT.B-1_SM', + 'desc': 'query the "bold" state, selection including non-bold text', + 'pad': '<b>fo[o</b>bar<b>b]az</b>', + 'expected': False }, + + { 'id': 'B_B.B.B-1_SM', + 'desc': 'query the "bold" state, selection mixed, but all bold', + 'pad': '<b>fo[o</b><b>bar</b><b>b]az</b>', + 'expected': True }, + + { 'id': 'B_B.STRONG.B-1_SM', + 'desc': 'query the "bold" state, selection mixed, but all bold', + 'pad': '<b>fo[o</b><strong>bar</strong><b>b]az</b>', + 'expected': True }, + + { 'id': 'B_SPAN.b.MYB.SPANs:fw:b-1_SM', + 'desc': 'query the "bold" state, selection mixed, but all bold', + 'pad': '<span class="b">fo[o</span><myb>bar</myb><span style="font-weight: bold">b]az</span>', + 'expected': True } + ] + }, + + { 'desc': 'query italic state', + 'qcstate': 'italic', + 'tests': [ + { 'id': 'I_TEXT_SI', + 'rte1-id': 'q-italic-0', + 'desc': 'query the "italic" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'I_I-1_SI', + 'rte1-id': 'q-italic-1', + 'desc': 'query the "italic" state', + 'pad': '<i>foo[bar]baz</i>', + 'expected': True }, + + { 'id': 'I_EM-1_SI', + 'rte1-id': 'q-italic-2', + 'desc': 'query the "italic" state', + 'pad': '<em>foo[bar]baz</em>', + 'expected': True }, + + { 'id': 'I_SPANs:fs:i-1_SI', + 'rte1-id': 'q-italic-3', + 'desc': 'query the "italic" state', + 'pad': '<span style="font-style: italic">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'I_SPANs:fs:n-1_SI', + 'desc': 'query the "italic" state', + 'pad': '<span style="font-style: normal">foo[bar]baz</span>', + 'expected': False }, + + { 'id': 'I_I-SPANs:fs:n-1_SI', + 'rte1-id': 'q-italic-4', + 'desc': 'query the "italic" state', + 'pad': '<i><span style="font-style: normal">foo[bar]baz</span></i>', + 'expected': False }, + + { 'id': 'I_SPAN.i-1-SI', + 'desc': 'query the "italic" state', + 'pad': '<span class="i">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'I_MYI-1-SI', + 'desc': 'query the "italic" state', + 'pad': '<myi>foo[bar]baz</myi>', + 'expected': True } + ] + }, + + { 'desc': 'query underline state', + 'qcstate': 'underline', + 'tests': [ + { 'id': 'U_TEXT_SI', + 'rte1-id': 'q-underline-0', + 'desc': 'query the "underline" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'U_U-1_SI', + 'rte1-id': 'q-underline-1', + 'desc': 'query the "underline" state', + 'pad': '<u>foo[bar]baz</u>', + 'expected': True }, + + { 'id': 'U_Us:td:n-1_SI', + 'rte1-id': 'q-underline-4', + 'desc': 'query the "underline" state', + 'pad': '<u style="text-decoration: none">foo[bar]baz</u>', + 'expected': False }, + + { 'id': 'U_Ah:url-1_SI', + 'rte1-id': 'q-underline-2', + 'desc': 'query the "underline" state', + 'pad': '<a href="http://www.goo.gl">foo[bar]baz</a>', + 'expected': True }, + + { 'id': 'U_Ah:url.s:td:n-1_SI', + 'rte1-id': 'q-underline-5', + 'desc': 'query the "underline" state', + 'pad': '<a href="http://www.goo.gl" style="text-decoration: none">foo[bar]baz</a>', + 'expected': False }, + + { 'id': 'U_SPANs:td:u-1_SI', + 'rte1-id': 'q-underline-3', + 'desc': 'query the "underline" state', + 'pad': '<span style="text-decoration: underline">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'U_SPAN.u-1-SI', + 'desc': 'query the "underline" state', + 'pad': '<span class="u">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'U_MYU-1-SI', + 'desc': 'query the "underline" state', + 'pad': '<myu>foo[bar]baz</myu>', + 'expected': True } + ] + }, + + { 'desc': 'query strike-through state', + 'qcstate': 'strikethrough', + 'tests': [ + { 'id': 'S_TEXT_SI', + 'rte1-id': 'q-strikethrough-0', + 'desc': 'query the "strikethrough" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'S_S-1_SI', + 'rte1-id': 'q-strikethrough-3', + 'desc': 'query the "strikethrough" state', + 'pad': '<s>foo[bar]baz</s>', + 'expected': True }, + + { 'id': 'S_STRIKE-1_SI', + 'rte1-id': 'q-strikethrough-1', + 'desc': 'query the "strikethrough" state', + 'pad': '<strike>foo[bar]baz</strike>', + 'expected': True }, + + { 'id': 'S_STRIKEs:td:n-1_SI', + 'rte1-id': 'q-strikethrough-2', + 'desc': 'query the "strikethrough" state', + 'pad': '<strike style="text-decoration: none">foo[bar]baz</strike>', + 'expected': False }, + + { 'id': 'S_DEL-1_SI', + 'rte1-id': 'q-strikethrough-4', + 'desc': 'query the "strikethrough" state', + 'pad': '<del>foo[bar]baz</del>', + 'expected': True }, + + { 'id': 'S_SPANs:td:lt-1_SI', + 'rte1-id': 'q-strikethrough-5', + 'desc': 'query the "strikethrough" state', + 'pad': '<span style="text-decoration: line-through">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'S_SPAN.s-1-SI', + 'desc': 'query the "strikethrough" state', + 'pad': '<span class="s">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'S_MYS-1-SI', + 'desc': 'query the "strikethrough" state', + 'pad': '<mys>foo[bar]baz</mys>', + 'expected': True }, + + { 'id': 'S_S.STRIKE.DEL-1_SM', + 'desc': 'query the "strikethrough" state, selection mixed, but all struck', + 'pad': '<s>fo[o</s><strike>bar</strike><del>b]az</del>', + 'expected': True } + ] + }, + + { 'desc': 'query subscript state', + 'qcstate': 'subscript', + 'tests': [ + { 'id': 'SUB_TEXT_SI', + 'rte1-id': 'q-subscript-0', + 'desc': 'query the "subscript" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'SUB_SUB-1_SI', + 'rte1-id': 'q-subscript-1', + 'desc': 'query the "subscript" state', + 'pad': '<sub>foo[bar]baz</sub>', + 'expected': True }, + + { 'id': 'SUB_SPAN.sub-1-SI', + 'desc': 'query the "subscript" state', + 'pad': '<span class="sub">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'SUB_MYSUB-1-SI', + 'desc': 'query the "subscript" state', + 'pad': '<mysub>foo[bar]baz</mysub>', + 'expected': True } + ] + }, + + { 'desc': 'query superscript state', + 'qcstate': 'superscript', + 'tests': [ + { 'id': 'SUP_TEXT_SI', + 'rte1-id': 'q-superscript-0', + 'desc': 'query the "superscript" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'SUP_SUP-1_SI', + 'rte1-id': 'q-superscript-1', + 'desc': 'query the "superscript" state', + 'pad': '<sup>foo[bar]baz</sup>', + 'expected': True }, + + { 'id': 'IOL_TEXT_SI', + 'desc': 'query the "insertorderedlist" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'SUP_SPAN.sup-1-SI', + 'desc': 'query the "superscript" state', + 'pad': '<span class="sup">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'SUP_MYSUP-1-SI', + 'desc': 'query the "superscript" state', + 'pad': '<mysup>foo[bar]baz</mysup>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the selection is in an ordered list', + 'qcstate': 'insertorderedlist', + 'tests': [ + { 'id': 'IOL_TEXT-1_SI', + 'rte1-id': 'q-insertorderedlist-0', + 'desc': 'query the "insertorderedlist" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'IOL_OL-LI-1_SI', + 'rte1-id': 'q-insertorderedlist-1', + 'desc': 'query the "insertorderedlist" state', + 'pad': '<ol><li>foo[bar]baz</li></ol>', + 'expected': True }, + + { 'id': 'IOL_UL_LI-1_SI', + 'rte1-id': 'q-insertorderedlist-2', + 'desc': 'query the "insertorderedlist" state', + 'pad': '<ul><li>foo[bar]baz</li></ul>', + 'expected': False } + ] + }, + + { 'desc': 'query whether the selection is in an unordered list', + 'qcstate': 'insertunorderedlist', + 'tests': [ + { 'id': 'IUL_TEXT_SI', + 'rte1-id': 'q-insertunorderedlist-0', + 'desc': 'query the "insertunorderedlist" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'IUL_OL-LI-1_SI', + 'rte1-id': 'q-insertunorderedlist-1', + 'desc': 'query the "insertunorderedlist" state', + 'pad': '<ol><li>foo[bar]baz</li></ol>', + 'expected': False }, + + { 'id': 'IUL_UL-LI-1_SI', + 'rte1-id': 'q-insertunorderedlist-2', + 'desc': 'query the "insertunorderedlist" state', + 'pad': '<ul><li>foo[bar]baz</li></ul>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the paragraph is centered', + 'qcstate': 'justifycenter', + 'tests': [ + { 'id': 'JC_TEXT_SI', + 'rte1-id': 'q-justifycenter-0', + 'desc': 'query the "justifycenter" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'JC_DIVa:c-1_SI', + 'rte1-id': 'q-justifycenter-1', + 'desc': 'query the "justifycenter" state', + 'pad': '<div align="center">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JC_Pa:c-1_SI', + 'rte1-id': 'q-justifycenter-2', + 'desc': 'query the "justifycenter" state', + 'pad': '<p align="center">foo[bar]baz</p>', + 'expected': True }, + + { 'id': 'JC_SPANs:ta:c-1_SI', + 'rte1-id': 'q-justifycenter-3', + 'desc': 'query the "justifycenter" state', + 'pad': '<div style="text-align: center">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JC_SPAN.jc-1-SI', + 'desc': 'query the "justifycenter" state', + 'pad': '<span class="jc">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JC_MYJC-1-SI', + 'desc': 'query the "justifycenter" state', + 'pad': '<myjc>foo[bar]baz</myjc>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the paragraph is justified', + 'qcstate': 'justifyfull', + 'tests': [ + { 'id': 'JF_TEXT_SI', + 'rte1-id': 'q-justifyfull-0', + 'desc': 'query the "justifyfull" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'JF_DIVa:j-1_SI', + 'rte1-id': 'q-justifyfull-1', + 'desc': 'query the "justifyfull" state', + 'pad': '<div align="justify">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JF_Pa:j-1_SI', + 'rte1-id': 'q-justifyfull-2', + 'desc': 'query the "justifyfull" state', + 'pad': '<p align="justify">foo[bar]baz</p>', + 'expected': True }, + + { 'id': 'JF_SPANs:ta:j-1_SI', + 'rte1-id': 'q-justifyfull-3', + 'desc': 'query the "justifyfull" state', + 'pad': '<span style="text-align: justify">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JF_SPAN.jf-1-SI', + 'desc': 'query the "justifyfull" state', + 'pad': '<span class="jf">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JF_MYJF-1-SI', + 'desc': 'query the "justifyfull" state', + 'pad': '<myjf>foo[bar]baz</myjf>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the paragraph is aligned left', + 'qcstate': 'justifyleft', + 'tests': [ + { 'id': 'JL_TEXT_SI', + 'desc': 'query the "justifyleft" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'JL_DIVa:l-1_SI', + 'rte1-id': 'q-justifyleft-0', + 'desc': 'query the "justifyleft" state', + 'pad': '<div align="left">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JL_Pa:l-1_SI', + 'rte1-id': 'q-justifyleft-1', + 'desc': 'query the "justifyleft" state', + 'pad': '<p align="left">foo[bar]baz</p>', + 'expected': True }, + + { 'id': 'JL_SPANs:ta:l-1_SI', + 'rte1-id': 'q-justifyleft-2', + 'desc': 'query the "justifyleft" state', + 'pad': '<span style="text-align: left">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JL_SPAN.jl-1-SI', + 'desc': 'query the "justifyleft" state', + 'pad': '<span class="jl">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JL_MYJL-1-SI', + 'desc': 'query the "justifyleft" state', + 'pad': '<myjl>foo[bar]baz</myjl>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the paragraph is aligned right', + 'qcstate': 'justifyright', + 'tests': [ + { 'id': 'JR_TEXT_SI', + 'rte1-id': 'q-justifyright-0', + 'desc': 'query the "justifyright" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'JR_DIVa:r-1_SI', + 'rte1-id': 'q-justifyright-1', + 'desc': 'query the "justifyright" state', + 'pad': '<div align="right">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JR_Pa:r-1_SI', + 'rte1-id': 'q-justifyright-2', + 'desc': 'query the "justifyright" state', + 'pad': '<p align="right">foo[bar]baz</p>', + 'expected': True }, + + { 'id': 'JR_SPANs:ta:r-1_SI', + 'rte1-id': 'q-justifyright-3', + 'desc': 'query the "justifyright" state', + 'pad': '<span style="text-align: right">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JR_SPAN.jr-1-SI', + 'desc': 'query the "justifyright" state', + 'pad': '<span class="jr">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JR_MYJR-1-SI', + 'desc': 'query the "justifyright" state', + 'pad': '<myjr>foo[bar]baz</myjr>', + 'expected': True } + ] + } + ] +} + +QUERYSTATE_TESTS_CSS = { + 'id': 'QSC', + 'caption': 'queryCommandState Tests, using styleWithCSS', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': True, + + 'Proposed': QUERYSTATE_TESTS['Proposed'] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py new file mode 100644 index 000000000..af23a428c --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py @@ -0,0 +1,226 @@ + +QUERYSUPPORTED_TESTS = { + 'id': 'Q', + 'caption': 'queryCommandSupported Tests', + 'pad': 'foo[bar]baz', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + 'expected': True, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'HTML5 commands', + 'tests': [ + { 'id': 'SELECTALL_TEXT-1', + 'desc': 'check whether the "selectall" command is supported', + 'qcsupported': 'selectall' }, + + { 'id': 'UNSELECT_TEXT-1', + 'desc': 'check whether the "unselect" command is supported', + 'qcsupported': 'unselect' }, + + { 'id': 'UNDO_TEXT-1', + 'desc': 'check whether the "undo" command is supported', + 'qcsupported': 'undo' }, + + { 'id': 'REDO_TEXT-1', + 'desc': 'check whether the "redo" command is supported', + 'qcsupported': 'redo' }, + + { 'id': 'BOLD_TEXT-1', + 'desc': 'check whether the "bold" command is supported', + 'qcsupported': 'bold' }, + + { 'id': 'BOLD_B', + 'desc': 'check whether the "bold" command is supported', + 'qcsupported': 'bold', + 'pad': '<b>foo[bar]baz</b>' }, + + { 'id': 'ITALIC_TEXT-1', + 'desc': 'check whether the "italic" command is supported', + 'qcsupported': 'italic' }, + + { 'id': 'ITALIC_I', + 'desc': 'check whether the "italic" command is supported', + 'qcsupported': 'italic', + 'pad': '<i>foo[bar]baz</i>' }, + + { 'id': 'UNDERLINE_TEXT-1', + 'desc': 'check whether the "underline" command is supported', + 'qcsupported': 'underline' }, + + { 'id': 'STRIKETHROUGH_TEXT-1', + 'desc': 'check whether the "strikethrough" command is supported', + 'qcsupported': 'strikethrough' }, + + { 'id': 'SUBSCRIPT_TEXT-1', + 'desc': 'check whether the "subscript" command is supported', + 'qcsupported': 'subscript' }, + + { 'id': 'SUPERSCRIPT_TEXT-1', + 'desc': 'check whether the "superscript" command is supported', + 'qcsupported': 'superscript' }, + + { 'id': 'FORMATBLOCK_TEXT-1', + 'desc': 'check whether the "formatblock" command is supported', + 'qcsupported': 'formatblock' }, + + { 'id': 'CREATELINK_TEXT-1', + 'desc': 'check whether the "createlink" command is supported', + 'qcsupported': 'createlink' }, + + { 'id': 'UNLINK_TEXT-1', + 'desc': 'check whether the "unlink" command is supported', + 'qcsupported': 'unlink' }, + + { 'id': 'INSERTHTML_TEXT-1', + 'desc': 'check whether the "inserthtml" command is supported', + 'qcsupported': 'inserthtml' }, + + { 'id': 'INSERTHORIZONTALRULE_TEXT-1', + 'desc': 'check whether the "inserthorizontalrule" command is supported', + 'qcsupported': 'inserthorizontalrule' }, + + { 'id': 'INSERTIMAGE_TEXT-1', + 'desc': 'check whether the "insertimage" command is supported', + 'qcsupported': 'insertimage' }, + + { 'id': 'INSERTLINEBREAK_TEXT-1', + 'desc': 'check whether the "insertlinebreak" command is supported', + 'qcsupported': 'insertlinebreak' }, + + { 'id': 'INSERTPARAGRAPH_TEXT-1', + 'desc': 'check whether the "insertparagraph" command is supported', + 'qcsupported': 'insertparagraph' }, + + { 'id': 'INSERTORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertorderedlist" command is supported', + 'qcsupported': 'insertorderedlist' }, + + { 'id': 'INSERTUNORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertunorderedlist" command is supported', + 'qcsupported': 'insertunorderedlist' }, + + { 'id': 'INSERTTEXT_TEXT-1', + 'desc': 'check whether the "inserttext" command is supported', + 'qcsupported': 'inserttext' }, + + { 'id': 'DELETE_TEXT-1', + 'desc': 'check whether the "delete" command is supported', + 'qcsupported': 'delete' }, + + { 'id': 'FORWARDDELETE_TEXT-1', + 'desc': 'check whether the "forwarddelete" command is supported', + 'qcsupported': 'forwarddelete' } + ] + }, + + { 'desc': 'MIDAS commands', + 'tests': [ + { 'id': 'STYLEWITHCSS_TEXT-1', + 'desc': 'check whether the "styleWithCSS" command is supported', + 'qcsupported': 'styleWithCSS' }, + + { 'id': 'CONTENTREADONLY_TEXT-1', + 'desc': 'check whether the "contentreadonly" command is supported', + 'qcsupported': 'contentreadonly' }, + + { 'id': 'BACKCOLOR_TEXT-1', + 'desc': 'check whether the "backcolor" command is supported', + 'qcsupported': 'backcolor' }, + + { 'id': 'FORECOLOR_TEXT-1', + 'desc': 'check whether the "forecolor" command is supported', + 'qcsupported': 'forecolor' }, + + { 'id': 'HILITECOLOR_TEXT-1', + 'desc': 'check whether the "hilitecolor" command is supported', + 'qcsupported': 'hilitecolor' }, + + { 'id': 'FONTNAME_TEXT-1', + 'desc': 'check whether the "fontname" command is supported', + 'qcsupported': 'fontname' }, + + { 'id': 'FONTSIZE_TEXT-1', + 'desc': 'check whether the "fontsize" command is supported', + 'qcsupported': 'fontsize' }, + + { 'id': 'INCREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "increasefontsize" command is supported', + 'qcsupported': 'increasefontsize' }, + + { 'id': 'DECREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "decreasefontsize" command is supported', + 'qcsupported': 'decreasefontsize' }, + + { 'id': 'HEADING_TEXT-1', + 'desc': 'check whether the "heading" command is supported', + 'qcsupported': 'heading' }, + + { 'id': 'INDENT_TEXT-1', + 'desc': 'check whether the "indent" command is supported', + 'qcsupported': 'indent' }, + + { 'id': 'OUTDENT_TEXT-1', + 'desc': 'check whether the "outdent" command is supported', + 'qcsupported': 'outdent' }, + + { 'id': 'CREATEBOOKMARK_TEXT-1', + 'desc': 'check whether the "createbookmark" command is supported', + 'qcsupported': 'createbookmark' }, + + { 'id': 'UNBOOKMARK_TEXT-1', + 'desc': 'check whether the "unbookmark" command is supported', + 'qcsupported': 'unbookmark' }, + + { 'id': 'JUSTIFYCENTER_TEXT-1', + 'desc': 'check whether the "justifycenter" command is supported', + 'qcsupported': 'justifycenter' }, + + { 'id': 'JUSTIFYFULL_TEXT-1', + 'desc': 'check whether the "justifyfull" command is supported', + 'qcsupported': 'justifyfull' }, + + { 'id': 'JUSTIFYLEFT_TEXT-1', + 'desc': 'check whether the "justifyleft" command is supported', + 'qcsupported': 'justifyleft' }, + + { 'id': 'JUSTIFYRIGHT_TEXT-1', + 'desc': 'check whether the "justifyright" command is supported', + 'qcsupported': 'justifyright' }, + + { 'id': 'REMOVEFORMAT_TEXT-1', + 'desc': 'check whether the "removeformat" command is supported', + 'qcsupported': 'removeformat' }, + + { 'id': 'COPY_TEXT-1', + 'desc': 'check whether the "copy" command is supported', + 'qcsupported': 'copy' }, + + { 'id': 'CUT_TEXT-1', + 'desc': 'check whether the "cut" command is supported', + 'qcsupported': 'cut' }, + + { 'id': 'PASTE_TEXT-1', + 'desc': 'check whether the "paste" command is supported', + 'qcsupported': 'paste' } + ] + }, + + { 'desc': 'Other tests', + 'tests': [ + { 'id': 'garbage-1_TEXT-1', + 'desc': 'check correct return value with garbage input', + 'qcsupported': '#!#@7', + 'expected': False } + ] + } + ] +} + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py new file mode 100644 index 000000000..793b7cb6c --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py @@ -0,0 +1,429 @@ + +QUERYVALUE_TESTS = { + 'id': 'QV', + 'caption': 'queryCommandValue Tests', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] query bold value', + 'qcvalue': 'bold', + 'tests': [ + { 'id': 'B_TEXT_SI', + 'desc': 'query the "bold" value', + 'pad': 'foo[bar]baz', + 'expected': 'false' }, + + { 'id': 'B_B-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<b>foo[bar]baz</b>', + 'expected': 'true' }, + + { 'id': 'B_STRONG-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<strong>foo[bar]baz</strong>', + 'expected': 'true' }, + + { 'id': 'B_SPANs:fw:b-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span style="font-weight: bold">foo[bar]baz</span>', + 'expected': 'true' }, + + { 'id': 'B_SPANs:fw:n-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span style="font-weight: normal">foo[bar]baz</span>', + 'expected': 'false' }, + + { 'id': 'B_Bs:fw:n-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<b><span style="font-weight: normal">foo[bar]baz</span></b>', + 'expected': 'false' }, + + { 'id': 'B_SPAN.b-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span class="b">foo[bar]baz</span>', + 'expected': 'true' }, + + { 'id': 'B_MYB-1-SI', + 'desc': 'query the "bold" value', + 'pad': '<myb>foo[bar]baz</myb>', + 'expected': 'true' } + ] + }, + + { 'desc': '[HTML5] query italic value', + 'qcvalue': 'italic', + 'tests': [ + { 'id': 'I_TEXT_SI', + 'desc': 'query the "bold" value', + 'pad': 'foo[bar]baz', + 'expected': 'false' }, + + { 'id': 'I_I-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<i>foo[bar]baz</i>', + 'expected': 'true' }, + + { 'id': 'I_EM-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<em>foo[bar]baz</em>', + 'expected': 'true' }, + + { 'id': 'I_SPANs:fs:i-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span style="font-style: italic">foo[bar]baz</span>', + 'expected': 'true' }, + + { 'id': 'I_SPANs:fs:n-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span style="font-style: normal">foo[bar]baz</span>', + 'expected': 'false' }, + + { 'id': 'I_I-SPANs:fs:n-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<i><span style="font-style: normal">foo[bar]baz</span></i>', + 'expected': 'false' }, + + { 'id': 'I_SPAN.i-1_SI', + 'desc': 'query the "italic" value', + 'pad': '<span class="i">foo[bar]baz</span>', + 'expected': 'true' }, + + { 'id': 'I_MYI-1-SI', + 'desc': 'query the "italic" value', + 'pad': '<myi>foo[bar]baz</myi>', + 'expected': 'true' } + ] + }, + + { 'desc': '[HTML5] query block formatting value', + 'qcvalue': 'formatblock', + 'tests': [ + { 'id': 'FB_TEXT-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': 'foobar^baz', + 'expected': '', + 'accept': 'normal' }, + + { 'id': 'FB_H1-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<h1>foobar^baz</h1>', + 'expected': 'h1' }, + + { 'id': 'FB_PRE-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<pre>foobar^baz</pre>', + 'expected': 'pre' }, + + { 'id': 'FB_BQ-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<blockquote>foobar^baz</blockquote>', + 'expected': 'blockquote' }, + + { 'id': 'FB_ADDRESS-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<address>foobar^baz</address>', + 'expected': 'address' }, + + { 'id': 'FB_H1-H2-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<h1>foo<h2>ba^r</h2>baz</h1>', + 'expected': 'h2' }, + + { 'id': 'FB_H1-H2-1_SL', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': '<h1>fo[o<h2>ba]r</h2>baz</h1>', + 'expected': 'h1' }, + + { 'id': 'FB_H1-H2-1_SR', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': '<h1>foo<h2>b[ar</h2>ba]z</h1>', + 'expected': 'h1' }, + + { 'id': 'FB_TEXT-ADDRESS-1_SL', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': 'fo[o<ADDRESS>ba]r</ADDRESS>baz', + 'expected': '', + 'accept': 'normal' }, + + { 'id': 'FB_TEXT-ADDRESS-1_SR', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': 'foo<ADDRESS>b[ar</ADDRESS>ba]z', + 'expected': '', + 'accept': 'normal' }, + + { 'id': 'FB_H1-H2.TEXT.H2-1_SM', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': '<h1><h2>fo[o</h2>bar<h2>b]az</h2></h1>', + 'expected': 'h1' } + ] + }, + + { 'desc': '[MIDAS] query heading type', + 'qcvalue': 'heading', + 'tests': [ + { 'id': 'H_H1-1_SC', + 'desc': 'query the "heading" type', + 'pad': '<h1>foobar^baz</h1>', + 'expected': 'h1', + 'accept': '<h1>' }, + + { 'id': 'H_H3-1_SC', + 'desc': 'query the "heading" type', + 'pad': '<h3>foobar^baz</h3>', + 'expected': 'h3', + 'accept': '<h3>' }, + + { 'id': 'H_H1-H2-H3-H4-1_SC', + 'desc': 'query the "heading" type within nested heading tags', + 'pad': '<h1><h2><h3><h4>foobar^baz</h4></h3></h2></h1>', + 'expected': 'h4', + 'accept': '<h4>' }, + + { 'id': 'H_P-1_SC', + 'desc': 'query the "heading" type outside of a heading', + 'pad': '<p>foobar^baz</p>', + 'expected': '' } + ] + }, + + { 'desc': '[MIDAS] query font name', + 'qcvalue': 'fontname', + 'tests': [ + { 'id': 'FN_FONTf:a-1_SI', + 'rte1-id': 'q-fontname-0', + 'desc': 'query the "fontname" value', + 'pad': '<font face="arial">foo[bar]baz</font>', + 'expected': 'arial' }, + + { 'id': 'FN_SPANs:ff:a-1_SI', + 'rte1-id': 'q-fontname-1', + 'desc': 'query the "fontname" value', + 'pad': '<span style="font-family: arial">foo[bar]baz</span>', + 'expected': 'arial' }, + + { 'id': 'FN_FONTf:a.s:ff:c-1_SI', + 'rte1-id': 'q-fontname-2', + 'desc': 'query the "fontname" value', + 'pad': '<font face="arial" style="font-family: courier">foo[bar]baz</font>', + 'expected': 'courier' }, + + { 'id': 'FN_FONTf:a-FONTf:c-1_SI', + 'rte1-id': 'q-fontname-3', + 'desc': 'query the "fontname" value', + 'pad': '<font face="arial"><font face="courier">foo[bar]baz</font></font>', + 'expected': 'courier' }, + + { 'id': 'FN_SPANs:ff:c-FONTf:a-1_SI', + 'rte1-id': 'q-fontname-4', + 'desc': 'query the "fontname" value', + 'pad': '<span style="font-family: courier"><font face="arial">foo[bar]baz</font></span>', + 'expected': 'arial' }, + + { 'id': 'FN_SPAN.fs18px-1_SI', + 'desc': 'query the "fontname" value', + 'pad': '<span class="courier">foo[bar]baz</span>', + 'expected': 'courier' }, + + { 'id': 'FN_MYCOURIER-1-SI', + 'desc': 'query the "fontname" value', + 'pad': '<mycourier>foo[bar]baz</mycourier>', + 'expected': 'courier' } + ] + }, + + { 'desc': '[MIDAS] query font size', + 'qcvalue': 'fontsize', + 'tests': [ + { 'id': 'FS_FONTsz:4-1_SI', + 'rte1-id': 'q-fontsize-0', + 'desc': 'query the "fontsize" value', + 'pad': '<font size=4>foo[bar]baz</font>', + 'expected': '18px' }, + + { 'id': 'FS_FONTs:fs:l-1_SI', + 'desc': 'query the "fontsize" value', + 'pad': '<font style="font-size: large">foo[bar]baz</font>', + 'expected': '18px' }, + + { 'id': 'FS_FONT.ass.s:fs:l-1_SI', + 'rte1-id': 'q-fontsize-1', + 'desc': 'query the "fontsize" value', + 'pad': '<font class="Apple-style-span" style="font-size: large">foo[bar]baz</font>', + 'expected': '18px' }, + + { 'id': 'FS_FONTsz:1.s:fs:xl-1_SI', + 'rte1-id': 'q-fontsize-2', + 'desc': 'query the "fontsize" value', + 'pad': '<font size=1 style="font-size: x-large">foo[bar]baz</font>', + 'expected': '24px' }, + + { 'id': 'FS_SPAN.large-1_SI', + 'desc': 'query the "fontsize" value', + 'pad': '<span class="large">foo[bar]baz</span>', + 'expected': 'large' }, + + { 'id': 'FS_SPAN.fs18px-1_SI', + 'desc': 'query the "fontsize" value', + 'pad': '<span class="fs18px">foo[bar]baz</span>', + 'expected': '18px' }, + + { 'id': 'FA_MYLARGE-1-SI', + 'desc': 'query the "fontsize" value', + 'pad': '<mylarge>foo[bar]baz</mylarge>', + 'expected': 'large' }, + + { 'id': 'FA_MYFS18PX-1-SI', + 'desc': 'query the "fontsize" value', + 'pad': '<myfs18px>foo[bar]baz</myfs18px>', + 'expected': '18px' } + ] + }, + + { 'desc': '[MIDAS] query background color', + 'qcvalue': 'backcolor', + 'tests': [ + { 'id': 'BC_FONTs:bc:fca-1_SI', + 'rte1-id': 'q-backcolor-0', + 'desc': 'query the "backcolor" value', + 'pad': '<font style="background-color: #ffccaa">foo[bar]baz</font>', + 'expected': '#ffccaa' }, + + { 'id': 'BC_SPANs:bc:abc-1_SI', + 'rte1-id': 'q-backcolor-2', + 'desc': 'query the "backcolor" value', + 'pad': '<span style="background-color: #aabbcc">foo[bar]baz</span>', + 'expected': '#aabbcc' }, + + { 'id': 'BC_FONTs:bc:084-SPAN-1_SI', + 'desc': 'query the "backcolor" value, where the color was set on an ancestor', + 'pad': '<font style="background-color: #008844"><span>foo[bar]baz</span></font>', + 'expected': '#008844' }, + + { 'id': 'BC_SPANs:bc:cde-SPAN-1_SI', + 'desc': 'query the "backcolor" value, where the color was set on an ancestor', + 'pad': '<span style="background-color: #ccddee"><span>foo[bar]baz</span></span>', + 'expected': '#ccddee' }, + + { 'id': 'BC_SPAN.ass.s:bc:rgb-1_SI', + 'rte1-id': 'q-backcolor-1', + 'desc': 'query the "backcolor" value', + 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">foo[bar]baz</span>', + 'expected': '#ff0000' }, + + { 'id': 'BC_SPAN.bcred-1_SI', + 'desc': 'query the "backcolor" value', + 'pad': '<span class="bcred">foo[bar]baz</span>', + 'expected': 'red' }, + + { 'id': 'BC_MYBCRED-1-SI', + 'desc': 'query the "backcolor" value', + 'pad': '<mybcred>foo[bar]baz</mybcred>', + 'expected': 'red' } + ] + }, + + { 'desc': '[MIDAS] query text color', + 'qcvalue': 'forecolor', + 'tests': [ + { 'id': 'FC_FONTc:f00-1_SI', + 'rte1-id': 'q-forecolor-0', + 'desc': 'query the "forecolor" value', + 'pad': '<font color="#ff0000">foo[bar]baz</font>', + 'expected': '#ff0000' }, + + { 'id': 'FC_SPANs:c:0f0-1_SI', + 'rte1-id': 'q-forecolor-1', + 'desc': 'query the "forecolor" value', + 'pad': '<span style="color: #00ff00">foo[bar]baz</span>', + 'expected': '#00ff00' }, + + { 'id': 'FC_FONTc:333.s:c:999-1_SI', + 'rte1-id': 'q-forecolor-2', + 'desc': 'query the "forecolor" value', + 'pad': '<font color="#333333" style="color: #999999">foo[bar]baz</font>', + 'expected': '#999999' }, + + { 'id': 'FC_FONTc:641-SPAN-1_SI', + 'desc': 'query the "forecolor" value, where the color was set on an ancestor', + 'pad': '<font color="#664411"><span>foo[bar]baz</span></font>', + 'expected': '#664411' }, + + { 'id': 'FC_SPANs:c:d95-SPAN-1_SI', + 'desc': 'query the "forecolor" value, where the color was set on an ancestor', + 'pad': '<span style="color: #dd9955"><span>foo[bar]baz</span></span>', + 'expected': '#dd9955' }, + + { 'id': 'FC_SPAN.red-1_SI', + 'desc': 'query the "forecolor" value', + 'pad': '<span class="red">foo[bar]baz</span>', + 'expected': 'red' }, + + { 'id': 'FC_MYRED-1-SI', + 'desc': 'query the "forecolor" value', + 'pad': '<myred>foo[bar]baz</myred>', + 'expected': 'red' } + ] + }, + + { 'desc': '[MIDAS] query hilight color (same as background color)', + 'qcvalue': 'hilitecolor', + 'tests': [ + { 'id': 'HC_FONTs:bc:fc0-1_SI', + 'rte1-id': 'q-hilitecolor-0', + 'desc': 'query the "hilitecolor" value', + 'pad': '<font style="background-color: #ffcc00">foo[bar]baz</font>', + 'expected': '#ffcc00' }, + + { 'id': 'HC_SPANs:bc:a0c-1_SI', + 'rte1-id': 'q-hilitecolor-2', + 'desc': 'query the "hilitecolor" value', + 'pad': '<span style="background-color: #aa00cc">foo[bar]baz</span>', + 'expected': '#aa00cc' }, + + { 'id': 'HC_SPAN.ass.s:bc:rgb-1_SI', + 'rte1-id': 'q-hilitecolor-1', + 'desc': 'query the "hilitecolor" value', + 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">foo[bar]baz</span>', + 'expected': '#ff0000' }, + + { 'id': 'HC_FONTs:bc:83e-SPAN-1_SI', + 'desc': 'query the "hilitecolor" value, where the color was set on an ancestor', + 'pad': '<font style="background-color: #8833ee"><span>foo[bar]baz</span></font>', + 'expected': '#8833ee' }, + + { 'id': 'HC_SPANs:bc:b12-SPAN-1_SI', + 'desc': 'query the "hilitecolor" value, where the color was set on an ancestor', + 'pad': '<span style="background-color: #bb1122"><span>foo[bar]baz</span></span>', + 'expected': '#bb1122' }, + + { 'id': 'HC_SPAN.bcred-1_SI', + 'desc': 'query the "hilitecolor" value', + 'pad': '<span class="bcred">foo[bar]baz</span>', + 'expected': 'red' }, + + { 'id': 'HC_MYBCRED-1-SI', + 'desc': 'query the "hilitecolor" value', + 'pad': '<mybcred>foo[bar]baz</mybcred>', + 'expected': 'red' } + ] + } + ] +} + +QUERYVALUE_TESTS_CSS = { + 'id': 'QVC', + 'caption': 'queryCommandValue Tests, using styleWithCSS', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': True, + + 'Proposed': QUERYVALUE_TESTS['Proposed'] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py new file mode 100644 index 000000000..35891386a --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py @@ -0,0 +1,772 @@ + +SELECTION_TESTS = { + 'id': 'S', + 'caption': 'Selection Tests', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'selectall', + 'command': 'selectall', + 'tests': [ + { 'id': 'SELALL_TEXT-1_SI', + 'desc': 'select all, text only', + 'pad': 'foo [bar] baz', + 'expected': [ '[foo bar baz]', + '{foo bar baz}' ] }, + + { 'id': 'SELALL_I-1_SI', + 'desc': 'select all, with outer tags', + 'pad': '<i>foo [bar] baz</i>', + 'expected': '{<i>foo bar baz</i>}' } + ] + }, + + { 'desc': 'unselect', + 'command': 'unselect', + 'tests': [ + { 'id': 'UNSEL_TEXT-1_SI', + 'desc': 'unselect', + 'pad': 'foo [bar] baz', + 'expected': 'foo bar baz' } + ] + }, + + { 'desc': 'sel.modify (generic)', + 'tests': [ + { 'id': 'SM:m.f.c_TEXT-1_SC-1', + 'desc': 'move caret 1 character forward', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foo b^ar baz', + 'expected': 'foo ba^r baz' }, + + { 'id': 'SM:m.b.c_TEXT-1_SC-1', + 'desc': 'move caret 1 character backward', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo b^ar baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:m.f.c_TEXT-1_SI-1', + 'desc': 'move caret forward (sollapse selection)', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foo [bar] baz', + 'expected': 'foo bar^ baz' }, + + { 'id': 'SM:m.b.c_TEXT-1_SI-1', + 'desc': 'move caret backward (collapse selection)', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo [bar] baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:m.f.w_TEXT-1_SC-1', + 'desc': 'move caret 1 word forward', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': 'foo b^ar baz', + 'expected': 'foo bar^ baz' }, + + { 'id': 'SM:m.f.w_TEXT-1_SC-2', + 'desc': 'move caret 1 word forward', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': 'foo^ bar baz', + 'expected': 'foo bar^ baz' }, + + { 'id': 'SM:m.f.w_TEXT-1_SI-1', + 'desc': 'move caret 1 word forward from non-collapsed selection', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': 'foo [bar] baz', + 'expected': 'foo bar baz^' }, + + { 'id': 'SM:m.b.w_TEXT-1_SC-1', + 'desc': 'move caret 1 word backward', + 'function': 'sel.modify("move", "backward", "word");', + 'pad': 'foo b^ar baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:m.b.w_TEXT-1_SC-3', + 'desc': 'move caret 1 word backward', + 'function': 'sel.modify("move", "backward", "word");', + 'pad': 'foo bar ^baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:m.b.w_TEXT-1_SI-1', + 'desc': 'move caret 1 word backward from non-collapsed selection', + 'function': 'sel.modify("move", "backward", "word");', + 'pad': 'foo [bar] baz', + 'expected': '^foo bar baz' } + ] + }, + + { 'desc': 'sel.modify: move forward over combining diacritics, etc.', + 'tests': [ + { 'id': 'SM:m.f.c_CHAR-2_SC-1', + 'desc': 'move 1 character forward over combined o with diaeresis', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^öbarbaz', + 'expected': 'foö^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-3_SC-1', + 'desc': 'move 1 character forward over character with combining diaeresis above', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^öbarbaz', + 'expected': 'foö^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-4_SC-1', + 'desc': 'move 1 character forward over character with combining diaeresis below', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^o̤barbaz', + 'expected': 'foo̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SC-1', + 'desc': 'move 1 character forward over character with combining diaeresis above and below', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^ö̤barbaz', + 'expected': 'foö̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SI-1', + 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection on diaeresis above', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foo[̈]̤barbaz', + 'expected': 'foö̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SI-2', + 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection on diaeresis below', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foö[̤]barbaz', + 'expected': 'foö̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SL', + 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection oblique on diaeresis and preceding text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo[ö]̤barbaz', + 'expected': 'foö̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SR', + 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection oblique on diaeresis and following text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foö[̤bar]baz', + 'expected': 'foö̤bar^baz' }, + + { 'id': 'SM:m.f.c_CHAR-6_SC-1', + 'desc': 'move 1 character forward over character with enclosing square', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^o⃞barbaz', + 'expected': 'foo⃞^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-7_SC-1', + 'desc': 'move 1 character forward over character with combining long solidus overlay', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^o̸barbaz', + 'expected': 'foo̸^barbaz' } + ] + }, + + { 'desc': 'sel.modify: move backward over combining diacritics, etc.', + 'tests': [ + { 'id': 'SM:m.b.c_CHAR-2_SC-1', + 'desc': 'move 1 character backward over combined o with diaeresis', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö^barbaz', + 'expected': 'fo^öbarbaz' }, + + { 'id': 'SM:m.b.c_CHAR-3_SC-1', + 'desc': 'move 1 character backward over character with combining diaeresis above', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö^barbaz', + 'expected': 'fo^öbarbaz' }, + + { 'id': 'SM:m.b.c_CHAR-4_SC-1', + 'desc': 'move 1 character backward over character with combining diaeresis below', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo̤^barbaz', + 'expected': 'fo^o̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SC-1', + 'desc': 'move 1 character backward over character with combining diaeresis above and below', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö̤^barbaz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SI-1', + 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection on diaeresis above', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo[̈]̤barbaz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SI-2', + 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection on diaeresis below', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö[̤]barbaz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SL', + 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection oblique on diaeresis and preceding text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'fo[ö]̤barbaz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SR', + 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection oblique on diaeresis and following text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö[̤bar]baz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-6_SC-1', + 'desc': 'move 1 character backward over character with enclosing square', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo⃞^barbaz', + 'expected': 'fo^o⃞barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-7_SC-1', + 'desc': 'move 1 character backward over character with combining long solidus overlay', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo̸^barbaz', + 'expected': 'fo^o̸barbaz' } + ] + }, + + { 'desc': 'sel.modify: move forward/backward/left/right in RTL text', + 'tests': [ + { 'id': 'SM:m.f.c_Pdir:rtl-1_SC-1', + 'desc': 'move caret forward 1 character in right-to-left text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': '<p dir="rtl">foo b^ar baz</p>', + 'expected': '<p dir="rtl">foo ba^r baz</p>' }, + + { 'id': 'SM:m.b.c_Pdir:rtl-1_SC-1', + 'desc': 'move caret backward 1 character in right-to-left text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': '<p dir="rtl">foo ba^r baz</p>', + 'expected': '<p dir="rtl">foo b^ar baz</p>' }, + + { 'id': 'SM:m.r.c_Pdir:rtl-1_SC-1', + 'desc': 'move caret 1 character to the right in LTR text within RTL context', + 'function': 'sel.modify("move", "right", "character");', + 'pad': '<p dir="rtl">foo b^ar baz</p>', + 'expected': '<p dir="rtl">foo ba^r baz</p>' }, + + { 'id': 'SM:m.l.c_Pdir:rtl-1_SC-1', + 'desc': 'move caret 1 character to the left in LTR text within RTL context', + 'function': 'sel.modify("move", "left", "character");', + 'pad': '<p dir="rtl">foo ba^r baz</p>', + 'expected': '<p dir="rtl">foo b^ar baz</p>' }, + + + { 'id': 'SM:m.f.c_TEXT:ar-1_SC-1', + 'desc': 'move caret forward 1 character in Arabic text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'مرح^با العالم', + 'expected': 'مرحب^ا العالم' }, + + { 'id': 'SM:m.b.c_TEXT:ar-1_SC-1', + 'desc': 'move caret backward 1 character in Arabic text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'مرح^با العالم', + 'expected': 'مر^حبا العالم' }, + + { 'id': 'SM:m.f.c_TEXT:he-1_SC-1', + 'desc': 'move caret forward 1 character in Hebrew text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'של^ום עולם', + 'expected': 'שלו^ם עולם' }, + + { 'id': 'SM:m.b.c_TEXT:he-1_SC-1', + 'desc': 'move caret backward 1 character in Hebrew text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'של^ום עולם', + 'expected': 'ש^לום עולם' }, + + + { 'id': 'SM:m.f.c_BDOdir:rtl-1_SC-1', + 'desc': 'move caret forward 1 character inside <bdo>', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foo <bdo dir="rtl">b^ar</bdo> baz', + 'expected': 'foo <bdo dir="rtl">ba^r</bdo> baz' }, + + { 'id': 'SM:m.b.c_BDOdir:rtl-1_SC-1', + 'desc': 'move caret backward 1 character inside <bdo>', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo <bdo dir="rtl">ba^r</bdo> baz', + 'expected': 'foo <bdo dir="rtl">b^ar</bdo> baz' }, + + { 'id': 'SM:m.r.c_BDOdir:rtl-1_SC-1', + 'desc': 'move caret 1 character to the right inside <bdo>', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'foo <bdo dir="rtl">ba^r</bdo> baz', + 'expected': 'foo <bdo dir="rtl">b^ar</bdo> baz' }, + + { 'id': 'SM:m.l.c_BDOdir:rtl-1_SC-1', + 'desc': 'move caret 1 character to the left inside <bdo>', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'foo <bdo dir="rtl">b^ar</bdo> baz', + 'expected': 'foo <bdo dir="rtl">ba^r</bdo> baz' }, + + + { 'id': 'SM:m.f.c_TEXTrle-1_SC-rtl-1', + 'desc': 'move caret forward in RTL text within RLE-PDF marks', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "(RLE)‫car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫car يعني سيا^رة‬(PDF)".' }, + + { 'id': 'SM:m.b.c_TEXTrle-1_SC-rtl-1', + 'desc': 'move caret backward in RTL text within RLE-PDF marks', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "(RLE)‫car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫car يعني س^يارة‬(PDF)".' }, + + { 'id': 'SM:m.r.c_TEXTrle-1_SC-rtl-1', + 'desc': 'move caret 1 character to the right in RTL text within RLE-PDF marks', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "(RLE)‫car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫car يعني س^يارة‬(PDF)".' }, + + { 'id': 'SM:m.l.c_TEXTrle-1_SC-rtl-1', + 'desc': 'move caret 1 character to the left in RTL text within RLE-PDF marks', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "(RLE)‫car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫car يعني سيا^رة‬(PDF)".' }, + + { 'id': 'SM:m.f.c_TEXTrle-1_SC-ltr-1', + 'desc': 'move caret forward in LTR text within RLE-PDF marks', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "(RLE)‫c^ar يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫ca^r يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.b.c_TEXTrle-1_SC-ltr-1', + 'desc': 'move caret backward in LTR text within RLE-PDF marks', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "(RLE)‫ca^r يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫c^ar يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.r.c_TEXTrle-1_SC-ltr-1', + 'desc': 'move caret 1 character to the right in LTR text within RLE-PDF marks', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "(RLE)‫c^ar يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫ca^r يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.l.c_TEXTrle-1_SC-ltr-1', + 'desc': 'move caret 1 character to the left in LTR text within RLE-PDF marks', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "(RLE)‫ca^r يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫c^ar يعني سيارة‬(PDF)".' }, + + + { 'id': 'SM:m.f.c_TEXTrlo-1_SC-rtl-1', + 'desc': 'move caret forward in RTL text within RLO-PDF marks', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "(RLO)‮car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮car يعني سيا^رة‬(PDF)".' }, + + { 'id': 'SM:m.b.c_TEXTrlo-1_SC-rtl-1', + 'desc': 'move caret backward in RTL text within RLO-PDF marks', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "(RLO)‮car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮car يعني س^يارة‬(PDF)".' }, + + { 'id': 'SM:m.r.c_TEXTrlo-1_SC-rtl-1', + 'desc': 'move caret 1 character to the right in RTL text within RLO-PDF marks', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "(RLO)‮car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮car يعني س^يارة‬(PDF)".' }, + + { 'id': 'SM:m.l.c_TEXTrlo-1_SC-rtl-1', + 'desc': 'move caret 1 character to the left in RTL text within RLO-PDF marks', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "(RLO)‮car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮car يعني سيا^رة‬(PDF)".' }, + + { 'id': 'SM:m.f.c_TEXTrlo-1_SC-ltr-1', + 'desc': 'move caret forward in Latin text within RLO-PDF marks', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "(RLO)‮c^ar يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮ca^r يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.b.c_TEXTrlo-1_SC-ltr-1', + 'desc': 'move caret backward in Latin text within RLO-PDF marks', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "(RLO)‮ca^r يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮c^ar يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.r.c_TEXTrlo-1_SC-ltr-1', + 'desc': 'move caret 1 character to the right in Latin text within RLO-PDF marks', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "(RLO)‮ca^r يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮c^ar يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.l.c_TEXTrlo-1_SC-ltr-1', + 'desc': 'move caret 1 character to the left in Latin text within RLO-PDF marks', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "(RLO)‮c^ar يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮ca^r يعني سيارة‬(PDF)".' }, + + + { 'id': 'SM:m.f.c_TEXTrlm-1_SC-1', + 'desc': 'move caret forward in RTL text within neutral characters followed by RLM', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "يعني سيارة!^?!‏(RLM)".', + 'expected': 'I said, "يعني سيارة!?^!‏(RLM)".' }, + + { 'id': 'SM:m.b.c_TEXTrlm-1_SC-1', + 'desc': 'move caret backward in RTL text within neutral characters followed by RLM', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "يعني سيارة!?^!‏(RLM)".', + 'expected': 'I said, "يعني سيارة!^?!‏(RLM)".' }, + + { 'id': 'SM:m.r.c_TEXTrlm-1_SC-1', + 'desc': 'move caret 1 character to the right in RTL text within neutral characters followed by RLM', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "يعني سيارة!?^!‏(RLM)".', + 'expected': 'I said, "يعني سيارة!^?!‏(RLM)".' }, + + { 'id': 'SM:m.l.c_TEXTrlm-1_SC-1', + 'desc': 'move caret 1 character to the left in RTL text within neutral characters followed by RLM', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "يعني سيارة!^?!‏(RLM)".', + 'expected': 'I said, "يعني سيارة!?^!‏(RLM)".' } + ] + }, + + { 'desc': 'sel.modify: move forward/backward over words in Japanese text', + 'tests': [ + { 'id': 'SM:m.f.w_TEXT-jp_SC-1', + 'desc': 'move caret forward 1 word in Japanese text (adjective)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '^面白い例文をテストしましょう。', + 'expected': '面白い^例文をテストしましょう。' }, + + { 'id': 'SM:m.f.w_TEXT-jp_SC-2', + 'desc': 'move caret forward 1 word in Japanese text (in the middle of a word)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '面^白い例文をテストしましょう。', + 'expected': '面白い^例文をテストしましょう。' }, + + { 'id': 'SM:m.f.w_TEXT-jp_SC-3', + 'desc': 'move caret forward 1 word in Japanese text (noun)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '面白い^例文をテストしましょう。', + 'expected': [ '面白い例文^をテストしましょう。', + '面白い例文を^テストしましょう。' ] }, + + { 'id': 'SM:m.f.w_TEXT-jp_SC-4', + 'desc': 'move caret forward 1 word in Japanese text (Katakana)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '面白い例文を^テストしましょう。', + 'expected': '面白い例文をテスト^しましょう。' }, + + { 'id': 'SM:m.f.w_TEXT-jp_SC-5', + 'desc': 'move caret forward 1 word in Japanese text (verb)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '面白い例文をテスト^しましょう。', + 'expected': '面白い例文をテストしましょう^。' } + ] + }, + + { 'desc': 'sel.modify: extend selection forward', + 'tests': [ + { 'id': 'SM:e.f.c_TEXT-1_SC-1', + 'desc': 'extend selection 1 character forward', + 'function': 'sel.modify("extend", "forward", "character");', + 'pad': 'foo ^bar baz', + 'expected': 'foo [b]ar baz' }, + + { 'id': 'SM:e.f.c_TEXT-1_SI-1', + 'desc': 'extend selection 1 character forward', + 'function': 'sel.modify("extend", "forward", "character");', + 'pad': 'foo [b]ar baz', + 'expected': 'foo [ba]r baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SC-1', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': 'foo ^bar baz', + 'expected': 'foo [bar] baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SI-1', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': 'foo [b]ar baz', + 'expected': 'foo [bar] baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SI-2', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': 'foo [bar] baz', + 'expected': 'foo [bar baz]' } + ] + }, + + { 'desc': 'sel.modify: extend selection backward, shrinking it', + 'tests': [ + { 'id': 'SM:e.b.c_TEXT-1_SI-2', + 'desc': 'extend selection 1 character backward', + 'function': 'sel.modify("extend", "backward", "character");', + 'pad': 'foo [bar] baz', + 'expected': 'foo [ba]r baz' }, + + { 'id': 'SM:e.b.c_TEXT-1_SI-1', + 'desc': 'extend selection 1 character backward', + 'function': 'sel.modify("extend", "backward", "character");', + 'pad': 'foo [b]ar baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SI-3', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo [bar baz]', + 'expected': 'foo [bar] baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SI-2', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo [bar] baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SI-4', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo b[ar baz]', + 'expected': 'foo b[ar] baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SI-5', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo b[ar] baz', + 'expected': 'foo b^ar baz' } + ] + }, + + { 'desc': 'sel.modify: extend selection backward, creating or extending a reverse selections', + 'tests': [ + { 'id': 'SM:e.b.c_TEXT-1_SC-1', + 'desc': 'extend selection 1 character backward', + 'function': 'sel.modify("extend", "backward", "character");', + 'pad': 'foo b^ar baz', + 'expected': 'foo ]b[ar baz' }, + + { 'id': 'SM:e.b.c_TEXT-1_SIR-1', + 'desc': 'extend selection 1 character backward', + 'function': 'sel.modify("extend", "backward", "character");', + 'pad': 'foo b]a[r baz', + 'expected': 'foo ]ba[r baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SIR-1', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo b]a[r baz', + 'expected': 'foo ]ba[r baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SIR-2', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo ]ba[r baz', + 'expected': ']foo ba[r baz' } + ] + }, + + { 'desc': 'sel.modify: extend selection forward, shrinking a reverse selections', + 'tests': [ + { 'id': 'SM:e.f.c_TEXT-1_SIR-1', + 'desc': 'extend selection 1 character forward', + 'function': 'sel.modify("extend", "forward", "character");', + 'pad': 'foo b]a[r baz', + 'expected': 'foo ba^r baz' }, + + { 'id': 'SM:e.f.c_TEXT-1_SIR-2', + 'desc': 'extend selection 1 character forward', + 'function': 'sel.modify("extend", "forward", "character");', + 'pad': 'foo ]ba[r baz', + 'expected': 'foo b]a[r baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SIR-1', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': 'foo ]ba[r baz', + 'expected': 'foo ba^r baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SIR-3', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': ']foo ba[r baz', + 'expected': 'foo ]ba[r baz' } + ] + }, + + { 'desc': 'sel.modify: extend selection forward to line boundary', + 'tests': [ + { 'id': 'SM:e.f.lb_BR.BR-1_SC-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': 'fo^o<br>bar<br>baz', + 'expected': 'fo[o]<br>bar<br>baz' }, + + { 'id': 'SM:e.f.lb_BR.BR-1_SI-1', + 'desc': 'extend selection forward to next line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': 'fo[o]<br>bar<br>baz', + 'expected': 'fo[o<br>bar]<br>baz' }, + + { 'id': 'SM:e.f.lb_BR.BR-1_SM-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': 'fo[o<br>b]ar<br>baz', + 'expected': 'fo[o<br>bar]<br>baz' }, + + { 'id': 'SM:e.f.lb_P.P.P-1_SC-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': '<p>fo^o</p><p>bar</p><p>baz</p>', + 'expected': '<p>fo[o]</p><p>bar</p><p>baz</p>' }, + + { 'id': 'SM:e.f.lb_P.P.P-1_SI-1', + 'desc': 'extend selection forward to next line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': '<p>fo[o]</p><p>bar</p><p>baz</p>', + 'expected': '<p>fo[o</p><p>bar]</p><p>baz</p>' }, + + { 'id': 'SM:e.f.lb_P.P.P-1_SM-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': '<p>fo[o</p><p>b]ar</p><p>baz</p>', + 'expected': '<p>fo[o</p><p>bar]</p><p>baz</p>' }, + + { 'id': 'SM:e.f.lb_P.P.P-1_SMR-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': '<p>foo</p><p>b]a[r</p><p>baz</p>', + 'expected': '<p>foo</p><p>ba[r]</p><p>baz</p>' } + ] + }, + + { 'desc': 'sel.modify: extend selection backward to line boundary', + 'tests': [ + { 'id': 'SM:e.b.lb_BR.BR-1_SC-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': 'foo<br>bar<br>b^az', + 'expected': 'foo<br>bar<br>]b[az' }, + + { 'id': 'SM:e.b.lb_BR.BR-1_SIR-2', + 'desc': 'extend selection backward to previous line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': 'foo<br>bar<br>]b[az', + 'expected': 'foo<br>]bar<br>b[az' }, + + { 'id': 'SM:e.b.lb_BR.BR-1_SMR-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': 'foo<br>ba]r<br>b[az', + 'expected': 'foo<br>]bar<br>b[az' }, + + { 'id': 'SM:e.b.lb_P.P.P-1_SC-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': '<p>foo</p><p>bar</p><p>b^az</p>', + 'expected': '<p>foo</p><p>bar</p><p>]b[az</p>' }, + + { 'id': 'SM:e.b.lb_P.P.P-1_SIR-2', + 'desc': 'extend selection backward to previous line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': '<p>foo</p><p>bar</p><p>]b[az</p>', + 'expected': '<p>foo</p><p>]bar</p><p>b[az</p>' }, + + { 'id': 'SM:e.b.lb_P.P.P-1_SMR-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': '<p>foo</p><p>ba]r</p><p>b[az</p>', + 'expected': '<p>foo</p><p>]bar</p><p>b[az</p>' }, + + { 'id': 'SM:e.b.lb_P.P.P-1_SM-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': '<p>foo</p><p>b[a]r</p><p>baz</p>', + 'expected': '<p>foo</p><p>]b[ar</p><p>baz</p>' } + ] + }, + + { 'desc': 'sel.modify: extend selection forward to next line (NOTE: use identical text in every line!)', + 'tests': [ + { 'id': 'SM:e.f.l_BR.BR-2_SC-1', + 'desc': 'extend selection forward to next line', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': 'fo^o<br>foo<br>foo', + 'expected': 'fo[o<br>fo]o<br>foo' }, + + { 'id': 'SM:e.f.l_BR.BR-2_SI-1', + 'desc': 'extend selection forward to next line', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': 'fo[o]<br>foo<br>foo', + 'expected': 'fo[o<br>foo]<br>foo' }, + + { 'id': 'SM:e.f.l_BR.BR-2_SM-1', + 'desc': 'extend selection forward to next line', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': 'fo[o<br>f]oo<br>foo', + 'expected': 'fo[o<br>foo<br>f]oo' }, + + { 'id': 'SM:e.f.l_P.P-1_SC-1', + 'desc': 'extend selection forward to next line over paragraph boundaries', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': '<p>foo^bar</p><p>foobar</p>', + 'expected': '<p>foo[bar</p><p>foo]bar</p>' }, + + { 'id': 'SM:e.f.l_P.P-1_SMR-1', + 'desc': 'extend selection forward to next line over paragraph boundaries', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': '<p>fo]obar</p><p>foob[ar</p>', + 'expected': '<p>foobar</p><p>fo]ob[ar</p>' } + ] + }, + + { 'desc': 'sel.modify: extend selection backward to previous line (NOTE: use identical text in every line!)', + 'tests': [ + { 'id': 'SM:e.b.l_BR.BR-2_SC-2', + 'desc': 'extend selection backward to previous line', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': 'foo<br>foo<br>f^oo', + 'expected': 'foo<br>f]oo<br>f[oo' }, + + { 'id': 'SM:e.b.l_BR.BR-2_SIR-2', + 'desc': 'extend selection backward to previous line', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': 'foo<br>foo<br>]f[oo', + 'expected': 'foo<br>]foo<br>f[oo' }, + + { 'id': 'SM:e.b.l_BR.BR-2_SMR-2', + 'desc': 'extend selection backward to previous line', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': 'foo<br>fo]o<br>f[oo', + 'expected': 'fo]o<br>foo<br>f[oo' }, + + { 'id': 'SM:e.b.l_P.P-1_SC-2', + 'desc': 'extend selection backward to next line over paragraph boundaries', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': '<p>foobar</p><p>foo^bar</p>', + 'expected': '<p>foo]bar</p><p>foo[bar</p>' }, + + { 'id': 'SM:e.b.l_P.P-1_SM-1', + 'desc': 'extend selection backward to next line over paragraph boundaries', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': '<p>fo[obar</p><p>foob]ar</p>', + 'expected': '<p>fo[ob]ar</p><p>foobar</p>' } + ] + }, + + { 'desc': 'sel.selectAllChildren(<element>)', + 'function': 'sel.selectAllChildren(doc.getElementById("div"));', + 'tests': [ + { 'id': 'SAC:div_DIV-1_SC-1', + 'desc': 'selectAllChildren(<div>)', + 'pad': 'foo<div id="div">bar <span>ba^z</span></div>qoz', + 'expected': [ 'foo<div id="div">[bar <span>baz</span>}</div>qoz', + 'foo<div id="div">{bar <span>baz</span>}</div>qoz' ] }, + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py new file mode 100644 index 000000000..adad65617 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py @@ -0,0 +1,462 @@ + +UNAPPLY_TESTS = { + 'id': 'U', + 'caption': 'Unapply Existing Formatting Tests', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': False, + 'expected': 'foo[bar]baz', + + 'RFC': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'remove link', + 'command': 'unlink', + 'tests': [ + { 'id': 'UNLINK_A-1_SO', + 'desc': 'unlink wrapped <a> element', + 'pad': 'foo[<a>bar</a>]baz' }, + + { 'id': 'UNLINK_A-1_SW', + 'desc': 'unlink <a> element where the selection wraps the full content', + 'pad': 'foo<a>[bar]</a>baz' }, + + { 'id': 'UNLINK_An:a.h:id-1_SO', + 'desc': 'unlink wrapped <a> element that has a name and href attribute', + 'pad': 'foo[<a name="A" href="#UNLINK:An:a.h:id-1_SO">bar</a>]baz' }, + + { 'id': 'UNLINK_A-2_SO', + 'desc': 'unlink contained <a> element', + 'pad': 'foo[b<a>a</a>r]baz' }, + + { 'id': 'UNLINK_A2-1_SO', + 'desc': 'unlink 2 contained <a> elements', + 'pad': 'foo[<a>b</a>a<a>r</a>]baz' } + ] + } + ], + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'remove bold', + 'command': 'bold', + 'tests': [ + { 'id': 'B_B-1_SW', + 'rte1-id': 'u-bold-0', + 'desc': 'Selection within tags; remove <b> tags', + 'pad': 'foo<b>[bar]</b>baz' }, + + { 'id': 'B_B-1_SO', + 'desc': 'Selection outside of tags; remove <b> tags', + 'pad': 'foo[<b>bar</b>]baz' }, + + { 'id': 'B_B-1_SL', + 'desc': 'Selection oblique left; remove <b> tags', + 'pad': 'foo[<b>bar]</b>baz' }, + + { 'id': 'B_B-1_SR', + 'desc': 'Selection oblique right; remove <b> tags', + 'pad': 'foo<b>[bar</b>]baz' }, + + { 'id': 'B_STRONG-1_SW', + 'rte1-id': 'u-bold-1', + 'desc': 'Selection within tags; remove <strong> tags', + 'pad': 'foo<strong>[bar]</strong>baz' }, + + { 'id': 'B_STRONG-1_SO', + 'desc': 'Selection outside of tags; remove <strong> tags', + 'pad': 'foo[<strong>bar</strong>]baz' }, + + { 'id': 'B_STRONG-1_SL', + 'desc': 'Selection oblique left; remove <strong> tags', + 'pad': 'foo[<strong>bar]</strong>baz' }, + + { 'id': 'B_STRONG-1_SR', + 'desc': 'Selection oblique right; remove <strong> tags', + 'pad': 'foo<strong>[bar</strong>]baz' }, + + { 'id': 'B_SPANs:fw:b-1_SW', + 'rte1-id': 'u-bold-2', + 'desc': 'Selection within tags; remove "font-weight: bold"', + 'pad': 'foo<span style="font-weight: bold">[bar]</span>baz' }, + + { 'id': 'B_SPANs:fw:b-1_SO', + 'desc': 'Selection outside of tags; remove "font-weight: bold"', + 'pad': 'foo[<span style="font-weight: bold">bar</span>]baz' }, + + { 'id': 'B_SPANs:fw:b-1_SL', + 'desc': 'Selection oblique left; remove "font-weight: bold"', + 'pad': 'foo[<span style="font-weight: bold">bar]</span>baz' }, + + { 'id': 'B_SPANs:fw:b-1_SR', + 'desc': 'Selection oblique right; remove "font-weight: bold"', + 'pad': 'foo<span style="font-weight: bold">[bar</span>]baz' }, + + { 'id': 'B_B-P3-1_SO12', + 'desc': 'Unbolding multiple paragraphs in inside bolded content with content-model violation', + 'pad': '<b>{<p>foo</p><p>bar</p>}<p>baz</p></b>', + 'expected': [ '<p>[foo</p><p>bar]</p><p><b>baz</b></p>', + '<p>[foo</p><p>bar]</p><b><p>baz</p></b>' ] }, + + { 'id': 'B_B-P-I..P-1_SO-I', + 'desc': 'Unbolding italicized content inside bolded content with content-model violation', + 'pad': '<b><p>foo[<i>bar</i>]</p><p>baz</p></b>', + 'expected': [ '<p><b>foo</b><i>[bar]</i></p><p><b>baz</b></p>', + '<b><p>foo</p></b><p><i>[bar]</i></p><b><p>baz</p></b>' ] }, + + { 'id': 'B_B-2_SL', + 'desc': 'Remove partially covered bold, selection extends left', + 'pad': 'foo [bar <b>baz] qoz</b> quz sic', + 'expected': 'foo [bar baz]<b> qoz</b> quz sic' }, + + { 'id': 'B_B-2_SR', + 'desc': 'Remove partially covered bold, selection extends right', + 'pad': 'foo bar <b>baz [qoz</b> quz] sic', + 'expected': 'foo bar <b>baz </b>[qoz quz] sic' } + ] + }, + + { 'desc': 'remove italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_I-1_SW', + 'rte1-id': 'u-italic-0', + 'desc': 'Selection within tags; remove <i> tags', + 'pad': 'foo<i>[bar]</i>baz' }, + + { 'id': 'I_I-1_SO', + 'desc': 'Selection outside of tags; remove <i> tags', + 'pad': 'foo[<i>bar</i>]baz' }, + + { 'id': 'I_I-1_SL', + 'desc': 'Selection oblique left; remove <i> tags', + 'pad': 'foo[<i>bar]</i>baz' }, + + { 'id': 'I_I-1_SR', + 'desc': 'Selection oblique right; remove <i> tags', + 'pad': 'foo<i>[bar</i>]baz' }, + + { 'id': 'I_EM-1_SW', + 'rte1-id': 'u-italic-1', + 'desc': 'Selection within tags; remove <em> tags', + 'pad': 'foo<em>[bar]</em>baz' }, + + { 'id': 'I_EM-1_SO', + 'desc': 'Selection outside of tags; remove <em> tags', + 'pad': 'foo[<em>bar</em>]baz' }, + + { 'id': 'I_EM-1_SL', + 'desc': 'Selection oblique left; remove <em> tags', + 'pad': 'foo[<em>bar]</em>baz' }, + + { 'id': 'I_EM-1_SR', + 'desc': 'Selection oblique right; remove <em> tags', + 'pad': 'foo<em>[bar</em>]baz' }, + + { 'id': 'I_SPANs:fs:i-1_SW', + 'rte1-id': 'u-italic-2', + 'desc': 'Selection within tags; remove "font-style: italic"', + 'pad': 'foo<span style="font-style: italic">[bar]</span>baz' }, + + { 'id': 'I_SPANs:fs:i-1_SO', + 'desc': 'Selection outside of tags; Italicize "font-style: italic"', + 'pad': 'foo[<span style="font-style: italic">bar</span>]baz' }, + + { 'id': 'I_SPANs:fs:i-1_SL', + 'desc': 'Selection oblique left; Italicize "font-style: italic"', + 'pad': 'foo[<span style="font-style: italic">bar]</span>baz' }, + + { 'id': 'I_SPANs:fs:i-1_SR', + 'desc': 'Selection oblique right; Italicize "font-style: italic"', + 'pad': 'foo<span style="font-style: italic">[bar</span>]baz' }, + + { 'id': 'I_I-P3-1_SO2', + 'desc': 'Unitalicize content with content-model violation', + 'pad': '<i><p>foo</p>{<p>bar</p>}<p>baz</p></i>', + 'expected': [ '<p><i>foo</i></p><p>[bar]</p><p><i>baz</i></p>', + '<i><p>foo</p></i><p>[bar]</p><i><p>baz</p></i>' ] } + ] + }, + + { 'desc': 'remove underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_U-1_SW', + 'rte1-id': 'u-underline-0', + 'desc': 'Selection within tags; remove <u> tags', + 'pad': 'foo<u>[bar]</u>baz' }, + + { 'id': 'U_U-1_SO', + 'desc': 'Selection outside of tags; remove <u> tags', + 'pad': 'foo[<u>bar</u>]baz' }, + + { 'id': 'U_U-1_SL', + 'desc': 'Selection oblique left; remove <u> tags', + 'pad': 'foo[<u>bar]</u>baz' }, + + { 'id': 'U_U-1_SR', + 'desc': 'Selection oblique right; remove <u> tags', + 'pad': 'foo<u>[bar</u>]baz' }, + + { 'id': 'U_SPANs:td:u-1_SW', + 'rte1-id': 'u-underline-1', + 'desc': 'Selection within tags; remove "text-decoration: underline"', + 'pad': 'foo<span style="text-decoration: underline">[bar]</span>baz' }, + + { 'id': 'U_SPANs:td:u-1_SO', + 'desc': 'Selection outside of tags; remove "text-decoration: underline"', + 'pad': 'foo[<span style="text-decoration: underline">bar</span>]baz' }, + + { 'id': 'U_SPANs:td:u-1_SL', + 'desc': 'Selection oblique left; remove "text-decoration: underline"', + 'pad': 'foo[<span style="text-decoration: underline">bar]</span>baz' }, + + { 'id': 'U_SPANs:td:u-1_SR', + 'desc': 'Selection oblique right; remove "text-decoration: underline"', + 'pad': 'foo<span style="text-decoration: underline">[bar</span>]baz' }, + + { 'id': 'U_U-S-1_SO', + 'desc': 'Removing underline from underlined content with striked content', + 'pad': '<u>foo[bar<s>baz</s>quoz]</u>', + 'expected': '<u>foo</u>[bar<s>baz</s>quoz]' }, + + { 'id': 'U_U-S-2_SI', + 'desc': 'Removing underline from striked content inside underlined content', + 'pad': '<u><s>foo[bar]baz</s>quoz</u>', + 'expected': '<s><u>foo</u>[bar]<u>baz</u>quoz</s>' }, + + { 'id': 'U_U-P3-1_SO', + 'desc': 'Removing underline from underlined content with content-model violation', + 'pad': '<u><p>foo</p>{<p>bar</p>}<p>baz</p></u>', + 'expected': [ '<p><u>foo</u></p><p>[bar]</p><p><u>baz</u></p>', + '<u><p>foo</p></u><p>[bar]</p><u><p>baz</p></u>' ] } + ] + }, + + { 'desc': 'remove strike through', + 'command': 'strikethrough', + 'tests': [ + { 'id': 'S_S-1_SW', + 'rte1-id': 'u-strikethrough-1', + 'desc': 'Selection within tags; remove <s> tags', + 'pad': 'foo<s>[bar]</s>baz' }, + + { 'id': 'S_S-1_SO', + 'desc': 'Selection outside of tags; remove <s> tags', + 'pad': 'foo[<s>bar</s>]baz' }, + + { 'id': 'S_S-1_SL', + 'desc': 'Selection oblique left; remove <s> tags', + 'pad': 'foo[<s>bar]</s>baz' }, + + { 'id': 'S_S-1_SR', + 'desc': 'Selection oblique right; remove <s> tags', + 'pad': 'foo<s>[bar</s>]baz' }, + + { 'id': 'S_STRIKE-1_SW', + 'rte1-id': 'u-strikethrough-0', + 'desc': 'Selection within tags; remove <strike> tags', + 'pad': 'foo<strike>[bar]</strike>baz' }, + + { 'id': 'S_STRIKE-1_SO', + 'desc': 'Selection outside of tags; remove <strike> tags', + 'pad': 'foo[<strike>bar</strike>]baz' }, + + { 'id': 'S_STRIKE-1_SL', + 'desc': 'Selection oblique left; remove <strike> tags', + 'pad': 'foo[<strike>bar]</strike>baz' }, + + { 'id': 'S_STRIKE-2_SR', + 'desc': 'Selection oblique right; remove <strike> tags', + 'pad': 'foo<strike>[bar</strike>]baz' }, + + { 'id': 'S_DEL-1_SW', + 'rte1-id': 'u-strikethrough-2', + 'desc': 'Selection within tags; remove <del> tags', + 'pad': 'foo<del>[bar]</del>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SW', + 'rte1-id': 'u-strikethrough-3', + 'desc': 'Selection within tags; remove "text-decoration:line-through"', + 'pad': 'foo<span style="text-decoration:line-through">[bar]</span>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SO', + 'desc': 'Selection outside of tags; Italicize "text-decoration:line-through"', + 'pad': 'foo[<span style="text-decoration:line-through">bar</span>]baz' }, + + { 'id': 'S_SPANs:td:lt-1_SL', + 'desc': 'Selection oblique left; Italicize "text-decoration:line-through"', + 'pad': 'foo[<span style="text-decoration:line-through">bar]</span>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SR', + 'desc': 'Selection oblique right; Italicize "text-decoration:line-through"', + 'pad': 'foo<span style="text-decoration:line-through">[bar</span>]baz' }, + + { 'id': 'S_S-U-1_SI', + 'desc': 'Removing underline from underlined content inside striked content', + 'pad': '<s><u>foo[bar]baz</u>quoz</s>', + 'expected': '<s><u>foo</u></s><u>[bar]</u><s><u>baz</u>quoz</s>' }, + + { 'id': 'S_U-S-1_SI', + 'desc': 'Removing underline from striked content inside underlined content', + 'pad': '<u><s>foo[bar]baz</s>quoz</u>', + 'expected': '<u><s>foo</s>[bar]<s>baz</s>quoz</u>' } + ] + }, + + { 'desc': 'remove subscript', + 'command': 'subscript', + 'tests': [ + { 'id': 'SUB_SUB-1_SW', + 'rte1-id': 'u-subscript-0', + 'desc': 'remove subscript', + 'pad': 'foo<sub>[bar]</sub>baz' }, + + { 'id': 'SUB_SPANs:va:sub-1_SW', + 'rte1-id': 'u-subscript-1', + 'desc': 'remove subscript', + 'pad': 'foo<span style="vertical-align: sub">[bar]</span>baz' } + ] + }, + + { 'desc': 'remove superscript', + 'command': 'superscript', + 'tests': [ + { 'id': 'SUP_SUP-1_SW', + 'rte1-id': 'u-superscript-0', + 'desc': 'remove superscript', + 'pad': 'foo<sup>[bar]</sup>baz' }, + + { 'id': 'SUP_SPANs:va:super-1_SW', + 'rte1-id': 'u-superscript-1', + 'desc': 'remove superscript', + 'pad': 'foo<span style="vertical-align: super">[bar]</span>baz' } + ] + }, + + { 'desc': 'remove links', + 'command': 'unlink', + 'tests': [ + { 'id': 'UNLINK_Ahref:url-1_SW', + 'rte1-id': 'u-unlink-0', + 'desc': 'unlink an <a> element with href attribute where all children are selected', + 'pad': 'foo<a href="http://www.goo.gl">[bar]</a>baz' }, + + { 'id': 'UNLINK_A-1_SC', + 'desc': 'unlink an <a> element that contains the collapsed selection', + 'pad': 'foo<a>ba^r</a>baz', + 'expected': 'fooba^rbaz' }, + + { 'id': 'UNLINK_A-1_SI', + 'desc': 'unlink an <a> element that contains the whole selection', + 'pad': 'foo<a>b[a]r</a>baz', + 'expected': 'foob[a]rbaz' }, + + { 'id': 'UNLINK_A-2_SL', + 'desc': 'unlink a partially contained <a> element', + 'pad': 'foo[ba<a>r]ba</a>z' }, + + { 'id': 'UNLINK_A-3_SR', + 'desc': 'unlink a partially contained <a> element', + 'pad': 'fo<a>o[ba</a>r]baz' }, + + { 'id': 'UNLINK_As:d:b.fw:b-1_SW', + 'desc': 'unlink, preserving styles', + 'pad': 'foo<a href="#" style="display: block; font-weight: bold">[bar]</a>baz', + 'expected': 'foo<span style="display: block; font-weight: bold">[bar]</span>baz' }, + + { 'id': 'UNLINK_A-IMG-1_SO', + 'desc': 'unlink a linked image at the start of the content', + 'pad': '{<a href="#"><img src="pic.jpg" align="right" height="140" width="200"></a>abc]', + 'expected': '{<img src="pic.jpg" align="right" height="140" width="200">abc]' } + ] + }, + + { 'desc': 'outdent', + 'command': 'outdent', + 'tests': [ + { 'id': 'OUTDENT_BQ-1_SW', + 'rte1-id': 'u-outdent-0', + 'desc': 'outdent (remove) a <blockquote>', + 'pad': 'foo<blockquote>[bar]</blockquote>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' }, + + { 'id': 'OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW', + 'rte1-id': 'u-outdent-1', + 'desc': 'outdent (remove) a styled <blockquote>', + 'pad': 'foo<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px">[bar]</blockquote>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' }, + + { 'id': 'OUTDENT_OL-LI-1_SW', + 'rte1-id': 'u-outdent-3', + 'desc': 'outdent (remove) an ordered list', + 'pad': 'foo<ol><li>[bar]</li></ol>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' }, + + { 'id': 'OUTDENT_UL-LI-1_SW', + 'rte1-id': 'u-outdent-2', + 'desc': 'outdent (remove) an unordered list', + 'pad': 'foo<ul><li>[bar]</li></ul>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' }, + + { 'id': 'OUTDENT_DIV-1_SW', + 'rte1-id': 'u-outdent-4', + 'desc': 'outdent (remove) a styled <div> with margin', + 'pad': 'foo<div style="margin-left: 40px;">[bar]</div>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' } + ] + }, + + { 'desc': 'remove all formatting', + 'command': 'removeformat', + 'tests': [ + { 'id': 'REMOVEFORMAT_B-1_SW', + 'rte1-id': 'u-removeformat-0', + 'desc': 'remove a <b> tag using "removeformat"', + 'pad': 'foo<b>[bar]</b>baz' }, + + { 'id': 'REMOVEFORMAT_Ahref:url-1_SW', + 'rte1-id': 'u-removeformat-0', + 'desc': 'remove a link using "removeformat"', + 'pad': 'foo<a href="http://www.goo.gl">[bar]</a>baz' }, + + { 'id': 'REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW', + 'rte1-id': 'u-removeformat-2', + 'desc': 'remove a table using "removeformat"', + 'pad': 'foo<table><tbody><tr><td>[bar]</td></tr></tbody></table>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' } + ] + }, + + { 'desc': 'remove bookmark', + 'command': 'unbookmark', + 'tests': [ + { 'id': 'UNBOOKMARK_An:name-1_SW', + 'rte1-id': 'u-unbookmark-0', + 'desc': 'unlink a bookmark (a named <a> element) where all children are selected', + 'pad': 'foo<a name="bookmark">[bar]</a>baz' } + ] + } + ] +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py new file mode 100644 index 000000000..6f934a0f0 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py @@ -0,0 +1,226 @@ + +UNAPPLY_TESTS_CSS = { + 'id': 'UC', + 'caption': 'Unapply Existing Formatting Tests, using styleWithCSS', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': True, + 'expected': 'foo[bar]baz', + + 'Proposed': [ + { 'desc': '', + 'id': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'remove bold', + 'command': 'bold', + 'tests': [ + { 'id': 'B_B-1_SW', + 'desc': 'Selection within tags; remove <b> tags', + 'pad': 'foo<b>[bar]</b>baz' }, + + { 'id': 'B_B-1_SO', + 'desc': 'Selection outside of tags; remove <b> tags', + 'pad': 'foo[<b>bar</b>]baz' }, + + { 'id': 'B_B-1_SL', + 'desc': 'Selection oblique left; remove <b> tags', + 'pad': 'foo[<b>bar]</b>baz' }, + + { 'id': 'B_B-1_SR', + 'desc': 'Selection oblique right; remove <b> tags', + 'pad': 'foo<b>[bar</b>]baz' }, + + { 'id': 'B_STRONG-1_SW', + 'desc': 'Selection within tags; remove <strong> tags', + 'pad': 'foo<strong>[bar]</strong>baz' }, + + { 'id': 'B_STRONG-1_SO', + 'desc': 'Selection outside of tags; remove <strong> tags', + 'pad': 'foo[<strong>bar</strong>]baz' }, + + { 'id': 'B_STRONG-1_SL', + 'desc': 'Selection oblique left; remove <strong> tags', + 'pad': 'foo[<strong>bar]</strong>baz' }, + + { 'id': 'B_STRONG-1_SR', + 'desc': 'Selection oblique right; remove <strong> tags', + 'pad': 'foo<strong>[bar</strong>]baz' }, + + { 'id': 'B_SPANs:fw:b-1_SW', + 'desc': 'Selection within tags; remove "font-weight: bold"', + 'pad': 'foo<span style="font-weight: bold">[bar]</span>baz' }, + + { 'id': 'B_SPANs:fw:b-1_SO', + 'desc': 'Selection outside of tags; remove "font-weight: bold"', + 'pad': 'foo[<span style="font-weight: bold">bar</span>]baz' }, + + { 'id': 'B_SPANs:fw:b-1_SL', + 'desc': 'Selection oblique left; remove "font-weight: bold"', + 'pad': 'foo[<span style="font-weight: bold">bar]</span>baz' }, + + { 'id': 'B_SPANs:fw:b-1_SR', + 'desc': 'Selection oblique right; remove "font-weight: bold"', + 'pad': 'foo<span style="font-weight: bold">[bar</span>]baz' } + ] + }, + + { 'desc': 'remove italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_I-1_SW', + 'desc': 'Selection within tags; remove <i> tags', + 'pad': 'foo<i>[bar]</i>baz' }, + + { 'id': 'I_I-1_SO', + 'desc': 'Selection outside of tags; remove <i> tags', + 'pad': 'foo[<i>bar</i>]baz' }, + + { 'id': 'I_I-1_SL', + 'desc': 'Selection oblique left; remove <i> tags', + 'pad': 'foo[<i>bar]</i>baz' }, + + { 'id': 'I_I-1_SR', + 'desc': 'Selection oblique right; remove <i> tags', + 'pad': 'foo<i>[bar</i>]baz' }, + + { 'id': 'I_EM-1_SW', + 'desc': 'Selection within tags; remove <em> tags', + 'pad': 'foo<em>[bar]</em>baz' }, + + { 'id': 'I_EM-1_SO', + 'desc': 'Selection outside of tags; remove <em> tags', + 'pad': 'foo[<em>bar</em>]baz' }, + + { 'id': 'I_EM-1_SL', + 'desc': 'Selection oblique left; remove <em> tags', + 'pad': 'foo[<em>bar]</em>baz' }, + + { 'id': 'I_EM-1_SR', + 'desc': 'Selection oblique right; remove <em> tags', + 'pad': 'foo<em>[bar</em>]baz' }, + + { 'id': 'I_SPANs:fs:i-1_SW', + 'desc': 'Selection within tags; remove "font-style: italic"', + 'pad': 'foo<span style="font-style: italic">[bar]</span>baz' }, + + { 'id': 'I_SPANs:fs:i-1_SO', + 'desc': 'Selection outside of tags; Italicize "font-style: italic"', + 'pad': 'foo[<span style="font-style: italic">bar</span>]baz' }, + + { 'id': 'I_SPANs:fs:i-1_SL', + 'desc': 'Selection oblique left; Italicize "font-style: italic"', + 'pad': 'foo[<span style="font-style: italic">bar]</span>baz' }, + + { 'id': 'I_SPANs:fs:i-1_SR', + 'desc': 'Selection oblique right; Italicize "font-style: italic"', + 'pad': 'foo<span style="font-style: italic">[bar</span>]baz' } + ] + }, + + { 'desc': 'remove underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_U-1_SW', + 'desc': 'Selection within tags; remove <u> tags', + 'pad': 'foo<u>[bar]</u>baz' }, + + { 'id': 'U_U-1_SO', + 'desc': 'Selection outside of tags; remove <u> tags', + 'pad': 'foo[<u>bar</u>]baz' }, + + { 'id': 'U_U-1_SL', + 'desc': 'Selection oblique left; remove <u> tags', + 'pad': 'foo[<u>bar]</u>baz' }, + + { 'id': 'U_U-1_SR', + 'desc': 'Selection oblique right; remove <u> tags', + 'pad': 'foo<u>[bar</u>]baz' }, + + { 'id': 'U_SPANs:td:u-1_SW', + 'desc': 'Selection within tags; remove "text-decoration: underline"', + 'pad': 'foo<span style="text-decoration: underline">[bar]</span>baz' }, + + { 'id': 'U_SPANs:td:u-1_SO', + 'desc': 'Selection outside of tags; remove "text-decoration: underline"', + 'pad': 'foo[<span style="text-decoration: underline">bar</span>]baz' }, + + { 'id': 'U_SPANs:td:u-1_SL', + 'desc': 'Selection oblique left; remove "text-decoration: underline"', + 'pad': 'foo[<span style="text-decoration: underline">bar]</span>baz' }, + + { 'id': 'U_SPANs:td:u-1_SR', + 'desc': 'Selection oblique right; remove "text-decoration: underline"', + 'pad': 'foo<span style="text-decoration: underline">[bar</span>]baz' } + ] + }, + + { 'desc': 'remove strike-through', + 'command': 'strikethrough', + 'tests': [ + { 'id': 'S_S-1_SW', + 'desc': 'Selection within tags; remove <s> tags', + 'pad': 'foo<s>[bar]</s>baz' }, + + { 'id': 'S_S-1_SO', + 'desc': 'Selection outside of tags; remove <s> tags', + 'pad': 'foo[<s>bar</s>]baz' }, + + { 'id': 'S_S-1_SL', + 'desc': 'Selection oblique left; remove <s> tags', + 'pad': 'foo[<s>bar]</s>baz' }, + + { 'id': 'S_S-1_SR', + 'desc': 'Selection oblique right; remove <s> tags', + 'pad': 'foo<s>[bar</s>]baz' }, + + { 'id': 'S_STRIKE-1_SW', + 'desc': 'Selection within tags; remove <strike> tags', + 'pad': 'foo<strike>[bar]</strike>baz' }, + + { 'id': 'S_STRIKE-1_SO', + 'desc': 'Selection outside of tags; remove <strike> tags', + 'pad': 'foo[<strike>bar</strike>]baz' }, + + { 'id': 'S_STRIKE-1_SL', + 'desc': 'Selection oblique left; remove <strike> tags', + 'pad': 'foo[<strike>bar]</strike>baz' }, + + { 'id': 'S_STRIKE-1_SR', + 'desc': 'Selection oblique right; remove <strike> tags', + 'pad': 'foo<strike>[bar</strike>]baz' }, + + { 'id': 'S_SPANs:td:lt-1_SW', + 'desc': 'Selection within tags; remove "text-decoration:line-through"', + 'pad': 'foo<span style="text-decoration:line-through">[bar]</span>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SO', + 'desc': 'Selection outside of tags; Italicize "text-decoration:line-through"', + 'pad': 'foo[<span style="text-decoration:line-through">bar</span>]baz' }, + + { 'id': 'S_SPANs:td:lt-1_SL', + 'desc': 'Selection oblique left; Italicize "text-decoration:line-through"', + 'pad': 'foo[<span style="text-decoration:line-through">bar]</span>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SR', + 'desc': 'Selection oblique right; Italicize "text-decoration:line-through"', + 'pad': 'foo<span style="text-decoration:line-through">[bar</span>]baz' }, + + { 'id': 'S_SPANc:s-1_SW', + 'desc': 'Unapply "strike-through" on interited CSS style', + 'checkClass': True, + 'pad': 'foo<span class="s">[bar]</span>baz' }, + + { 'id': 'S_SPANc:s-2_SI', + 'desc': 'Unapply "strike-through" on interited CSS style', + 'pad': '<span class="s">foo[bar]baz</span>', + 'checkClass': True, + 'expected': '<span class="s">foo</span>[bar]<span class="s">baz</span>' } + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html new file mode 100644 index 000000000..4e27b0554 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <title>Rich Text 2 Unit Test Example</title> + + <!-- utility scripts --> + <script type="text/javascript" src="static/js/variables.js"></script> + <script type="text/javascript" src="static/js/canonicalize.js"></script> + <script type="text/javascript" src="static/js/compare.js"></script> + <script type="text/javascript" src="static/js/pad.js"></script> + <script type="text/javascript" src="static/js/range.js"></script> + <script type="text/javascript" src="static/js/units.js"></script> + <script type="text/javascript" src="static/js/run.js"></script> + <!-- you do not need static/js/output.js --> + + <!-- + Tests - note that those have the extensions .py, + but can be used as JS files directly. + --> + <script type="text/javascript" src="tests/selection.py"></script> + <script type="text/javascript" src="tests/apply.py"></script> + <script type="text/javascript" src="tests/applyCSS.py"></script> + <script type="text/javascript" src="tests/change.py"></script> + <script type="text/javascript" src="tests/changeCSS.py"></script> + <script type="text/javascript" src="tests/unapply.py"></script> + <script type="text/javascript" src="tests/unapplyCSS.py"></script> + <script type="text/javascript" src="tests/delete.py"></script> + <script type="text/javascript" src="tests/forwarddelete.py"></script> + <script type="text/javascript" src="tests/insert.py"></script> + <script type="text/javascript" src="tests/querySupported.py"></script> + <script type="text/javascript" src="tests/queryEnabled.py"></script> + <script type="text/javascript" src="tests/queryIndeterm.py"></script> + <script type="text/javascript" src="tests/queryState.py"></script> + <script type="text/javascript" src="tests/queryValue.py"></script> + + <!-- Do something --> + <script type="text/javascript"> + function runTest() { + initVariables(); + initEditorDocs(); + + runTestSuite(UNAPPLY_TESTS); + + // Below alert is just a simple demonstration on how to access the test results. + // Note that we only ran UNAPPLY tests above, so we have only results from that test set. + // + // The 'results' structure is as follows: + // + // results structure containing all results + // [<suite ID>] structure containing the results for the given suite *) + // .count number of tests in the given suite + // .valscore sum of all test value results (HTML or query value) + // .selscore sum of all selection results (HTML tests only) + // [<class ID>] structure containing the results for the given class **) + // .count number of tests in the given suite + // .valscore sum of all test value results (HTML or query value) + // .selscore sum of all selection results (HTML tests only) + // [<test ID>] structure containing the reults for a given test ***) + // .valscore value score (0 or 1), minimum over all containers + // .selscore selection score (0 or 1), minimum over all containers (HTML tests only) + // .valresult worst test value result (integer, see variables.js) + // .selresult worst selection result (integer, see variables.js) + // [<cont. ID>] structure containing the results of the test for a given container ****) + // .valscore value score (0 or 1) + // .selscore selection score (0 or 1) + // .valresult value result (integer, see variables.js) + // .selresult selection result (integer, see variables.js) + // .output output string (mainly for use by the online version) + // .innerHTML inner HTML of the testing container (<div> or <body>) after the test + // .outerHTML outer HTML of the testing container (<div> or <body>) after the test + // .bodyInnerHTML inner HTML of the <body> after the test + // .bodyOuterHTML outer HTML of the <body> after the test + // + // *) <suite ID>: a 1-3 character ID, e.g. UNAPPLY_TESTS.id, or 'U' (both referring the same suite) + // **) <class ID>: one of 'Proposed', 'RFC' or 'Finalized' + // ***) <test ID>: the ID of the test, without the leading 'RTE2-<suite ID>_' part + // ****) <container ID>: one of 'div' (test within a <div contenteditable="true">) + // 'dM' (test with designMode = 'on') + // 'body' (test within a <body contenteditable="true">) + + alert("Result of 'Apply' tests:\nOut of " + + results[UNAPPLY_TESTS.id].count + " tests\n" + + results[UNAPPLY_TESTS.id].valscore + " had correct HTML, and\n" + + results[UNAPPLY_TESTS.id].selscore + " had a correct result selection\n(in all testing containers)." + + "\n\n" + + "Test RTE2-U_B_B-1_SW results with a contenteditable <body>:\n" + + results['U']['Proposed']['B_B-1_SW']['body'].valscore + " points for the value result, and\n" + + results['U']['Proposed']['B_B-1_SW']['body'].selscore + " points for the selection" + + "" + ); + } + </script> +</head> + +<body onload="runTest()"> + <iframe name="iframe-dM" id="iframe-dM" src="static/editable-dM.html"></iframe> + <iframe name="iframe-body" id="iframe-body" src="static/editable-body.html"></iframe> + <iframe name="iframe-div" id="iframe-div" src="static/editable-div.html"></iframe> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream b/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream new file mode 100644 index 000000000..baeb76745 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream @@ -0,0 +1,19 @@ +#!/bin/sh + +set -x + +if test -d richtext2; then + rm -drf richtext2; +fi + +svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext2 richtext2 | tail -1 | sed 's/[^0-9]//g' > current_revision + +find richtext2 -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null + +# Remove test_set.py and other similarly named files because they confuse our mochitest runner +find richtext2 =type f -name test_\* -exec rm -rf \{\} \; 2> /dev/null + +hg add current_revision richtext2 + +hg stat . + diff --git a/editor/libeditor/tests/browserscope/mochitest.ini b/editor/libeditor/tests/browserscope/mochitest.ini new file mode 100644 index 000000000..e6e2db413 --- /dev/null +++ b/editor/libeditor/tests/browserscope/mochitest.ini @@ -0,0 +1,59 @@ +[default] +support-files = + lib/richtext2/current_revision + lib/richtext2/richtext2/common.py + lib/richtext2/richtext2/unittestexample.html + lib/richtext2/richtext2/static/editable-dM.html + lib/richtext2/richtext2/static/editable.css + lib/richtext2/richtext2/static/editable-body.html + lib/richtext2/richtext2/static/editable-div.html + lib/richtext2/richtext2/static/js/variables.js + lib/richtext2/richtext2/static/js/range-bootstrap.js + lib/richtext2/richtext2/static/js/range.js + lib/richtext2/richtext2/static/js/output.js + lib/richtext2/richtext2/static/js/compare.js + lib/richtext2/richtext2/static/js/canonicalize.js + lib/richtext2/richtext2/static/js/pad.js + lib/richtext2/richtext2/static/js/run.js + lib/richtext2/richtext2/static/js/units.js + lib/richtext2/richtext2/static/common.css + lib/richtext2/richtext2/__init__.py + lib/richtext2/richtext2/handlers.py + lib/richtext2/richtext2/templates/output.html + lib/richtext2/richtext2/templates/richtext2.html + lib/richtext2/richtext2/tests/forwarddelete.py + lib/richtext2/richtext2/tests/selection.py + lib/richtext2/richtext2/tests/queryIndeterm.py + lib/richtext2/richtext2/tests/unapplyCSS.py + lib/richtext2/richtext2/tests/apply.py + lib/richtext2/richtext2/tests/unapply.py + lib/richtext2/richtext2/tests/change.py + lib/richtext2/richtext2/tests/queryState.py + lib/richtext2/richtext2/tests/queryValue.py + lib/richtext2/richtext2/tests/__init__.py + lib/richtext2/richtext2/tests/insert.py + lib/richtext2/richtext2/tests/queryEnabled.py + lib/richtext2/richtext2/tests/applyCSS.py + lib/richtext2/richtext2/tests/changeCSS.py + lib/richtext2/richtext2/tests/delete.py + lib/richtext2/richtext2/tests/querySupported.py + lib/richtext2/README + lib/richtext2/update_from_upstream + lib/richtext2/LICENSE + lib/richtext2/README.Mozilla + lib/richtext2/currentStatus.js + lib/richtext/current_revision + lib/richtext/README + lib/richtext/update_from_upstream + lib/richtext/LICENSE + lib/richtext/README.Mozilla + lib/richtext/richtext/editable.html + lib/richtext/richtext/richtext.html + lib/richtext/richtext/js/range.js + lib/richtext/currentStatus.js + +[test_richtext2.html] +subsuite = clipboard +skip-if = os == 'android' && debug # Bug 1202045 +[test_richtext.html] + diff --git a/editor/libeditor/tests/browserscope/test_richtext.html b/editor/libeditor/tests/browserscope/test_richtext.html new file mode 100644 index 000000000..45f8bef38 --- /dev/null +++ b/editor/libeditor/tests/browserscope/test_richtext.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +BrowserScope richtext category tests +--> +<head> + <title>BrowserScope Richtext Tests</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="lib/richtext/currentStatus.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550569">Mozilla Bug 550569</a> +<p id="display"></p> +<div id="content"> + <iframe src="lib/richtext/richtext/richtext.html"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +// Running all of the tests can take a long time, try to account for it +SimpleTest.requestLongerTimeout(5); + +function sendScore(results, continueParams) { + ok(results.length > 1, "At least one test should have been run"); + for (var i = 1; i < results.length; ++i) { + var result = results[i]; + [type, command, param, success] = result.split(/[\-=]/); + var comp = is; + if (isKnownFailure(type, command, param)) { + comp = todo_is; + } + comp(success, "1", "Browserscope richtext category=" + type + + " test=" + command + + " param=" + param); + } +} + +document.getElementsByTagName("iframe")[0].addEventListener("load", function() { + SimpleTest.finish(); +}, false); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/test_richtext2.html b/editor/libeditor/tests/browserscope/test_richtext2.html new file mode 100644 index 000000000..c0ce07a8f --- /dev/null +++ b/editor/libeditor/tests/browserscope/test_richtext2.html @@ -0,0 +1,233 @@ +<!DOCTYPE html> +<html lang="en"> +<!-- +BrowserScope richtext2 category tests + +This test is originally based on the unit test example available as part of the +RichText2 suite: +http://code.google.com/p/browserscope/source/browse/trunk/categories/richtext2/unittestexample.html +--> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <title>BrowserScope Richtext2 Tests</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + + <!-- utility scripts --> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/variables.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/canonicalize.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/compare.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/pad.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/range.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/units.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/run.js"></script> + <!-- you do not need static/js/output.js --> + + <!-- + Tests - note that those have the extensions .py, + but can be used as JS files directly. + --> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/selection.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/apply.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/applyCSS.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/change.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/changeCSS.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/unapply.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/unapplyCSS.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/delete.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/forwarddelete.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/insert.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/querySupported.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryEnabled.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryIndeterm.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryState.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryValue.py"></script> + + <script type="text/javascript" src="lib/richtext2/currentStatus.js"></script> + + <!-- Do something --> + <script type="text/javascript"> + // Set this constant to true in order to get the current status of the test suite. + // This is useful for updating the currentStatus.js file when an editor bug is fixed. + const UPDATE_TEST_RESULTS = false; + + // some tests (at least RTE2-QE_PASTE_TEXT-1) require clipboard data + function startTest() { + SimpleTest.waitForClipboard("foo", + function() { + SpecialPowers.clipboardCopyString("foo"); + }, + runTest, + function() { + ok(false, "Failed to copy a string to the clipboard"); + SimpleTest.finish(); + } + ); + } + + function runTest() { + initVariables(); + initEditorDocs(); + + const tests = [ + SELECTION_TESTS, + APPLY_TESTS, + APPLY_TESTS_CSS, + CHANGE_TESTS, + CHANGE_TESTS_CSS, + UNAPPLY_TESTS, + UNAPPLY_TESTS_CSS, + DELETE_TESTS, + FORWARDDELETE_TESTS, + INSERT_TESTS, + QUERYSUPPORTED_TESTS, + QUERYENABLED_TESTS, + QUERYINDETERM_TESTS, + QUERYSTATE_TESTS, + QUERYVALUE_TESTS, + ]; + + for (var i = 0; i < tests.length; ++i) { + runTestSuite(tests[i]); + } + + // Below alert is just a simple demonstration on how to access the test results. + // Note that we only ran UNAPPLY tests above, so we have only results from that test set. + // + // The 'results' structure is as follows: + // + // results structure containing all results + // [<suite ID>] structure containing the results for the given suite *) + // .count number of tests in the given suite + // .valscore sum of all test value results (HTML or query value) + // .selscore sum of all selection results (HTML tests only) + // [<class ID>] structure containing the results for the given class **) + // .count number of tests in the given suite + // .valscore sum of all test value results (HTML or query value) + // .selscore sum of all selection results (HTML tests only) + // [<test ID>] structure containing the reults for a given test ***) + // .valscore value score (0 or 1), minimum over all containers + // .selscore selection score (0 or 1), minimum over all containers (HTML tests only) + // .valresult worst test value result (integer, see variables.js) + // .selresult worst selection result (integer, see variables.js) + // [<cont. ID>] structure containing the results of the test for a given container ****) + // .valscore value score (0 or 1) + // .selscore selection score (0 or 1) + // .valresult value result (integer, see variables.js) + // .selresult selection result (integer, see variables.js) + // .output output string (mainly for use by the online version) + // .innerHTML inner HTML of the testing container (<div> or <body>) after the test + // .outerHTML outer HTML of the testing container (<div> or <body>) after the test + // .bodyInnerHTML inner HTML of the <body> after the test + // .bodyOuterHTML outer HTML of the <body> after the test + // + // *) <suite ID>: a 1-3 character ID, e.g. UNAPPLY_TESTS.id, or 'U' (both referring the same suite) + // **) <class ID>: one of 'Proposed', 'RFC' or 'Finalized' + // ***) <test ID>: the ID of the test, without the leading 'RTE2-<suite ID>_' part + // ****) <container ID>: one of 'div' (test within a <div contenteditable="true">) + // 'dM' (test with designMode = 'on') + // 'body' (test within a <body contenteditable="true">) + + if (UPDATE_TEST_RESULTS) { + var newKnownFailures = {value: {}, select: {}}; + for (var i = 0; i < tests.length; ++i) { + var category = tests[i]; + for (var group in results[category.id]) { + switch (group) { + // Skip the known properties + case "count": + case "valscore": + case "selscore": + case "time": + break; + default: + for (var test_id in results[category.id][group]) { + switch (test_id) { + // Skip the known properties + case "count": + case "valscore": + case "selscore": + break; + default: + for (var structure in results[category.id][group][test_id]) { + switch (structure) { + // Only look at each test structure + case "dM": + case "body": + case "div": + if (!results[category.id][group][test_id][structure].valscore) { + newKnownFailures.value[category.id + "-" + group + "-" + test_id + "-" + structure] = true; + } + if (!results[category.id][group][test_id][structure].selscore) { + newKnownFailures.select[category.id + "-" + group + "-" + test_id + "-" + structure] = true; + } + } + } + } + } + } + } + } + var resultContainer = document.getElementById("results"); + resultContainer.style.display = ""; + resultContainer.textContent = JSON.stringify(newKnownFailures); + } else { + for (var i = 0; i < tests.length; ++i) { + var category = tests[i]; + for (var group in results[category.id]) { + switch (group) { + // Skip the known properties + case "count": + case "valscore": + case "selscore": + case "time": + break; + default: + for (var test_id in results[category.id][group]) { + switch (test_id) { + // Skip the known properties + case "count": + case "valscore": + case "selscore": + break; + default: + for (var structure in results[category.id][group][test_id]) { + switch (structure) { + // Only look at each test structure + case "dM": + case "body": + case "div": + var row = results[category.id][group][test_id][structure]; + var testName = [category.id, group, test_id, structure].join("-"); + (testName in knownFailures.value ? todo_is : is)( + row.valscore, 1, "Browserscope richtext2 value: " + testName); + (testName in knownFailures.select ? todo_is : is)( + row.selscore, 1, "Browserscope richtext2 selection: " + testName); + } + } + } + } + } + } + } + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + // Running all of the tests can take a long time, try to account for it + SimpleTest.requestLongerTimeout(5); + </script> +</head> + +<body onload="startTest()"> + <iframe name="iframe-dM" id="iframe-dM" src="lib/richtext2/richtext2/static/editable-dM.html"></iframe> + <iframe name="iframe-body" id="iframe-body" src="lib/richtext2/richtext2/static/editable-body.html"></iframe> + <iframe name="iframe-div" id="iframe-div" src="lib/richtext2/richtext2/static/editable-div.html"></iframe> + <pre id="results" style="display: none"></pre> +</body> +</html> diff --git a/editor/libeditor/tests/bug527935.html b/editor/libeditor/tests/bug527935.html new file mode 100644 index 000000000..4bfa1bac2 --- /dev/null +++ b/editor/libeditor/tests/bug527935.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<body> +<div id="content"> + <iframe id="formTarget" name="formTarget"></iframe> + <form action="data:text/html," target="formTarget"> + <input name="test" id="initValue"><input type="submit"> + </form> +</div> +</body> +</html diff --git a/editor/libeditor/tests/bug629172.html b/editor/libeditor/tests/bug629172.html new file mode 100644 index 000000000..e583b2d44 --- /dev/null +++ b/editor/libeditor/tests/bug629172.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> +<script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> +<style> +textarea { resize: none } +</style> +</head> +<body> +<div id="content"> +<textarea id="ltr-ref" style="display: none">test.</textarea> +<textarea id="rtl-ref" style="display: none; direction: rtl">test.</textarea> +</div +</body> +</html> diff --git a/editor/libeditor/tests/chrome.ini b/editor/libeditor/tests/chrome.ini new file mode 100644 index 000000000..98db30001 --- /dev/null +++ b/editor/libeditor/tests/chrome.ini @@ -0,0 +1,14 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = green.png + +[test_bug489202.xul] +[test_bug599983.xul] +[test_bug607584.xul] +[test_bug616590.xul] +[test_bug780908.xul] +[test_contenteditable_text_input_handling.html] +[test_htmleditor_keyevent_handling.html] +[test_set_document_title_transaction.html] +[test_texteditor_keyevent_handling.html] +skip-if = (debug && os=='win') || (os == 'linux') # Bug 1116205, leaks on windows debug, fails delete key on linux diff --git a/editor/libeditor/tests/data/cfhtml-chromium.txt b/editor/libeditor/tests/data/cfhtml-chromium.txt Binary files differnew file mode 100644 index 000000000..7e0253715 --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-chromium.txt diff --git a/editor/libeditor/tests/data/cfhtml-firefox.txt b/editor/libeditor/tests/data/cfhtml-firefox.txt Binary files differnew file mode 100644 index 000000000..cc686d856 --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-firefox.txt diff --git a/editor/libeditor/tests/data/cfhtml-ie.txt b/editor/libeditor/tests/data/cfhtml-ie.txt Binary files differnew file mode 100644 index 000000000..a30bc5295 --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-ie.txt diff --git a/editor/libeditor/tests/data/cfhtml-nocontext.txt b/editor/libeditor/tests/data/cfhtml-nocontext.txt new file mode 100644 index 000000000..aa4882227 --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-nocontext.txt @@ -0,0 +1,18 @@ +Version:0.9
+StartHTML:-1
+EndHTML:-1
+StartFragment:0000000111
+EndFragment:0000000246
+<!--StartFragment-->
+<html>
+ <head>
+ <title>Test</title>
+
+ </head>
+ <body>
+ <p>
+ 3.<b>1415926535897932</b>
+ </p>
+ </body>
+</html>
+<!--EndFragment-->
diff --git a/editor/libeditor/tests/data/cfhtml-ooo.txt b/editor/libeditor/tests/data/cfhtml-ooo.txt Binary files differnew file mode 100644 index 000000000..0bcf7616e --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-ooo.txt diff --git a/editor/libeditor/tests/file_bug549262.html b/editor/libeditor/tests/file_bug549262.html new file mode 100644 index 000000000..92a0c76f3 --- /dev/null +++ b/editor/libeditor/tests/file_bug549262.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> + <body> + <a href="">test</a> + <div id="editor" contenteditable="true">abc</div> + <div style="height: 20000px;"></div> + </body> +</html> diff --git a/editor/libeditor/tests/file_bug586662.html b/editor/libeditor/tests/file_bug586662.html new file mode 100644 index 000000000..298953197 --- /dev/null +++ b/editor/libeditor/tests/file_bug586662.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <body> + <div style="height: 20000px;"></div> + <textarea id="editor"></textarea> + </body> +</html> diff --git a/editor/libeditor/tests/file_bug674770-1.html b/editor/libeditor/tests/file_bug674770-1.html new file mode 100644 index 000000000..6750bb878 --- /dev/null +++ b/editor/libeditor/tests/file_bug674770-1.html @@ -0,0 +1,5 @@ +<!DOCTYPE> +<script> + localStorage["clicked"] = "true"; + close(); +</script> diff --git a/editor/libeditor/tests/file_bug915962.html b/editor/libeditor/tests/file_bug915962.html new file mode 100644 index 000000000..85c5139d3 --- /dev/null +++ b/editor/libeditor/tests/file_bug915962.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <body> + <button>Button</button> + <img src="green.png" usemap="#map"> + <map name="map"> + <!-- This URL ensures that the link doesn't get clicked, since + mochitests cannot access the outside network. --> + <area shape="rect" coords="0,0,10,10" href="https://youtube.com/"> + </map> + <div style="height: 20000px;" tabindex="-1"><hr></div> + </body> +</html> diff --git a/editor/libeditor/tests/file_select_all_without_body.html b/editor/libeditor/tests/file_select_all_without_body.html new file mode 100644 index 000000000..70050a847 --- /dev/null +++ b/editor/libeditor/tests/file_select_all_without_body.html @@ -0,0 +1,41 @@ +<html> +<head> +<script type="text/javascript"> + +function is(aLeft, aRight, aMessage) +{ + window.opener.SimpleTest.is(aLeft, aRight, aMessage); +} + +function unload() +{ + window.opener.SimpleTest.finish(); +} + +function boom() +{ + var root = document.documentElement; + while(root.firstChild) { + root.removeChild(root.firstChild); + } + root.appendChild(document.createTextNode("Mozilla")); + root.focus(); + cespan = document.createElementNS("http://www.w3.org/1999/xhtml", "span"); + cespan.setAttributeNS(null, "contenteditable", "true"); + root.appendChild(cespan); + try { + document.execCommand("selectAll", false, null); + } catch(e) { } + + is(window.getSelection().toString(), "Mozilla", + "The nodes are not selected"); + + window.close(); +} + +window.opener.SimpleTest.waitForFocus(boom, window); + +</script></head> + +<body onunload="unload();"></body> +</html> diff --git a/editor/libeditor/tests/green.png b/editor/libeditor/tests/green.png Binary files differnew file mode 100644 index 000000000..0aaec2093 --- /dev/null +++ b/editor/libeditor/tests/green.png diff --git a/editor/libeditor/tests/mochitest.ini b/editor/libeditor/tests/mochitest.ini new file mode 100644 index 000000000..447fb8b65 --- /dev/null +++ b/editor/libeditor/tests/mochitest.ini @@ -0,0 +1,245 @@ +[DEFAULT] +support-files = + data/cfhtml-chromium.txt + data/cfhtml-firefox.txt + data/cfhtml-ie.txt + data/cfhtml-ooo.txt + data/cfhtml-nocontext.txt + file_bug549262.html + file_bug586662.html + file_bug674770-1.html + file_bug915962.html + file_select_all_without_body.html + green.png + spellcheck.js + +[test_bug46555.html] +[test_bug200416.html] +[test_bug289384.html] +skip-if = os != "mac" +[test_bug290026.html] +[test_bug291780.html] +[test_bug309731.html] +[test_bug316447.html] +[test_bug318065.html] +[test_bug332636.html] +support-files = test_bug332636.html^headers^ +[test_bug366682.html] +skip-if = os == 'android' +[test_bug372345.html] +skip-if = toolkit == 'android' +[test_bug404320.html] +[test_bug408231.html] +skip-if = toolkit == 'android' +[test_bug410986.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug414526.html] +[test_bug417418.html] +skip-if = android_version == '18' # bug 1147989 +[test_bug432225.html] +skip-if = toolkit == 'android' +[test_bug439808.html] +[test_bug442186.html] +[test_bug449243.html] +[test_bug455992.html] +[test_bug456244.html] +[test_bug460740.html] +[test_bug468353.html] +[test_bug471319.html] +[test_bug471722.html] +[test_bug478725.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug480647.html] +[test_bug480972.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug483651.html] +[test_bug484181.html] +skip-if = toolkit == 'android' +[test_bug487524.html] +[test_bug490879.html] +subsuite = clipboard +skip-if = toolkit == 'android' # bug 1299578 +[test_bug502673.html] +[test_bug514156.html] +[test_bug520189.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug525389.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug537046.html] +[test_bug549262.html] +skip-if = toolkit == 'android' +[test_bug550434.html] +skip-if = android_version == '18' # bug 1147989 +[test_bug551704.html] +subsuite = clipboard +[test_bug552782.html] +[test_bug567213.html] +[test_bug569988.html] +skip-if = os == 'android' +[test_bug570144.html] +[test_bug578771.html] +skip-if = android_version == '18' # bug 1147989 +[test_bug586662.html] +skip-if = toolkit == 'android' +[test_bug587461.html] +[test_bug590554.html] +[test_bug592592.html] +[test_bug596001.html] +subsuite = clipboard +[test_bug596333.html] +skip-if = toolkit == 'android' +[test_bug596506.html] +[test_bug597331.html] +skip-if = toolkit == 'android' || asan || (os == "win" && os_version != "5.1") # Bug 718316, Bug 1211213 +[test_bug597784.html] +[test_bug599322.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug599983.html] +[test_bug600570.html] +subsuite = clipboard +skip-if = toolkit == 'android' || (os == "win" && os_version != "5.1") # Bug 718316 +[test_bug602130.html] +[test_bug603556.html] +subsuite = clipboard +[test_bug604532.html] +skip-if = toolkit == 'android' +[test_bug607584.html] +[test_bug611182.html] +skip-if = toolkit == 'android' +[test_bug612128.html] +[test_bug612447.html] +[test_bug620906.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_bug622371.html] +skip-if = toolkit == 'android' #bug 957797 +[test_bug625452.html] +[test_bug629845.html] +[test_bug635636.html] +skip-if = e10s || os == 'android' +[test_bug636465.html] +skip-if = os == 'android' +[test_bug638596.html] +[test_bug640321.html] +skip-if = android_version == '18' # bug 1147989 +[test_bug641466.html] +[test_bug645914.html] +[test_bug646194.html] +[test_bug668599.html] +[test_bug674770-1.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug674770-2.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug674861.html] +[test_bug676401.html] +[test_bug677752.html] +[test_bug681229.html] +subsuite = clipboard +[test_bug686203.html] +[test_bug692520.html] +[test_bug697842.html] +[test_bug725069.html] +[test_bug735059.html] +[test_bug738366.html] +[test_bug740784.html] +[test_bug742261.html] +[test_bug757371.html] +[test_bug757771.html] +[test_bug767684.html] +[test_bug772796.html] +skip-if = toolkit == 'android' # bug 1309431 +[test_bug773262.html] +[test_bug780035.html] +[test_bug787432.html] +[test_bug790475.html] +[test_bug795418.html] +[test_bug795418-2.html] +[test_bug795418-3.html] +[test_bug795418-4.html] +[test_bug795418-5.html] +[test_bug795418-6.html] +[test_bug795785.html] +[test_bug796839.html] +[test_bug830600.html] +subsuite = clipboard +skip-if = e10s +[test_bug832025.html] +[test_bug850043.html] +[test_bug857487.html] +[test_bug858918.html] +[test_bug915962.html] +[test_bug974309.html] +skip-if = toolkit == 'android' +[test_bug966155.html] +skip-if = os != "win" +[test_bug966552.html] +skip-if = os != "win" +[test_bug998188.html] +[test_bug1026397.html] +[test_bug1053048.html] +[test_bug1067255.html] +[test_bug1068979.html] +subsuite = clipboard +[test_bug1094000.html] +[test_bug1100966.html] +skip-if = os == 'android' +[test_bug1102906.html] +skip-if = os == 'android' +[test_bug1101392.html] +subsuite = clipboard +[test_bug1109465.html] +[test_bug1140105.html] +[test_bug1140617.html] +subsuite = clipboard +skip-if = toolkit == 'android' # bug 1299578 +[test_bug1153237.html] +[test_bug1154791.html] +skip-if = os == 'android' +[test_bug1162952.html] +[test_bug1181130-1.html] +[test_bug1181130-2.html] +[test_bug1186799.html] +[test_bug1230473.html] +[test_bug1247483.html] +subsuite = clipboard +skip-if = toolkit == 'android' +[test_bug1248128.html] +[test_bug1250010.html] +[test_bug1257363.html] +[test_bug1248185.html] +[test_bug1258085.html] +[test_bug1268736.html] +[test_bug1270235.html] +[test_bug1310912.html] +skip-if = toolkit == 'android' # bug 1315898 +[test_bug1314790.html] +[test_bug1315065.html] +[test_bug1328023.html] +[test_bug1330796.html] +[test_bug1332876.html] + +[test_CF_HTML_clipboard.html] +subsuite = clipboard +[test_composition_event_created_in_chrome.html] +[test_contenteditable_focus.html] +[test_dom_input_event_on_htmleditor.html] +skip-if = toolkit == 'android' # bug 1054087 +[test_dom_input_event_on_texteditor.html] +[test_dragdrop.html] +skip-if = os == 'android' +[test_keypress_untrusted_event.html] +[test_root_element_replacement.html] +[test_select_all_without_body.html] +[test_spellcheck_pref.html] +skip-if = toolkit == 'android' +[test_backspace_vs.html] +[test_css_chrome_load_access.html] +skip-if = toolkit == 'android' # chrome urls not available due to packaging +[test_selection_move_commands.html] diff --git a/editor/libeditor/tests/spellcheck.js b/editor/libeditor/tests/spellcheck.js new file mode 100644 index 000000000..9d36c3254 --- /dev/null +++ b/editor/libeditor/tests/spellcheck.js @@ -0,0 +1,20 @@ +function isSpellingCheckOk(aEditor, aMisspelledWords) { + var selcon = aEditor.selectionController; + var sel = selcon.getSelection(selcon.SELECTION_SPELLCHECK); + var numWords = sel.rangeCount; + + is(numWords, aMisspelledWords.length, "Correct number of misspellings and words."); + + if (numWords !== aMisspelledWords.length) { + return false; + } + + for (var i = 0; i < numWords; ++i) { + var word = String(sel.getRangeAt(i)); + is(word, aMisspelledWords[i], "Misspelling is what we think it is."); + if (word !== aMisspelledWords[i]) { + return false; + } + } + return true; +} diff --git a/editor/libeditor/tests/test_CF_HTML_clipboard.html b/editor/libeditor/tests/test_CF_HTML_clipboard.html new file mode 100644 index 000000000..4949f40b3 --- /dev/null +++ b/editor/libeditor/tests/test_CF_HTML_clipboard.html @@ -0,0 +1,159 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=572642 +--> +<head> + <title>Test for Bug 572642</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572642">Mozilla Bug 572642</a> +<p id="display"></p> +<div id="content"> + <div id="editor1" contenteditable="true"></div> + <iframe id="editor2"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 572642 **/ + +function copyCF_HTML(cfhtml, success, failure) { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + const CF_HTML = "application/x-moz-nativehtml"; + + function getLoadContext() { + return SpecialPowers.wrap(window).QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); + } + + var cb = Cc["@mozilla.org/widget/clipboard;1"]. + getService(Ci.nsIClipboard); + + var counter = 0; + function copyCF_HTML_worker(success, failure) { + if (++counter > 50) { + ok(false, "Timed out while polling clipboard for pasted data"); + failure(); + return; + } + + var flavors = [CF_HTML]; + if (!cb.hasDataMatchingFlavors(flavors, flavors.length, cb.kGlobalClipboard)) { + setTimeout(function() { copyCF_HTML_worker(success, failure); }, 100); + return; + } + + var trans = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + trans.addDataFlavor(CF_HTML); + cb.getData(trans, cb.kGlobalClipboard); + var data = SpecialPowers.createBlankObject(); + try { + trans.getTransferData(CF_HTML, data, {}); + data = SpecialPowers.wrap(data).value.QueryInterface(Ci.nsISupportsCString).data; + } catch (e) { + setTimeout(function() { copyCF_HTML_worker(success, failure); }, 100); + return; + } + success(); + } + + var trans = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + trans.addDataFlavor(CF_HTML); + var data = Cc["@mozilla.org/supports-cstring;1"]. + createInstance(Ci.nsISupportsCString); + data.data = cfhtml; + trans.setTransferData(CF_HTML, data, cfhtml.length); + cb.setData(trans, null, cb.kGlobalClipboard); + copyCF_HTML_worker(success, failure); +} + +function loadCF_HTMLdata(filename) { + var req = new XMLHttpRequest(); + req.open("GET", filename, false); + req.overrideMimeType("text/plain; charset=x-user-defined"); + req.send(null); + ok(req.status, 200, "Could not read the binary file " + filename); + return req.responseText; +} + +var gTests = [ + // Copied from Firefox + {fileName: "cfhtml-firefox.txt", expected: "Firefox"}, + // Copied from OpenOffice.org + {fileName: "cfhtml-ooo.txt", expected: "hello"}, + // Copied from IE + {fileName: "cfhtml-ie.txt", expected: "browser"}, + // Copied from Chromium + {fileName: "cfhtml-chromium.txt", expected: "Pacific"}, + // CF_HTML with no context specified (StartHTML and EndHTML set to -1) + {fileName: "cfhtml-nocontext.txt", expected: "3.1415926535897932"}, +]; +var gTestIndex = 0; + +SimpleTest.waitForExplicitFinish(); + +for (var i = 0; i < gTests.length; ++i) { + gTests[i].data = loadCF_HTMLdata("data/" + gTests[i].fileName); +} + +function runTest() { + var test = gTests[gTestIndex++]; + + copyCF_HTML(test.data, function() { + // contenteditable + var contentEditable = document.getElementById("editor1"); + contentEditable.innerHTML = ""; + contentEditable.focus(); + synthesizeKey("v", {accelKey: true}); + isnot(contentEditable.textContent.indexOf(test.expected), -1, + "Paste operation for " + test.fileName + " should be successful in contenteditable"); + + // designMode + var iframe = document.getElementById("editor2"); + iframe.addEventListener("load", function() { + iframe.removeEventListener("load", arguments.callee, false); + var doc = iframe.contentDocument; + var win = doc.defaultView; + setTimeout(function() { + win.addEventListener("focus", function() { + win.removeEventListener("focus", arguments.callee, false); + doc.designMode = "on"; + synthesizeKey("v", {accelKey: true}, win); + isnot(doc.body.textContent.indexOf(test.expected), -1, + "Paste operation for " + test.fileName + " should be successful in designMode"); + + if (gTestIndex == gTests.length) + SimpleTest.finish(); + else + runTest(); + }, false); + win.focus(); + }, 0); + }, false); + iframe.src = "data:text/html,"; + }, SimpleTest.finish); +} + +var isMac = ("nsILocalFileMac" in SpecialPowers.Ci); +if (isMac) + SimpleTest.waitForFocus(runTest); +else { + // This test is not yet supported on non-Mac platforms, see bug 574005. + todo(false, "Test not supported on this platform"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_backspace_vs.html b/editor/libeditor/tests/test_backspace_vs.html new file mode 100644 index 000000000..1ee754c95 --- /dev/null +++ b/editor/libeditor/tests/test_backspace_vs.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1216427 +--> +<head> + <title>Test for Bug 1216427</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1216427">Mozilla Bug 1216427</a> +<p id="display"></p> +<div id="content"> + <div id="edit1" contenteditable="true">a☺️b</div><!-- BMP symbol with VS16 --> + <div id="edit2" contenteditable="true">a🌐︎b</div><!-- plane 1 symbol with VS15 --> + <div id="edit3" contenteditable="true">a㐂󠄀b</div><!-- BMP ideograph with VS17 --> + <div id="edit4" contenteditable="true">a𠀀󠄁b</div><!-- SMP ideograph with VS18 --> + <div id="edit5" contenteditable="true">a☺︁︂︃b</div><!-- BMP symbol with extra VSes --> + <div id="edit6" contenteditable="true">a𠀀󠄀󠄁󠄂b</div><!-- SMP symbol with extra VSes --> + <!-- The Regional Indicator combinations here were supported by Apple Color Emoji + even prior to the major extension of coverage in the 10.10.5 timeframe. --> + <div id="edit7" contenteditable="true">a🇨🇳b</div><!-- Regional Indicator flag: CN --> + <div id="edit8" contenteditable="true">a🇨🇳🇩🇪b</div><!-- two RI flags: CN, DE --> + <div id="edit9" contenteditable="true">a🇨🇳🇩🇪🇪🇸b</div><!-- three RI flags: CN, DE, ES --> + <div id="edit10" contenteditable="true">a🇨🇳🇩🇪🇪🇸🇫🇷b</div><!-- four RI flags: CN, DE, ES, FR --> + <div id="edit11" contenteditable="true">a🇨🇳🇩🇪🇪🇸🇫🇷🇬🇧b</div><!-- five RI flags: CN, DE, ES, FR, GB --> + + <div id="edit1b" contenteditable="true">a☺️b</div><!-- BMP symbol with VS16 --> + <div id="edit2b" contenteditable="true">a🌐︎b</div><!-- plane 1 symbol with VS15 --> + <div id="edit3b" contenteditable="true">a㐂󠄀b</div><!-- BMP ideograph with VS17 --> + <div id="edit4b" contenteditable="true">a𠀀󠄁b</div><!-- SMP ideograph with VS18 --> + <div id="edit5b" contenteditable="true">a☺︁︂︃b</div><!-- BMP symbol with extra VSes --> + <div id="edit6b" contenteditable="true">a𠀀󠄀󠄁󠄂b</div><!-- SMP symbol with extra VSes --> + <div id="edit7b" contenteditable="true">a🇨🇳b</div><!-- Regional Indicator flag: CN --> + <div id="edit8b" contenteditable="true">a🇨🇳🇩🇪b</div><!-- two RI flags: CN, DE --> + <div id="edit9b" contenteditable="true">a🇨🇳🇩🇪🇪🇸b</div><!-- three RI flags: CN, DE, ES --> + <div id="edit10b" contenteditable="true">a🇨🇳🇩🇪🇪🇸🇫🇷b</div><!-- four RI flags: CN, DE, ES, FR --> + <div id="edit11b" contenteditable="true">a🇨🇳🇩🇪🇪🇸🇫🇷🇬🇧b</div><!-- five RI flags: CN, DE, ES, FR, GB --> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1216427 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +function test(edit, bsCount) { + edit.focus(); + var sel = window.getSelection(); + sel.collapse(edit.childNodes[0], edit.textContent.length - 1); + for (i = 0; i < bsCount; ++i) { + synthesizeKey("VK_BACK_SPACE", {}); + } + is(edit.textContent, "ab", "The backspace key should delete the characters correctly"); +} + +function testWithMove(edit, offset, bsCount) { + edit.focus(); + var sel = window.getSelection(); + sel.collapse(edit.childNodes[0], 0); + var i; + for (i = 0; i < offset; ++i) { + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_LEFT", {}); + synthesizeKey("VK_RIGHT", {}); + } + for (i = 0; i < bsCount; ++i) { + synthesizeKey("VK_BACK_SPACE", {}); + } + is(edit.textContent, "ab", "The backspace key should delete the characters correctly"); +} + +function runTest() { + /* test backspace-deletion of the middle character(s) */ + test(document.getElementById("edit1"), 1); + test(document.getElementById("edit2"), 1); + test(document.getElementById("edit3"), 1); + test(document.getElementById("edit4"), 1); + test(document.getElementById("edit5"), 1); + test(document.getElementById("edit6"), 1); + + /* + * Tests with Regional Indicator flags: these behave differently depending + * whether an emoji font is present, as ligated flags are edited as single + * characters whereas non-ligated RI characters act individually. + * + * For now, only rely on such an emoji font on OS X 10.7+. (Note that the + * Segoe UI Emoji font on Win8.1 and Win10 does not implement Regional + * Indicator flags.) + * + * Once the Firefox Emoji font is ready, we can load that via @font-face + * and expect these tests to work across all platforms. + */ + hasEmojiFont = + (navigator.platform.indexOf("Mac") == 0 && + /10\.([7-9]|[1-9][0-9])/.test(navigator.oscpu)); + + if (hasEmojiFont) { + test(document.getElementById("edit7"), 1); + test(document.getElementById("edit8"), 2); + test(document.getElementById("edit9"), 3); + test(document.getElementById("edit10"), 4); + test(document.getElementById("edit11"), 5); + } + + /* extra tests with the use of RIGHT and LEFT to get to the right place */ + testWithMove(document.getElementById("edit1b"), 2, 1); + testWithMove(document.getElementById("edit2b"), 2, 1); + testWithMove(document.getElementById("edit3b"), 2, 1); + testWithMove(document.getElementById("edit4b"), 2, 1); + testWithMove(document.getElementById("edit5b"), 2, 1); + testWithMove(document.getElementById("edit6b"), 2, 1); + if (hasEmojiFont) { + testWithMove(document.getElementById("edit7b"), 2, 1); + testWithMove(document.getElementById("edit8b"), 3, 2); + testWithMove(document.getElementById("edit9b"), 4, 3); + testWithMove(document.getElementById("edit10b"), 5, 4); + testWithMove(document.getElementById("edit11b"), 6, 5); + } + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1026397.html b/editor/libeditor/tests/test_bug1026397.html new file mode 100644 index 000000000..487f3e87f --- /dev/null +++ b/editor/libeditor/tests/test_bug1026397.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1026397 +--> +<head> + <title>Test for Bug 1026397</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026397">Mozilla Bug 1026397</a> +<p id="display"></p> +<div id="content"> +<input id="input"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1026397 **/ +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + var input = document.getElementById("input"); + input.focus(); + + function doTest(aMaxLength, aInitialValue, aCaretOffset, + aInsertString, aExpectedValueDuringComposition, + aExpectedValueAfterCompositionEnd, aAdditionalExplanation) + { + input.value = aInitialValue; + var maxLengthStr = ""; + if (aMaxLength >= 0) { + input.maxLength = aMaxLength; + maxLengthStr = aMaxLength.toString(); + } else { + input.removeAttribute("maxlength"); + maxLengthStr = "not specified"; + } + input.selectionStart = input.selectionEnd = aCaretOffset; + if (aAdditionalExplanation) { + aAdditionalExplanation = " " + aAdditionalExplanation; + } else { + aAdditionalExplanation = ""; + } + + synthesizeCompositionChange( + { "composition": + { "string": aInsertString, + "clauses": + [ + { "length": aInsertString.length, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": aInsertString.length, "length": 0 } + }); + is(input.value, aExpectedValueDuringComposition, + "The value of input whose maxlength is " + maxLengthStr + " should be " + + aExpectedValueDuringComposition + " during composition" + aAdditionalExplanation); + synthesizeComposition({ type: "compositioncommitasis" }); + is(input.value, aExpectedValueAfterCompositionEnd, + "The value of input whose maxlength is " + maxLengthStr + " should be " + + aExpectedValueAfterCompositionEnd + " after compositionend" + aAdditionalExplanation); + } + + // maxlength hasn't been specified yet. + doTest(-1, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6"); + + // maxlength="1" + doTest(1, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", ""); + + // maxlength="2" + doTest(2, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7"); + doTest(2, "X", 1, "\uD842\uDFB7\u91CE\u5BB6", "X\uD842\uDFB7\u91CE\u5BB6", "X"); + doTest(2, "Y", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6Y", "Y"); + + // maxlength="3" + doTest(3, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE"); + doTest(3, "A", 1, "\uD842\uDFB7\u91CE\u5BB6", "A\uD842\uDFB7\u91CE\u5BB6", "A\uD842\uDFB7"); + doTest(3, "B", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6B", "\uD842\uDFB7B"); + doTest(3, "CD", 1, "\uD842\uDFB7\u91CE\u5BB6", "C\uD842\uDFB7\u91CE\u5BB6D", "CD"); + + // maxlength="4" + doTest(4, "EF", 1, "\uD842\uDFB7\u91CE\u5BB6", "E\uD842\uDFB7\u91CE\u5BB6F", "E\uD842\uDFB7F"); + doTest(4, "GHI", 1, "\uD842\uDFB7\u91CE\u5BB6", "G\uD842\uDFB7\u91CE\u5BB6HI", "GHI"); + + // maxlength="1", inputting only high surrogate + doTest(1, "", 0, "\uD842", "\uD842", "\uD842", "even if input string is only a high surrogate"); + + // maxlength="1", inputting only low surrogate + doTest(1, "", 0, "\uDFB7", "\uDFB7", "\uDFB7", "even if input string is only a low surrogate"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1053048.html b/editor/libeditor/tests/test_bug1053048.html new file mode 100644 index 000000000..4032d32c2 --- /dev/null +++ b/editor/libeditor/tests/test_bug1053048.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1053048 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1053048</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script type="application/javascript"> + + /** Test for Bug 1053048 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(runTests); + + const nsISelectionPrivate = SpecialPowers.Ci.nsISelectionPrivate; + const nsISelectionListener = SpecialPowers.Ci.nsISelectionListener; + const nsIDOMNSEditableElement = SpecialPowers.Ci.nsIDOMNSEditableElement; + + function runTests() + { + var textarea = SpecialPowers.wrap(document.getElementById("textarea")); + textarea.focus(); + + var editor = textarea.QueryInterface(nsIDOMNSEditableElement).editor; + var selectionPrivate = editor.selection.QueryInterface(nsISelectionPrivate); + + var selectionListener = { + count: 0, + notifySelectionChanged: function (aDocument, aSelection, aReason) + { + ok(true, "selectionStart: " + textarea.selectionStart); + ok(true, "selectionEnd: " + textarea.selectionEnd); + this.count++; + } + }; + + // Move caret to the end of the textarea + synthesizeMouse(textarea, 290, 10, {}); + is(textarea.selectionStart, 3, "selectionStart should be 3 (after \"foo\")"); + is(textarea.selectionEnd, 3, "selectionEnd should be 3 (after \"foo\")"); + + selectionPrivate.addSelectionListener(selectionListener); + + sendKey("RETURN"); + is(selectionListener.count, 1, "nsISelectionListener.notifySelectionChanged() should be called"); + is(textarea.selectionStart, 4, "selectionStart should be 4"); + is(textarea.selectionEnd, 4, "selectionEnd should be 4"); + is(textarea.value, "foo\n", "The line break should be appended"); + + selectionPrivate.removeSelectionListener(selectionListener); + SimpleTest.finish(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1053048">Mozilla Bug 1053048</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> + +<textarea id="textarea" + style="height: 100px; width: 300px; -moz-appearance: none" + spellcheck="false" + onkeydown="this.style.display='block'; this.style.height='200px';">foo</textarea> + +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1067255.html b/editor/libeditor/tests/test_bug1067255.html new file mode 100644 index 000000000..be2d703c5 --- /dev/null +++ b/editor/libeditor/tests/test_bug1067255.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1067255 +--> + +<head> + <title>Test for Bug 1067255</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1067255">Mozilla Bug 1067255</a> + + <pre id="test"> + <script type="application/javascript"> + /** Test for Bug 1067255 **/ + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var text = $("text-field"); + var password = $("password-field"); + + var editor1 = SpecialPowers.wrap(text).editor; + var editor2 = SpecialPowers.wrap(password).editor; + + text.focus(); + text.select(); + + ok(editor1.canCopy(), "can copy, text"); + ok(editor1.canCut(), "can cut, text"); + ok(editor1.canDelete(), "can delete, text"); + + password.focus(); + password.select(); + + // Copy and cut commands don't do anything on passoword fields by default, + // but webpages can hook up event handlers to the event, and thus, we have to + // always keep the cut and copy event enabled in HTML/XHTML documents. + ok(editor2.canCopy(), "can copy, password"); + ok(editor2.canCut(), "can cut, password"); + ok(editor2.canDelete(), "can delete, password"); + + SimpleTest.finish(); + } + </script> + </pre> + + <input type="text" value="Gonzo says hi" id="text-field" /> + <input type="password" value="Jan also" id="password-field" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1068979.html b/editor/libeditor/tests/test_bug1068979.html new file mode 100644 index 000000000..126c39d27 --- /dev/null +++ b/editor/libeditor/tests/test_bug1068979.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1068979 +--> +<head> + <title>Test for Bug 1068979</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1068979">Mozilla Bug 1068979</a> +<p id="display"></p> +<div id="content"> + <div id="editor1" contenteditable="true">𝐀</div> + <div id="editor2" contenteditable="true">a<u>𝐁</u>b</div> + <div id="editor3" contenteditable="true">a𝐂<u>b</u></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1068979 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + // Test backspacing over SMP characters pasted-in to a contentEditable + getSelection().selectAllChildren(document.getElementById("editor1")); + var ed1 = document.getElementById("editor1"); + var ch1 = ed1.textContent; + ed1.focus(); + synthesizeKey("C", {accelKey: true}); + synthesizeKey("V", {accelKey: true}); + synthesizeKey("V", {accelKey: true}); + synthesizeKey("V", {accelKey: true}); + synthesizeKey("V", {accelKey: true}); + is(ed1.textContent, ch1 + ch1 + ch1 + ch1, "Should have four SMP characters"); + sendKey("back_space"); + is(ed1.textContent, ch1 + ch1 + ch1, "Three complete characters should remain"); + sendKey("back_space"); + is(ed1.textContent, ch1 + ch1, "Two complete characters should remain"); + sendKey("back_space"); + is(ed1.textContent, ch1, "Only one complete SMP character should remain"); + ed1.blur(); + + // Test backspacing across an SMP character in a sub-element + getSelection().selectAllChildren(document.getElementById("editor2")); + var ed2 = document.getElementById("editor2"); + ed2.focus(); + sendKey("right"); + sendKey("back_space"); + sendKey("back_space"); + is(ed2.textContent, "a", "Only the 'a' should remain"); + ed2.blur(); + + // Test backspacing across an SMP character from a following sub-element + getSelection().selectAllChildren(document.getElementById("editor3")); + var ed3 = document.getElementById("editor3"); + ed3.focus(); + sendKey("right"); + sendKey("left"); + sendKey("back_space"); + is(ed3.textContent, "ab", "The letters 'ab' should remain"); + ed3.blur(); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1094000.html b/editor/libeditor/tests/test_bug1094000.html new file mode 100644 index 000000000..cc27cc675 --- /dev/null +++ b/editor/libeditor/tests/test_bug1094000.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1094000 +--> +<head> + <title>Test for Bug 1094000</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1094000">Mozilla Bug 1094000</a> +<p id="display"></p> +<div id="content"> + <div id="editor0" contenteditable></div> + <div id="editor1" contenteditable><br></div> + <div id="editor2" contenteditable>a</div> + <div id="editor3" contenteditable>b</div> + <div id="editor4" contenteditable>c</div> + <div id="editor5" contenteditable>d</div> + <div id="editor6" contenteditable>e</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1094000 **/ + +SimpleTest.waitForExplicitFinish(); + +const kIsLinux = navigator.platform.indexOf("Linux") == 0; + +function runTests() +{ + var editor0 = document.getElementById("editor0"); + var editor1 = document.getElementById("editor1"); + var editor2 = document.getElementById("editor2"); + var editor3 = document.getElementById("editor3"); + var editor4 = document.getElementById("editor4"); + var editor5 = document.getElementById("editor5"); + var editor6 = document.getElementById("editor6"); + + ok(editor1.getBoundingClientRect().height - editor0.getBoundingClientRect().height > 1, + "an editor having a <br> element and an empty editor shouldn't be same height"); + ok(Math.abs(editor1.getBoundingClientRect().height - editor2.getBoundingClientRect().height) <= 1, + "an editor having only a <br> element and an editor having \"a\" should be same height"); + + editor2.focus(); + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_BACK_SPACE", {}); + is(editor2.innerHTML, "<br>", + "an editor which had \"a\" should have only <br> element after Backspace keypress"); + ok(Math.abs(editor2.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was removed by Backspace key should have a place to put a caret"); + + editor3.focus(); + synthesizeKey("VK_LEFT", {}); + synthesizeKey("VK_DELETE", {}); + is(editor3.innerHTML, "<br>", + "an editor which had \"b\" should have only <br> element after Delete keypress"); + ok(Math.abs(editor3.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was removed by Delete key should have a place to put a caret"); + + editor4.focus(); + window.getSelection().selectAllChildren(editor4); + synthesizeKey("VK_BACK_SPACE", {}); + is(editor4.innerHTML, "<br>", + "an editor which had \"c\" should have only <br> element after removing selected text with Backspace key"); + ok(Math.abs(editor4.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was selected and removed by Backspace key should have a place to put a caret"); + + editor5.focus(); + window.getSelection().selectAllChildren(editor5); + synthesizeKey("VK_DELETE", {}); + is(editor5.innerHTML, "<br>", + "an editor which had \"d\" should have only <br> element after removing selected text with Delete key"); + ok(Math.abs(editor5.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was selected and removed by Delete key should have a place to put a caret"); + + editor6.focus(); + window.getSelection().selectAllChildren(editor6); + synthesizeKey("x", { accelKey: true }); + is(editor6.innerHTML, "<br>", + "an editor which had \"e\" should have only <br> element after removing selected text by \"Cut\""); + ok(Math.abs(editor6.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was selected and removed by \"Cut\" should have a place to put a caret"); + + editor0.focus(); + synthesizeKey("VK_BACK_SPACE", {}); + is(editor0.innerHTML, "", + "an empty editor should keep being empty even if Backspace key is pressed"); + synthesizeKey("VK_DELETE", {}); + is(editor0.innerHTML, "", + "an empty editor should keep being empty even if Delete key is pressed"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1100966.html b/editor/libeditor/tests/test_bug1100966.html new file mode 100644 index 000000000..28c30c849 --- /dev/null +++ b/editor/libeditor/tests/test_bug1100966.html @@ -0,0 +1,65 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1100966 +--> +<head> + <title>Test for Bug 1100966</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable> +=====<br> +correct<br> +fivee sixx<br> +==== +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1100966 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("content"); + div.focus(); + synthesizeMouseAtCenter(div, {}); + + synthesizeKey(" ", {}); + setTimeout(function() { + synthesizeKey("a", {}); + setTimeout(function() { + synthesizeKey("VK_BACK_SPACE", {}); + + var sel = getSpellCheckSelection(); + is(sel.rangeCount, 2, "We should have two misspelled words"); + is(String(sel.getRangeAt(0)), "fivee", "Correct misspelled word"); + is(String(sel.getRangeAt(1)), "sixx", "Correct misspelled word"); + + SimpleTest.finish(); + },0); + },0); + +}); + +function getSpellCheckSelection() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + var editor = editingSession.getEditorForWindow(window); + var selcon = editor.selectionController; + return selcon.getSelection(selcon.SELECTION_SPELLCHECK); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1101392.html b/editor/libeditor/tests/test_bug1101392.html new file mode 100644 index 000000000..76917203b --- /dev/null +++ b/editor/libeditor/tests/test_bug1101392.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1101392 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1101392</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1101392 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(runTests); + + function runCopyCommand(element, compareText, nextTest) + { + element.focus(); + var expectedEndpoint, sel; + if (element.localName == "textarea") { + element.select(); + expectedEndpoint = element.selectionEnd; + } else { + sel = getSelection(); + sel.selectAllChildren(element.parentNode); + expectedEndpoint = [sel.getRangeAt(0).endContainer, + sel.getRangeAt(0).endOffset]; + } + + function checkCollapse() { + var desc = " after cmd_copyAndCollapseToEnd for " + + element.localName; + if (element.localName == "textarea") { + is(element.selectionStart, expectedEndpoint, "start offset" + desc); + is(element.selectionEnd, expectedEndpoint, "end offset" + desc); + } else { + is(sel.isCollapsed, true, "collapsed" + desc); + is(sel.anchorNode, expectedEndpoint[0], "node" + desc); + is(sel.anchorOffset, expectedEndpoint[1], "offset" + desc); + } + + nextTest(); + } + + SimpleTest.waitForClipboard(compareText, + () => SpecialPowers.doCommand(window, "cmd_copyAndCollapseToEnd"), + checkCollapse, checkCollapse); + } + + function testDiv() + { + var content = document.getElementById("content"); + runCopyCommand(content, 'abc', testTextarea); + } + + function testTextarea() + { + var textarea = document.getElementById("textarea"); + runCopyCommand(textarea, 'def', SimpleTest.finish); + } + + function runTests() + { + testDiv(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1101392">Mozilla Bug 1101392</a> +<div><div id="content">abc</div></div> + +<textarea id="textarea">def</textarea> + +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1102906.html b/editor/libeditor/tests/test_bug1102906.html new file mode 100644 index 000000000..36bfc8600 --- /dev/null +++ b/editor/libeditor/tests/test_bug1102906.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1102906 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1102906</title> + + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + + <script> + "use strict"; + + /* Test for Bug 1102906 */ + /* The caret should be movable by using keyboard after drag-and-drop. */ + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus( () => { + let content = document.getElementById("content"); + let drag = document.getElementById("drag") + let selection = window.getSelection(); + + /* Perform drag-and-drop for an arbitrary content. The caret should be at + the end of the contenteditable. */ + selection.selectAllChildren(drag); + synthesizeDrop(drag, content, {}, "copy"); + + let textContentAfterDrop = content.textContent; + + /* Move the caret to the front of the contenteditable by using keyboard. */ + for (let i = 0; i < content.textContent.length; ++i) { + sendKey("LEFT"); + } + sendChar("!"); + + is(content.textContent, "!" + textContentAfterDrop, + "The exclamation mark should be inserted at the front."); + + SimpleTest.finish(); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1102906">Mozilla Bug 1102906</a> +<div id="content" contenteditable="true"><span id="drag">Drag</span></div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1109465.html b/editor/libeditor/tests/test_bug1109465.html new file mode 100644 index 000000000..dc31e9bf0 --- /dev/null +++ b/editor/libeditor/tests/test_bug1109465.html @@ -0,0 +1,69 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1109465 +--> +<head> + <title>Test for Bug 1109465</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <textarea></textarea> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1109465 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.focus(); + + // Type foo\nbar and place the caret at the end of the last line + synthesizeKey("f", {}); + synthesizeKey("o", {}); + synthesizeKey("o", {}); + synthesizeKey("VK_RETURN", {}); + synthesizeKey("b", {}); + synthesizeKey("a", {}); + synthesizeKey("r", {}); + synthesizeKey("VK_UP", {}); + is(t.selectionStart, 3, "Correct start of selection"); + is(t.selectionEnd, 3, "Correct end of selection"); + + // Compose an IME string + synthesizeComposition({ type: "compositionstart" }); + var composingString = "\u306B"; + synthesizeCompositionChange( + { "composition": + { "string": composingString, + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + synthesizeComposition({ type: "compositioncommitasis" }); + is(t.value, "foo\u306B\nbar", "Correct value after composition"); + + // Now undo to test that the transaction merger has correctly detected the + // IMETextTxn. + synthesizeKey("Z", {accelKey: true}); + is(t.value, "foo\nbar", "Correct value after undo"); + + SimpleTest.finish(); +}); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1140105.html b/editor/libeditor/tests/test_bug1140105.html new file mode 100644 index 000000000..21d003054 --- /dev/null +++ b/editor/libeditor/tests/test_bug1140105.html @@ -0,0 +1,71 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1140105 +--> +<head> + <title>Test for Bug 1140105</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> + +<div id="content" contenteditable><font face="Arial">1234567890</font></div> + +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1140105 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("content"); + div.focus(); + synthesizeMouseAtCenter(div, {}); + synthesizeKey("VK_LEFT", {}); + + var sel = window.getSelection(); + var selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be in text node"); + is(selRange.endOffset, 9, "offset should be 9"); + + var firstHas = {}; + var anyHas = {}; + var allHas = {}; + var editor = getEditor(); + + var atomService = SpecialPowers.Cc["@mozilla.org/atom-service;1"].getService(SpecialPowers.Ci.nsIAtomService); + var propAtom = atomService.getAtom("font"); + editor.getInlineProperty(propAtom, "face", "Arial", firstHas, anyHas, allHas); + is(firstHas.value, true, "Test for Arial: firstHas: true expected"); + is(anyHas.value, true, "Test for Arial: anyHas: true expected"); + is(allHas.value, true, "Test for Arial: allHas: true expected"); + editor.getInlineProperty(propAtom, "face", "Courier", firstHas, anyHas, allHas); + is(firstHas.value, false, "Test for Courier: firstHas: false expected"); + is(anyHas.value, false, "Test for Courier: anyHas: false expected"); + is(allHas.value, false, "Test for Courier: allHas: false expected"); + + SimpleTest.finish(); + +}); + +function getEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + var editor = editingSession.getEditorForWindow(window); + editor.QueryInterface(Ci.nsIHTMLEditor); + return editor; +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1140617.html b/editor/libeditor/tests/test_bug1140617.html new file mode 100644 index 000000000..078458a3a --- /dev/null +++ b/editor/libeditor/tests/test_bug1140617.html @@ -0,0 +1,37 @@ +<!doctype html> +<title>Mozilla Bug 1140617</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1140617" + target="_blank">Mozilla Bug 1140617</a> +<iframe id="i1" width="200" height="100" src="about:blank"></iframe> +<img id="i" src="green.png"> +<script> +function runTest() { + SpecialPowers.setCommandNode(window, document.getElementById("i")); + SpecialPowers.doCommand(window, "cmd_copyImageContents"); + + var e = document.getElementById('i1'); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(doc.body); + selection.collapseToEnd(); + + doc.execCommand("fontname", false, "Arial"); + doc.execCommand("bold", false, null); + doc.execCommand("insertText", false, "12345"); + doc.execCommand("paste", false, null); + doc.execCommand("insertText", false, "a"); + + is(doc.queryCommandValue("fontname"), "Arial", "Arial expected"); + is(doc.queryCommandState("bold"), true, "Bold expected"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> diff --git a/editor/libeditor/tests/test_bug1153237.html b/editor/libeditor/tests/test_bug1153237.html new file mode 100644 index 000000000..38d631326 --- /dev/null +++ b/editor/libeditor/tests/test_bug1153237.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1153237 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1153237</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + // Avoid platform selection differences + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({ + "set": [["layout.word_select.eat_space_to_next_word", true]] + }, runTests); + }); + + function runTests() + { + var element = document.getElementById("editor"); + var sel = window.getSelection(); + + element.focus(); + is(sel.getRangeAt(0).startOffset, 0, "offset is zero"); + + SpecialPowers.doCommand(window, "cmd_selectRight2"); + is(sel.toString(), "Some ", + "first word + space is selected: got '" + sel.toString() + "'"); + + SpecialPowers.doCommand(window, "cmd_selectRight2"); + is(sel.toString(), "Some text", + "both words are selected: got '" + sel.toString() + "'"); + + SimpleTest.finish(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1153237">Mozilla Bug 1153237</a> +<div id="editor" contenteditable>Some text</div><span></span> + +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1154791.html b/editor/libeditor/tests/test_bug1154791.html new file mode 100644 index 000000000..03b605e20 --- /dev/null +++ b/editor/libeditor/tests/test_bug1154791.html @@ -0,0 +1,67 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1154791 +--> +<head> + <title>Test for Bug 1154791</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> + +<div id="content" contenteditable> +<tt>thiss onee is stilll a</tt> +</div> + +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1154791 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("content"); + div.focus(); + synthesizeMouseAtCenter(div, {}); + synthesizeKey("VK_LEFT", {}); + synthesizeKey("VK_LEFT", {}); + + setTimeout(function() { + synthesizeKey("VK_BACK_SPACE", {}); + setTimeout(function() { + synthesizeKey(" ", {}); + + setTimeout(function() { + var sel = getSpellCheckSelection(); + is(sel.rangeCount, 2, "We should have two misspelled words"); + is(String(sel.getRangeAt(0)), "thiss", "Correct misspelled word"); + is(String(sel.getRangeAt(1)), "onee", "Correct misspelled word"); + + SimpleTest.finish(); + },0); + },0); + },0); + +}); + +function getSpellCheckSelection() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + var editor = editingSession.getEditorForWindow(window); + var selcon = editor.selectionController; + return selcon.getSelection(selcon.SELECTION_SPELLCHECK); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1162952.html b/editor/libeditor/tests/test_bug1162952.html new file mode 100644 index 000000000..ad119de87 --- /dev/null +++ b/editor/libeditor/tests/test_bug1162952.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1162952 +--> +<head> + <title>Test for Bug 1162952</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1162952">Mozilla Bug 1162952</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1162952 **/ +var userCallbackRun = false; + +document.addEventListener('keydown', function() { + // During a user callback, the commands should be enabled + userCallbackRun = true; + is(true, document.queryCommandEnabled('cut')); + is(true, document.queryCommandEnabled('copy')); +}); + +// Otherwise, they should be disabled +is(false, document.queryCommandEnabled('cut')); +is(false, document.queryCommandEnabled('copy')); + +// Fire a user callback +synthesizeKey('A', {}); + +ok(userCallbackRun, "User callback should've been run"); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1181130-1.html b/editor/libeditor/tests/test_bug1181130-1.html new file mode 100644 index 000000000..eb27526a3 --- /dev/null +++ b/editor/libeditor/tests/test_bug1181130-1.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1181130 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1181130</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181130">Mozilla Bug 1181130</a> +<p id="display"></p> +<div id="container" contenteditable="true"> + editable div + <div id="noneditable" contenteditable="false"> + non-editable div + <div id="editable" contenteditable="true">nested editable div</div> + </div> +</div> +<script type="application/javascript"> +/** Test for Bug 1181130 **/ +var container = document.getElementById("container"); +var noneditable = document.getElementById("noneditable"); +var editable = document.getElementById("editable"); + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + synthesizeMouseAtCenter(noneditable, {}); + ok(!document.getSelection().toString().includes("nested editable div"), + "Selection should not include non-editable content"); + + synthesizeMouseAtCenter(container, {}); + ok(!document.getSelection().toString().includes("nested editable div"), + "Selection should not include non-editable content"); + + synthesizeMouseAtCenter(editable, {}); + ok(!document.getSelection().toString().includes("nested editable div"), + "Selection should not include non-editable content"); + + SimpleTest.finish(); +}); +</script> +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1181130-2.html b/editor/libeditor/tests/test_bug1181130-2.html new file mode 100644 index 000000000..edb380e98 --- /dev/null +++ b/editor/libeditor/tests/test_bug1181130-2.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1181130 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1181130</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181130">Mozilla Bug 1181130</a> +<p id="display"></p> +<div id="container" contenteditable="true"> + editable div + <div id="noneditable" contenteditable="false"> + non-editable div + </div> +</div> +<script type="application/javascript"> +/** Test for Bug 1181130 **/ +var container = document.getElementById("container"); +var noneditable = document.getElementById("noneditable"); + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var nonHTMLElement = document.createElementNS("http://www.example.com", "element"); + nonHTMLElement.innerHTML = '<div contenteditable="true">nested editable div</div>'; + noneditable.appendChild(nonHTMLElement); + + synthesizeMouseAtCenter(noneditable, {}); + ok(!document.getSelection().toString().includes("nested editable div"), + "Selection should not include non-editable content"); + + SimpleTest.finish(); +}); +</script> +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1186799.html b/editor/libeditor/tests/test_bug1186799.html new file mode 100644 index 000000000..b0b583a7e --- /dev/null +++ b/editor/libeditor/tests/test_bug1186799.html @@ -0,0 +1,81 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1186799 +--> +<head> + <title>Test for Bug 1186799</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1186799">Mozilla Bug 1186799</a> +<p id="display"></p> +<div id="content"> + <span id="span">span</span> + <div id="editor" contenteditable></div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1186799 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var span = document.getElementById("span"); + var editor = document.getElementById("editor"); + editor.focus(); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + ok(isThereIMESelection(), "There should be IME selection"); + + var compositionEnd = false; + editor.addEventListener("compositionend", function () { compositionEnd = true; }, true); + + synthesizeMouseAtCenter(span, {}); + + ok(compositionEnd, "composition end should be fired at clicking outside of the editor"); + ok(!isThereIMESelection(), "There should be no IME selection"); + + SimpleTest.finish(); +}); + +function isThereIMESelection() +{ + var selCon = SpecialPowers.wrap(window). + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsIWebNavigation). + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsIEditingSession). + getEditorForWindow(window). + selectionController; + const kIMESelections = [ + SpecialPowers.Ci.nsISelectionController.SELECTION_IME_RAWINPUT, + SpecialPowers.Ci.nsISelectionController.SELECTION_IME_SELECTEDRAWTEXT, + SpecialPowers.Ci.nsISelectionController.SELECTION_IME_CONVERTEDTEXT, + SpecialPowers.Ci.nsISelectionController.SELECTION_IME_SELECTEDCONVERTEDTEXT + ]; + for (var i = 0; i < kIMESelections.length; i++) { + var sel = selCon.getSelection(kIMESelections[i]); + if (sel && sel.rangeCount) { + return true; + } + } + return false; +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1230473.html b/editor/libeditor/tests/test_bug1230473.html new file mode 100644 index 000000000..bff7826d1 --- /dev/null +++ b/editor/libeditor/tests/test_bug1230473.html @@ -0,0 +1,124 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1230473 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1230473</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1230473">Mozilla Bug 1230473</a> +<input id="input"> +<textarea id="textarea"></textarea> +<div id="div" contenteditable></div> +<script type="application/javascript"> +/** Test for Bug 1230473 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(()=>{ + function runTest(aEditor) { + function committer() { + aEditor.blur(); + aEditor.focus(); + } + function isNSEditableElement() { + return aEditor.tagName.toLowerCase() == "input" || aEditor.tagName.toLowerCase() == "textarea"; + } + function value() { + return isNSEditableElement() ? aEditor.value : aEditor.textContent; + } + function isComposing() { + return isNSEditableElement() ? SpecialPowers.wrap(aEditor) + .QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement) + .editor + .QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport) + .composing : + SpecialPowers.wrap(window) + .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsIWebNavigation) + .QueryInterface(SpecialPowers.Ci.nsIDocShell) + .editor + .QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport) + .composing; + } + function clear() { + if (isNSEditableElement()) { + aEditor.value = ""; + } else { + aEditor.textContent = ""; + } + } + + clear(); + + // Committing at compositionstart + aEditor.focus(); + aEditor.addEventListener("compositionstart", committer, true); + synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 1, length: 0 }}); + aEditor.removeEventListener("compositionstart", committer, true); + ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionstart event handler"); + is(value(), "", "composition in " + aEditor.id + " shouldn't insert any text since it's committed at compositionstart"); + clear(); + + // Committing at first compositionupdate + aEditor.focus(); + aEditor.addEventListener("compositionupdate", committer, true); + synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 1, length: 0 }}); + aEditor.removeEventListener("compositionupdate", committer, true); + ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionupdate event handler"); + is(value(), "", "composition in " + aEditor.id + " shouldn't have inserted any text since it's committed at first compositionupdate"); + clear(); + + // Committing at first text (eCompositionChange) + aEditor.focus(); + aEditor.addEventListener("text", committer, true); + synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 1, length: 0 }}); + aEditor.removeEventListener("text", committer, true); + ok(!isComposing(), "composition in " + aEditor.id + " should be committed by text event handler"); + is(value(), "", "composition in " + aEditor.id + " should have inserted any text since it's committed at first text"); + clear(); + + // Committing at second compositionupdate + aEditor.focus(); + synthesizeComposition({ type: "compositionstart" }); + synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 1, length: 0 }}); + ok(isComposing(), "composition should be in " + aEditor.id + " before dispatching second compositionupdate"); + is(value(), "a", "composition in " + aEditor.id + " should be 'a' before dispatching second compositionupdate"); + aEditor.addEventListener("compositionupdate", committer, true); + synthesizeCompositionChange({ composition: { string: "ab", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 2, length: 0 }}); + aEditor.removeEventListener("compositionupdate", committer, true); + ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionupdate event handler"); + todo_is(value(), "a", "composition in " + aEditor.id + " shouldn't have been modified since it's committed at second compositionupdate"); + clear(); + + // Committing at second text (eCompositionChange) + aEditor.focus(); + synthesizeComposition({ type: "compositionstart" }); + synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 1, length: 0 }}); + ok(isComposing(), "composition should be in " + aEditor.id + " before dispatching second text"); + is(value(), "a", "composition in " + aEditor.id + " should be 'a' before dispatching second text"); + aEditor.addEventListener("text", committer, true); + synthesizeCompositionChange({ composition: { string: "ab", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 2, length: 0 }}); + aEditor.removeEventListener("text", committer, true); + ok(!isComposing(), "composition in " + aEditor.id + " should be committed by text event handler"); + todo_is(value(), "a", "composition in " + aEditor.id + " shouldn't have been modified since it's committed at second text"); + clear(); + } + runTest(document.getElementById("input")); + runTest(document.getElementById("textarea")); + runTest(document.getElementById("div")); + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1247483.html b/editor/libeditor/tests/test_bug1247483.html new file mode 100644 index 000000000..40dbc36ce --- /dev/null +++ b/editor/libeditor/tests/test_bug1247483.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 1247483</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + +function runTest() { + // Copy content from table. + var selection = getSelection(); + var startRange = document.createRange(); + startRange.setStart(document.getElementById("start"), 0); + startRange.setEnd(document.getElementById("end"), 2); + selection.removeAllRanges(); + selection.addRange(startRange); + SpecialPowers.wrap(document).execCommand("copy", false, null); + + // Paste content into "pastecontainer" + var pasteContainer = document.getElementById("pastecontainer"); + var pasteRange = document.createRange(); + pasteRange.selectNodeContents(pasteContainer); + pasteRange.collapse(false); + selection.removeAllRanges(); + selection.addRange(pasteRange); + SpecialPowers.wrap(document).execCommand("paste", false, null); + + is(pasteContainer.querySelectorAll("td").length, 4, "4 <td> should be pasted."); + + document.execCommand("undo", false, null); + + is(pasteContainer.querySelectorAll("td").length, 0, "Undo should have remove the 4 pasted <td>."); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1247483">Mozilla Bug 1247483</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<div id="container" contenteditable="true"> +<table> + <tr id="start"><td>1 1</td><td>1 2</td></tr> + <tr id="end"><td>2 1</td><td>2 2</td></tr> +</table> +</div> + +<div id="pastecontainer" contenteditable="true"> +</div> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1248128.html b/editor/libeditor/tests/test_bug1248128.html new file mode 100644 index 000000000..08b0139c9 --- /dev/null +++ b/editor/libeditor/tests/test_bug1248128.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1248128 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1248128</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(function() { + var outer = document.querySelector("html"); + ok(outer.scrollTop == 0, "scrollTop is zero: got " + outer.scrollTop); + + var input = document.getElementById("testInput"); + input.focus(); + + var scroll = outer.scrollTop; + ok(scroll > 0, "element has scrolled: new value " + scroll); + + try { + SpecialPowers.doCommand(window, "cmd_moveLeft"); + ok(false, "should not be able to do kMoveLeft"); + } catch (e) { + ok(true, "unable to perform kMoveLeft"); + } + + ok(outer.scrollTop == scroll, + "scroll is unchanged: got " + outer.scrollTop + ", expected " + scroll); + + // Make sure cmd_moveLeft isn't failing for some unrelated reason + synthesizeKey("a", {}); + is(input.selectionStart, 1, "selectionStart after typing"); + SpecialPowers.doCommand(window, "cmd_moveLeft"); + is(input.selectionStart, 0, "selectionStart after move left"); + + SimpleTest.finish(); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248128">Mozilla Bug 1248128</a> +<div style="height: 2000px;"></div> +<input type="text" id="testInput"></input> +<div style="height: 200px;"></div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1248185.html b/editor/libeditor/tests/test_bug1248185.html new file mode 100644 index 000000000..d545cfc53 --- /dev/null +++ b/editor/libeditor/tests/test_bug1248185.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1248185 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1248185</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + // Avoid platform selection differences + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({ + "set": [["layout.word_select.eat_space_to_next_word", true]] + }, runTests); + }); + + function runTests() + { + var editor = document.querySelector("#test"); + editor.focus(); + + var sel = window.getSelection(); + + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_selectRight2"); + ok(sel.toString() == "three ", "expected 'three ' to be selected"); + + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_moveRight2"); + ok(sel.toString() == "", "expected empty selection"); + + SpecialPowers.doCommand(window, "cmd_selectLeft2"); + ok(sel.toString() == "five", "expected 'five' to be selected"); + + SimpleTest.finish(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248185">Mozilla Bug 1248185</a> +<body> +<div style="font: 12px monospace; width: 45ch;"> +<span contenteditable="" id="test">blablablablablablablablablablablablablabla one two three four five</span> +<div> +<span>foo</span> +</div> +</div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1250010.html b/editor/libeditor/tests/test_bug1250010.html new file mode 100644 index 000000000..d1e0154dc --- /dev/null +++ b/editor/libeditor/tests/test_bug1250010.html @@ -0,0 +1,93 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1250010 +--> +<head> + <title>Test for Bug 1250010</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> + +<div id="test1" contenteditable><p><b><font color="red">1234567890</font></b></p></div> +<div id="test2" contenteditable><p><tt>xyz</tt></p><p><tt><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAIAAABvrngfAAAAFklEQVQImWMwjWhCQwxECoW3oCHihAB0LyYv5/oAHwAAAABJRU5ErkJggg=="></tt></p></div> + +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +function getImageDataURI() +{ + return document.getElementsByTagName("img")[0].getAttribute("src"); +} + +/** Test for Bug 1250010 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + + // First test: Empty paragraph is split correctly. + var div = document.getElementById("test1"); + div.focus(); + synthesizeMouseAtCenter(div, {}); + + var sel = window.getSelection(); + var selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 10, "offset should be 10"); + + synthesizeKey("VK_RETURN", {}); + synthesizeKey("VK_RETURN", {}); + synthesizeKey("b", {}); + synthesizeKey("VK_UP", {}); + synthesizeKey("a", {}); + + is(div.innerHTML, "<p><b><font color=\"red\">1234567890</font></b></p>" + + "<p><b><font color=\"red\">a<br></font></b></p>" + + "<p><b><font color=\"red\">b<br></font></b></p>", + "unexpected HTML"); + + // Second test: Since we modified the code path that splits non-text nodes, + // test that this works, if the split node is not empty. + div = document.getElementById("test2"); + div.focus(); + synthesizeMouseAtCenter(div, {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 3, "offset should be 3"); + + // Move behind the image and press enter, insert an "A". + // That should insert a new empty paragraph with the "A" after what we have. + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_RETURN", {}); + synthesizeKey("A", {}); + + // The resulting HTML is sadly less than optimal: + // A <br> gets inserted after the image and the "A" is followed by an empty <tt></tt>. + var newHTML = div.innerHTML; + var expectedHTML; + // Existing part with additional <br> inserted. + expectedHTML = "<p><tt>xyz</tt></p><p><tt><img src=\"" + getImageDataURI() + "\"><br></tt></p>" + + // New part caused by pressing enter after the image and typing an "A". + "<p><tt>A</tt><br><tt></tt></p>"; + is(newHTML, expectedHTML, "unexpected HTML"); + + // In case the empty tag gets deleted some day, let them know that something improved. + expectedHTML = "<p><tt>xyz</tt></p><p><tt><img src=\"" + getImageDataURI() + "\"><br></tt></p>" + + "<p><tt>A</tt><br></p>"; + todo_is(newHTML, expectedHTML, "unexpected HTML"); + + SimpleTest.finish(); + +}); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1257363.html b/editor/libeditor/tests/test_bug1257363.html new file mode 100644 index 000000000..c1610494f --- /dev/null +++ b/editor/libeditor/tests/test_bug1257363.html @@ -0,0 +1,182 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1257363 +--> +<head> + <title>Test for Bug 1257363</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> + +<div id="backspaceCSS" contenteditable><p style="color:red;">12345</p>67</div> +<div id="backspace" contenteditable><p><font color="red">12345</font></p>67</div> +<div id="deleteCSS" contenteditable><p style="color:red;">x</p></div> +<div id="delete" contenteditable><p><font color="red">y</font></p></div> + +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1257363 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + + // ***** Backspace test ***** + var div = document.getElementById("backspaceCSS"); + div.focus(); + synthesizeMouse(div, 100, 2, {}); /* click behind and down */ + + var sel = window.getSelection(); + var selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 5, "offset should be 5"); + + // Return and backspace should take us to where we started. + synthesizeKey("VK_RETURN", {}); + synthesizeKey("VK_BACK_SPACE", {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 5, "offset should be 5"); + + // Add an "a" to the end of the paragraph. + synthesizeKey("a", {}); + + // Return and forward delete should take us to the following line. + synthesizeKey("VK_RETURN", {}); + synthesizeKey("VK_DELETE", {}); + + // Add a "b" to the start. + synthesizeKey("b", {}); + + is(div.innerHTML, "<p style=\"color:red;\">12345a</p>b67", + "unexpected HTML"); + + // Let's repeat the whole thing, but a font tag instead of CSS. + // The behaviour is different since the font is carried over. + div = document.getElementById("backspace"); + div.focus(); + synthesizeMouse(div, 100, 2, {}); /* click behind and down */ + + sel = window.getSelection(); + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 5, "offset should be 5"); + + // Return and backspace should take us to where we started. + synthesizeKey("VK_RETURN", {}); + synthesizeKey("VK_BACK_SPACE", {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 5, "offset should be 5"); + + // Add an "a" to the end of the paragraph. + synthesizeKey("a", {}); + + // Return and forward delete should take us to the following line. + synthesizeKey("VK_RETURN", {}); + synthesizeKey("VK_DELETE", {}); + + // Add a "b" to the start. + synthesizeKey("b", {}); + + // Here we get a somewhat ugly result since the red sticks. + is(div.innerHTML, "<p><font color=\"red\">12345a</font></p><font color=\"red\">b</font>67", + "unexpected HTML"); + + // ***** Delete test ***** + div = document.getElementById("deleteCSS"); + div.focus(); + synthesizeMouse(div, 100, 2, {}); /* click behind and down */ + + var sel = window.getSelection(); + var selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 1, "offset should be 1"); + + // left, enter should create a new empty paragraph before + // but leave the selection at the start of the existing paragraph. + synthesizeKey("VK_LEFT", {}); + synthesizeKey("VK_RETURN", {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node"); + is(selRange.endOffset, 0, "offset should be 0"); + is(selRange.endContainer.nodeValue, "x", "we should be in the text node with the x"); + + // Now moving up into the new empty paragraph. + synthesizeKey("VK_UP", {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "P", "selection should be the new empty paragraph"); + is(selRange.endOffset, 0, "offset should be 0"); + + // Forward delete should now take us to where we started. + synthesizeKey("VK_DELETE", {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node"); + is(selRange.endOffset, 0, "offset should be 0"); + + // Add an "a" to the start of the paragraph. + synthesizeKey("a", {}); + + is(div.innerHTML, "<p style=\"color:red;\">ax</p>", + "unexpected HTML"); + + // Let's repeat the whole thing, but a font tag instead of CSS. + div = document.getElementById("delete"); + div.focus(); + synthesizeMouse(div, 100, 2, {}); /* click behind and down */ + + var sel = window.getSelection(); + var selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 1, "offset should be 1"); + + // left, enter should create a new empty paragraph before + // but leave the selection at the start of the existing paragraph. + synthesizeKey("VK_LEFT", {}); + synthesizeKey("VK_RETURN", {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node"); + is(selRange.endOffset, 0, "offset should be 0"); + is(selRange.endContainer.nodeValue, "y", "we should be in the text node with the y"); + + // Now moving up into the new empty paragraph. + synthesizeKey("VK_UP", {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "FONT", "selection should be the font tag"); + is(selRange.endOffset, 0, "offset should be 0"); + is(selRange.endContainer.parentNode.nodeName, "P", "the parent of the font should be a paragraph"); + + // Forward delete should now take us to where we started. + synthesizeKey("VK_DELETE", {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node"); + is(selRange.endOffset, 0, "offset should be 0"); + + // Add an "a" to the start of the paragraph. + synthesizeKey("a", {}); + + is(div.innerHTML, "<p><font color=\"red\">ay</font></p>", + "unexpected HTML"); + + SimpleTest.finish(); + +}); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1258085.html b/editor/libeditor/tests/test_bug1258085.html new file mode 100644 index 000000000..342f068ee --- /dev/null +++ b/editor/libeditor/tests/test_bug1258085.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<title>Test for Bug 1186799</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<div contenteditable></div> +<script> +var div = document.querySelector("div"); + +function reset() { + div.innerHTML = "x<br> y"; + div.focus(); + synthesizeKey("VK_DOWN", {}); +} + +function checks(msg) { + is(div.innerHTML, "x<br><br>", + msg + ": Should add a second <br> to prevent collapse of first"); + is(div.childNodes.length, 3, msg + ": No empty text nodes allowed"); + ok(getSelection().isCollapsed, msg + ": Selection must be collapsed"); + is(getSelection().focusNode, div, msg + ": Focus must be in div"); + is(getSelection().focusOffset, 2, + msg + ": Focus must be between the two <br>s"); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + // Put selection after the "y" and backspace + reset(); + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_BACK_SPACE", {}); + checks("Collapsed backspace"); + + // Now do the same with delete + reset(); + synthesizeKey("VK_DELETE", {}); + checks("Collapsed delete"); + + // Forward selection + reset(); + synthesizeKey("VK_RIGHT", {shiftKey: true}); + synthesizeKey("VK_BACK_SPACE", {}); + checks("Forward-selected backspace"); + + // Backward selection + reset(); + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_LEFT", {shiftKey: true}); + synthesizeKey("VK_BACK_SPACE", {}); + checks("Backward-selected backspace"); + + // Make sure we're not deleting if the whitespace isn't actually collapsed + div.style.whiteSpace = "pre-wrap"; + reset(); + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_BACK_SPACE", {}); + if (div.innerHTML, "x<br> ", "pre-wrap: Don't delete uncollapsed space"); + ok(getSelection().isCollapsed, "pre-wrap: Selection must be collapsed"); + is(getSelection().focusNode, div.lastChild, + "pre-wrap: Focus must be in final text node"); + is(getSelection().focusOffset, 1, "pre-wrap: Focus must be at end of node"); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug1268736.html b/editor/libeditor/tests/test_bug1268736.html new file mode 100644 index 000000000..fb1f341b5 --- /dev/null +++ b/editor/libeditor/tests/test_bug1268736.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1268736 +--> +<head> + <title>Test for Bug 1268736</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268736">Mozilla Bug 1268736</a> +<table id="table" border="1" width="100%"> + <tbody> + <tr> + <td>a</td> + <td>b</td> + <td>c</td> + </tr> + <tr> + <td>d</td> + <td id="cell_readonly">e</td> + <td contenteditable="true" id="cell_writable">f</td> + </tr> + </tbody> +</table> + +<script type="application/javascript"> + +/** + * Test for Bug 1268736 + * + * Tests for editing a table cell's contents when the table cell is or isn't a child of a contenteditable node. + * + */ + +function getEditor() { + const Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +var table = document.getElementById("table"); +var tableHTML = table.innerHTML; +var editor = getEditor(); + +var cell = document.getElementById("cell_readonly"); +cell.focus(); +editor.deleteTableCellContents(); +is(table.innerHTML == tableHTML, true, "editor should not modify non-editable table cell" ); + +cell = document.getElementById("cell_writable"); +cell.focus(); +editor.deleteTableCellContents(); +is(cell.innerHTML == "<br>", true, "editor can modify editable table cells" ); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1270235.html b/editor/libeditor/tests/test_bug1270235.html new file mode 100644 index 000000000..da7fd4e7a --- /dev/null +++ b/editor/libeditor/tests/test_bug1270235.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1270235 +--> +<head> + <title>Test for Bug 1270235</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1270235">Mozilla Bug 1270235</a> +<p id="display"></p> +<div id="content" style="display: none;"></div> + +<div id="edit1" contenteditable="true"><p>AB</p></div> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(()=>{ + let element = document.getElementById('edit1'); + element.focus(); + let textNode = element.firstChild.firstChild; + let node = textNode.splitText(0); + node.parentNode.removeChild(node); + + ok(!node.parentNode, 'parent must be null'); + + let newRange = document.createRange(); + newRange.setStart(node, 0); + newRange.setEnd(node, 0); + let selection = document.getSelection(); + selection.removeAllRanges(); + selection.addRange(newRange); + + ok(selection.isCollapsed, 'isCollapsed must be true'); + + // Don't crash by user input + synthesizeKey("X", {}); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1310912.html b/editor/libeditor/tests/test_bug1310912.html new file mode 100644 index 000000000..d73366a63 --- /dev/null +++ b/editor/libeditor/tests/test_bug1310912.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1310912 +--> +<html> +<head> + <title>Test for Bug 1310912</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1310912">Mozilla Bug 1310912</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="editable1" contenteditable="true">ABC</div> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let elm = document.getElementById("editable1"); + + elm.focus(); + let sel = window.getSelection(); + sel.collapse(elm.childNodes[0], elm.textContent.length); + + synthesizeCompositionChange({ + composition: { + string: "DEF", + clauses: [ + { length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + caret: { start: 3, length: 0 } + }); + ok(elm.textContent == "ABCDEF", "composing text should be set"); + + window.getSelection().getRangeAt(0).insertNode(document.createTextNode("")); + synthesizeCompositionChange({ + composition: { + string: "GHI", + clauses: [ + { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + caret: { start: 0, length: 0 } + }); + ok(elm.textContent == "ABCGHI", "composing text should be replaced"); + + window.getSelection().getRangeAt(0).insertNode(document.createTextNode("")); + synthesizeCompositionChange({ + composition: { + string: "JKL", + clauses: [ + { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + caret: { start: 0, length: 0 } + }); + ok(elm.textContent == "ABCJKL", "composing text should be replaced"); + + window.getSelection().getRangeAt(0).insertNode(document.createTextNode("")); + synthesizeCompositionChange({ + composition: { + string: "MNO", + clauses: [ + { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + caret: { start: 1, length: 0 } + }); + ok(elm.textContent == "ABCMNO", "composing text should be replaced"); + + window.getSelection().getRangeAt(0).insertNode(document.createTextNode("")); + synthesizeComposition({ type: "compositioncommitasis" }); + ok(elm.textContent == "ABCMNO", "composing text should be committed"); + + synthesizeKey("Z", { accelKey: true }); + ok(elm.textContent == "ABC", "text should be undoed"); + + synthesizeKey("Z", { accelKey: true, shiftKey: true }); + ok(elm.textContent == "ABCMNO", "text should be redoed"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1314790.html b/editor/libeditor/tests/test_bug1314790.html new file mode 100644 index 000000000..ff1487244 --- /dev/null +++ b/editor/libeditor/tests/test_bug1314790.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1314790 +--> +<html> +<head> + <title>Test for Bug 1314790</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1314790">Mozilla Bug 1314790</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div contenteditable="true" id="contenteditable1"><p>pen pineapple</p></div> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let elm = document.getElementById("contenteditable1"); + elm.focus(); + window.getSelection().collapse(elm.childNodes[0], 0); + + SpecialPowers.doCommand(window, "cmd_wordNext"); + SpecialPowers.doCommand(window, "cmd_wordNext"); + + synthesizeKey("VK_RETURN", {}); + synthesizeKey("a", {}); + synthesizeKey("p", {}); + synthesizeKey("p", {}); + synthesizeKey("l", {}); + synthesizeKey("e", {}); + synthesizeKey(" ", {}); + synthesizeKey("p", {}); + synthesizeKey("e", {}); + synthesizeKey("n", {}); + + is(elm.childNodes[0].textContent, "pen pineapple", + "'pen pineapple' is first elment"); + is(elm.childNodes[1].textContent, "apple pen", + "'apple pen' is second elment"); + + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + is(elm.childNodes[0].textContent, "pen pineapple", + "'pen pineapple' is first elment"); + + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + is(elm.childNodes[0].textContent, "pen pineapple", + "'pen pineapple' is first elment"); + + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + is(elm.childNodes[0].textContent, "pen ", "'pen ' is first elment"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1315065.html b/editor/libeditor/tests/test_bug1315065.html new file mode 100644 index 000000000..16f0de4e3 --- /dev/null +++ b/editor/libeditor/tests/test_bug1315065.html @@ -0,0 +1,145 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1315065 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1315065</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315065">Mozilla Bug 1315065</a> +<div contenteditable><p>abc<br></p></div> +<script type="application/javascript"> +/** Test for Bug 1315065 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(()=>{ + var editor = document.getElementsByTagName("div")[0]; + function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) { + editor.innerHTML = "<p id='p'>abc<br></p>"; + var p = document.getElementById("p"); + // FYI: We cannot inserting empty text nodes as expected with + // Node.appendChild() nor Node.insertBefore(). Therefore, let's use + // Range.insertNode() like actual web apps. + var selection = window.getSelection(); + selection.collapse(p, 1); + var range = selection.getRangeAt(0); + var emptyTextNode3 = document.createTextNode(""); + range.insertNode(emptyTextNode3); + var emptyTextNode2 = document.createTextNode(""); + range.insertNode(emptyTextNode2); + var emptyTextNode1 = document.createTextNode(""); + range.insertNode(emptyTextNode1); + is(p.childNodes.length, 5, "Failed to initialize the editor"); + is(p.childNodes.item(1), emptyTextNode1, "1st text node should be emptyTextNode1"); + is(p.childNodes.item(2), emptyTextNode2, "2nd text node should be emptyTextNode2"); + is(p.childNodes.item(3), emptyTextNode3, "3rd text node should be emptyTextNode3"); + switch (aSelectionCollapsedTo) { + case 0: + selection.collapse(p.firstChild, 3); // next to 'c' + break; + case 1: + selection.collapse(emptyTextNode1, 0); + break; + case 2: + selection.collapse(emptyTextNode2, 0); + break; + case 3: + selection.collapse(emptyTextNode3, 0); + break; + default: + ok(false, "aSelectionCollapsedTo is illegal value"); + } + } + + for (var i = 0; i < 4; i++) { + const kDescription = i == 0 ? "Backspace from immediately after the last character" : + "Backspace from " + i + "th empty text node"; + editor.focus(); + initForBackspace(i); + synthesizeKey("KEY_Backspace", { code: "Backspace" }); + var p = document.getElementById("p"); + ok(p, kDescription + ": <p> element shouldn't be removed by Backspace key press"); + is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Backspace key press"); + // When Backspace key is pressed even in empty text nodes, Gecko should not remove empty text nodes for now + // because we should keep our traditional behavior (same as Edge) for backward compatibility as far as possible. + // In this case, Chromium removes all empty text nodes, but Edge doesn't remove any empty text nodes. + is(p.childNodes.length, 5, kDescription + ": <p> should have 5 children after pressing Backspace key"); + is(p.childNodes.item(0).textContent, "ab", kDescription + ": 'c' should be removed by pressing Backspace key"); + is(p.childNodes.item(1).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Backspace key"); + is(p.childNodes.item(2).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Backspace key"); + is(p.childNodes.item(3).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Backspace key"); + editor.blur(); + } + + function initForDelete(aSelectionCollapsedTo /* = 0 ~ 3 */) { + editor.innerHTML = "<p id='p'>abc<br></p>"; + var p = document.getElementById("p"); + // FYI: We cannot inserting empty text nodes as expected with + // Node.appendChild() nor Node.insertBefore(). Therefore, let's use + // Range.insertNode() like actual web apps. + var selection = window.getSelection(); + selection.collapse(p, 0); + var range = selection.getRangeAt(0); + var emptyTextNode1 = document.createTextNode(""); + range.insertNode(emptyTextNode1); + var emptyTextNode2 = document.createTextNode(""); + range.insertNode(emptyTextNode2); + var emptyTextNode3 = document.createTextNode(""); + range.insertNode(emptyTextNode3); + is(p.childNodes.length, 5, "Failed to initialize the editor"); + is(p.childNodes.item(0), emptyTextNode3, "1st text node should be emptyTextNode3"); + is(p.childNodes.item(1), emptyTextNode2, "2nd text node should be emptyTextNode2"); + is(p.childNodes.item(2), emptyTextNode1, "3rd text node should be emptyTextNode1"); + switch (aSelectionCollapsedTo) { + case 0: + selection.collapse(p.childNodes.item(3), 0); // next to 'a' + break; + case 1: + selection.collapse(emptyTextNode1, 0); + break; + case 2: + selection.collapse(emptyTextNode2, 0); + break; + case 3: + selection.collapse(emptyTextNode3, 0); + break; + default: + ok(false, "aSelectionCollapsedTo is illegal value"); + } + } + + for (var i = 0; i < 4; i++) { + const kDescription = i == 0 ? "Delete from immediately before the first character" : + "Delete from " + i + "th empty text node"; + editor.focus(); + initForDelete(i); + synthesizeKey("KEY_Delete", { code: "Delete" }); + var p = document.getElementById("p"); + ok(p, kDescription + ": <p> element shouldn't be removed by Delete key press"); + is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Delete key press"); + if (i == 0) { + // If Delete key is pressed in non-empty text node, only the text node should be modified. + // This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case. + is(p.childNodes.length, 5, kDescription + ": <p> should have only 2 children after pressing Delete key (empty text nodes should be removed"); + is(p.childNodes.item(0).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Delete key"); + is(p.childNodes.item(1).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Delete key"); + is(p.childNodes.item(2).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Delete key"); + is(p.childNodes.item(3).textContent, "bc", kDescription + ": 'a' should be removed by pressing Delete key"); + } else { + // If Delete key is pressed in an empty text node, it and following empty text nodes should be removed and the non-empty text node should be modified. + // This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case. + var expectedEmptyTextNodes = 3 - i; + is(p.childNodes.length, expectedEmptyTextNodes + 2, kDescription + ": <p> should have only " + i + " children after pressing Delete key (" + i + " empty text nodes should be removed"); + is(p.childNodes.item(expectedEmptyTextNodes).textContent, "bc", kDescription + ": empty text nodes and 'a' should be removed by pressing Delete key"); + } + editor.blur(); + } + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1328023.html b/editor/libeditor/tests/test_bug1328023.html new file mode 100644 index 000000000..1b7fb7bf5 --- /dev/null +++ b/editor/libeditor/tests/test_bug1328023.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1328023 +--> +<html> +<head> + <title>Test for Bug 1328023</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1328023">Mozilla Bug 1328023</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<input type="text" id="input1"/> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let elm = document.getElementById("input1"); + + elm.focus(); + synthesizeKey("A", {}); + synthesizeKey("B", {}); + is(elm.value, "AB", "AB is input.value now"); + + synthesizeKey("VK_BACK_SPACE", {}); + is(elm.value, "A", "A is input.value now"); + + synthesizeKey("Z", { accelKey: true }); + is(elm.value, "AB", "AB is input.value now"); + + synthesizeKey("C", {}); + is(elm.value, "ABC", "ABC is input.value now"); + + synthesizeKey("VK_BACK_SPACE", {}); + synthesizeKey("VK_BACK_SPACE", {}); + synthesizeKey("VK_BACK_SPACE", {}); + + synthesizeKey("A", {}); + synthesizeKey("B", {}); + synthesizeKey("C", {}); + is(elm.value, "ABC", "ABC is input.value now"); + + synthesizeKey("Z", { accelKey: true }); + is(elm.value, "", "'' is input.value now"); + + synthesizeKey("Z", { accelKey: true, shiftKey: true }); + is(elm.value, "ABC", "ABC is input.value now"); + + synthesizeKey("D", {}); + is(elm.value, "ABCD", "ABCD is input.value now"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1330796.html b/editor/libeditor/tests/test_bug1330796.html new file mode 100644 index 000000000..f8af02087 --- /dev/null +++ b/editor/libeditor/tests/test_bug1330796.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1330796 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 772796</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> .pre { white-space: pre } </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 1330796</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="editable" contenteditable></div> + +<pre id="test"> + +<script type="application/javascript"> +// We want to test what happens when the user splits a mail cite by clicking +// at the start, the middle and the end of the cite and hitting the enter key. +// Mail cites are spans, and since bug 1288911 they are displayed as blocks. +// The _moz_quote attribute is used to give the cite a blue color via CSS. +// As an internal attribute, it's not returned from the innerHTML. +// To the user the tests look like: +// > mailcite +// This text is 10 characters long, so we position at 0, 5 and 10. +// Althought since bug 1288911 those cites are displayed as block, +// the tests are repeated also for inline display. +// Each entry of the 'tests' array has the original HTML, the offset to click +// at and the expected result HTML. +var tests = [ + // With style="display: block;". + [ "<span _moz_quote=true style=\"display: block;\">> mailcite<br></span>", 0, + "x<br><span style=\"display: block;\">> mailcite<br></span>" ], + [ "<span _moz_quote=true style=\"display: block;\">> mailcite<br></span>", 5, + "<span style=\"display: block;\">> mai<br></span>x<br><span style=\"display: block;\">lcite<br></span>"], + [ "<span _moz_quote=true style=\"display: block;\">> mailcite<br></span>", 10, + "<span style=\"display: block;\">> mailcite<br></span>x<br>" ], + // No <br> at the end to simulate prior deletion to the end of the quote. + [ "<span _moz_quote=true style=\"display: block;\">> mailcite</span>", 10, + "<span style=\"display: block;\">> mailcite<br></span>x<br>" ], + + // Without style="display: block;". + [ "<span _moz_quote=true>> mailcite<br></span>", 0, + "x<br><span>> mailcite<br></span>" ], + [ "<span _moz_quote=true>> mailcite<br></span>", 5, + "<span>> mai</span><br>x<br><span>lcite<br></span>" ], + [ "<span _moz_quote=true>> mailcite<br></span>", 10, + "<span>> mailcite<br></span>x<br>" ], + // No <br> at the end to simulate prior deletion to the end of the quote. + [ "<span _moz_quote=true>> mailcite</span>", 10, + "<span>> mailcite</span><br>x<br>" ] +]; + +/** Test for Bug 1330796 **/ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + + var sel = window.getSelection(); + var theEdit = document.getElementById("editable"); + makeMailEditor(); + + for (i = 0; i < tests.length; i++) { + theEdit.innerHTML = tests[i][0]; + theEdit.focus(); + var theText = theEdit.firstChild.firstChild; + // Position set at the beginning , middle and end of the text. + sel.collapse(theText, tests[i][1]); + + synthesizeKey("KEY_Enter", { code: "Enter" }); + synthesizeKey("x", { code: "KeyX" }); + is(theEdit.innerHTML, tests[i][2], "unexpected HTML for test " + i.toString()); + } + + SimpleTest.finish(); + +}); + +function makeMailEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + var editor = editingSession.getEditorForWindow(window); + editor.flags |= Ci.nsIPlaintextEditor.eEditorMailMask; +} +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1332876.html b/editor/libeditor/tests/test_bug1332876.html new file mode 100644 index 000000000..76dfa0fc7 --- /dev/null +++ b/editor/libeditor/tests/test_bug1332876.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1332876</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1332876">Mozilla Bug 1332876</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<iframe src="data:text/html,<html><body><span>Edit me!</span>"></iframe> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 1332876 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var iframe = document.querySelector("iframe"); + iframe.contentDocument.designMode='on'; + + iframe.contentWindow.addEventListener('keypress', function() { + iframe.style.display='none'; + document.body.offsetHeight; + ok(true, "did not crash"); + SimpleTest.finish(); + }); + + iframe.contentWindow.addEventListener('click', function() { + synthesizeKey('a', {}, iframe.contentWindow); + }); + + synthesizeMouse(iframe,20,20,{}) +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug200416.html b/editor/libeditor/tests/test_bug200416.html new file mode 100644 index 000000000..9fb656425 --- /dev/null +++ b/editor/libeditor/tests/test_bug200416.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=200416 +--> +<title>Test for Bug 200416</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=200416">Mozilla Bug 200416</a> +<div contenteditable><span>foo<p>bar</p></span></div> +<script> +getSelection().collapse(document.querySelector("p").firstChild, 0); +document.execCommand("delete"); +var innerHTML = document.querySelector("div").innerHTML; +ok(/foo.*bar/.test(innerHTML), "foo needs to still come before bar"); +</script> diff --git a/editor/libeditor/tests/test_bug289384.html b/editor/libeditor/tests/test_bug289384.html new file mode 100644 index 000000000..1d55e0c3f --- /dev/null +++ b/editor/libeditor/tests/test_bug289384.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=289384 +--> +<head> + <title>Test for Bug 289384</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=289384">Mozilla Bug 289384</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var win = window.open("data:text/html,<a href=\"data:text/html,<body contenteditable onload='opener.continueTest(window);'>foo bar</body>\">link</a>", "", "test-289384"); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + win.document.querySelector("a").click(); + }, false); +}); + +function continueTest(win) { + SimpleTest.waitForFocus(function() { + var doc = win.document; + var sel = win.getSelection(); + doc.body.focus(); + sel.collapse(doc.body.firstChild, 3); + SimpleTest.executeSoon(function() { + synthesizeKey("VK_LEFT", {accelKey: true}, win); + ok(sel.isCollapsed, "The selection must be collapsed"); + is(sel.anchorNode, doc.body.firstChild, "The anchor node should be the body element's text node"); + is(sel.anchorOffset, 0, "The anchor offset should be 0"); + win.close(); + SimpleTest.finish(); + }); + }, win); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug290026.html b/editor/libeditor/tests/test_bug290026.html new file mode 100644 index 000000000..9e7686e72 --- /dev/null +++ b/editor/libeditor/tests/test_bug290026.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=290026 +--> +<head> + <title>Test for Bug 290026</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=290026">Mozilla Bug 290026</a> +<p id="display"></p> +<div id="editor" contenteditable></div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 290026 **/ +SimpleTest.waitForExplicitFinish(); + +var editor = document.getElementById("editor"); +editor.innerHTML = '<p></p><ul><li>Item 1</li><li>Item 2</li></ul><p></p>'; +editor.focus(); + +addLoadEvent(function() { + document.execCommand("stylewithcss", false, "true"); + var sel = window.getSelection(); + sel.removeAllRanges(); + var lis = document.getElementsByTagName("li"); + var range = document.createRange(); + range.setStart(lis[0], 0); + range.setEnd(lis[1], lis[1].childNodes.length); + sel.addRange(range); + document.execCommand("indent", false, false); + var oneindent = '<p></p><ul style="margin-left: 40px;"><li>Item 1</li><li>Item 2</li></ul><p></p>'; + is(editor.innerHTML, oneindent, "a once indented bulleted list"); + document.execCommand("indent", false, false); + var twoindent = '<p></p><ul style="margin-left: 80px;"><li>Item 1</li><li>Item 2</li></ul><p></p>'; + is(editor.innerHTML, twoindent, "a twice indented bulleted list"); + document.execCommand("outdent", false, false); + is(editor.innerHTML, oneindent, "outdenting a twice indented bulleted list"); + + // done + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug291780.html b/editor/libeditor/tests/test_bug291780.html new file mode 100644 index 000000000..93f63af61 --- /dev/null +++ b/editor/libeditor/tests/test_bug291780.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=291780 +--> +<head> + <title>Test for Bug 291780</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=291780">Mozilla Bug 291780</a> +<p id="display"></p> +<div id="editor" contenteditable></div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 291780 **/ +SimpleTest.waitForExplicitFinish(); + +var original = '<ul style="margin-left: 40px;"><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li></ul>'; +var editor = document.getElementById("editor"); +editor.innerHTML = original; +editor.focus(); + +addLoadEvent(function() { + + var sel = window.getSelection(); + sel.removeAllRanges(); + var lis = document.getElementsByTagName("li"); + var range = document.createRange(); + range.setStart(lis[1], 0); + range.setEnd(lis[2], lis[2].childNodes.length); + sel.addRange(range); + document.execCommand("indent", false, false); + var expected = '<ul style="margin-left: 40px;"><li>Item 1</li><ul><li>Item 2</li><li>Item 3</li></ul><li>Item 4</li></ul>'; + is(editor.innerHTML, expected, "indenting part of an already indented bulleted list"); + document.execCommand("outdent", false, false); + is(editor.innerHTML, original, "outdenting the partially indented part of an already indented bulleted list"); + + // done + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug309731.html b/editor/libeditor/tests/test_bug309731.html new file mode 100644 index 000000000..85406905c --- /dev/null +++ b/editor/libeditor/tests/test_bug309731.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=309731 +--> +<head> + <title>Test for Bug 309731</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=309731">Mozilla Bug 309731</a> +<p id="display"></p> + +<div id="content"> + <div id="input" contentEditable="true"></div> +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 309731 **/ + +function selectNode(node) { + getSelection().selectAllChildren(node); +} + +function selectInNode(node) { + getSelection().collapse(node, 0); +} + +function doTest() { + var input = document.getElementById("input"); + + is(input.textContent, "", "Input node starts empty"); + + selectInNode(input); + ok(document.execCommand("inserthtml", false, ""), "execCommand should return true"); + is(input.textContent, "", "empty inserthtml with empty selection shouldn't change contents"); + + selectInNode(input); + ok(document.execCommand("inserthtml", false, "foo"), "execCommand should return true"); + is(input.textContent, "foo", "'foo'inserthtml with empty selection should add foo to contents"); + + selectNode(input); + ok(document.execCommand("inserthtml", false, "bar"), "execCommand should return true"); + is(input.textContent, "bar", "'bar' inserthtml with complete selection should replace contents with bar"); + + selectNode(input); + ok(document.execCommand("inserthtml", false, ""), "execCommand should return true"); + is(input.textContent, "", "empty inserthtml with complete selection should delete everything"); +} + +doTest(); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug316447.html b/editor/libeditor/tests/test_bug316447.html new file mode 100644 index 000000000..76d123815 --- /dev/null +++ b/editor/libeditor/tests/test_bug316447.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=316447 +--> +<title>Test for Bug 316447</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=316447">Mozilla Bug 316447</a> +<div contenteditable><br></div> +<script> +/** Test for Bug 316447 **/ + +getSelection().selectAllChildren(document.querySelector("div")); +document.execCommand("inserthorizontalrule"); +is(document.querySelector("div").innerHTML, "<hr>", "Wrong innerHTML"); +</script> diff --git a/editor/libeditor/tests/test_bug318065.html b/editor/libeditor/tests/test_bug318065.html new file mode 100644 index 000000000..541653ab1 --- /dev/null +++ b/editor/libeditor/tests/test_bug318065.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=318065 +--> + +<head> + <title>Test for Bug 318065</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=318065">Mozilla Bug 318065</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 318065 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var expectedValues = ["A", "", "A", "", "A", "", "A"]; + var messages = ["Initial text inserted", + "Initial text deleted", + "Undo of deletion", + "Redo of deletion", + "Initial text typed", + "Undo of typing", + "Redo of typing"]; + var step = 0; + + function onInput() { + is(this.value, expectedValues[step], messages[step]); + step++; + if (step == expectedValues.length) { + this.removeEventListener("input", onInput, false); + SimpleTest.finish(); + } + } + + var input = document.getElementById("t1"); + input.addEventListener("input", onInput, false); + var input2 = document.getElementById("t2"); + input2.addEventListener("input", onInput, false); + + input.focus(); + + // Tests 0 + 1: Input letter and delete it again + synthesizeKey("A", {}); + synthesizeKey("VK_BACK_SPACE", {}); + + // Test 2: Undo deletion. Value of input should be "A" + synthesizeKey("Z", {accelKey: true}); + + // Test 3: Redo deletion. Value of input should be "" + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + input2.focus(); + + // Test 4: Input letter + synthesizeKey("A", {}); + + // Test 5: Undo typing. Value of input should be "" + synthesizeKey("Z", {accelKey: true}); + + // Test 6: Redo typing. Value of input should be "A" + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + }); + </script> + </pre> + + <input type="text" value="" id="t1" /> + <input type="text" value="" id="t2" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug332636.html b/editor/libeditor/tests/test_bug332636.html new file mode 100644 index 000000000..5df386ac4 --- /dev/null +++ b/editor/libeditor/tests/test_bug332636.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=332636 +--> +<head> + <title>Test for Bug 332636</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=332636">Mozilla Bug 332636</a> +<p id="display"></p> +<div id="content"> + <div id="edit0" contenteditable="true">axb</div><!-- reference: plane 0 base character --> + <div id="edit1" contenteditable="true">äb</div><!-- reference: plane 0 diacritic --> + <div id="edit2" contenteditable="true">a𐐀b</div><!-- plane 1 base character --> + <div id="edit3" contenteditable="true">a𐨏b</div><!-- plane 1 diacritic --> + + <div id="edit0b" contenteditable="true">axb</div><!-- reference: plane 0 base character --> + <div id="edit1b" contenteditable="true">äb</div><!-- reference: plane 0 diacritic --> + <div id="edit2b" contenteditable="true">a𐐀b</div><!-- plane 1 base character --> + <div id="edit3b" contenteditable="true">a𐨏b</div><!-- plane 1 diacritic --> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 332636 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +function test(edit) { + edit.focus(); + var sel = window.getSelection(); + sel.collapse(edit.childNodes[0], edit.textContent.length - 1); + synthesizeKey("VK_BACK_SPACE", {}); + is(edit.textContent, "ab", "The backspace key should delete the UTF-16 surrogate pair correctly"); +} + +function testWithMove(edit, offset) { + edit.focus(); + var sel = window.getSelection(); + sel.collapse(edit.childNodes[0], 0); + var i; + for (i = 0; i < offset; ++i) { + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_LEFT", {}); + synthesizeKey("VK_RIGHT", {}); + } + synthesizeKey("VK_BACK_SPACE", {}); + is(edit.textContent, "ab", "The backspace key should delete the UTF-16 surrogate pair correctly"); +} + +function runTest() { + /* test backspace-deletion of the middle character */ + test(document.getElementById("edit0")); + test(document.getElementById("edit1")); + test(document.getElementById("edit2")); + test(document.getElementById("edit3")); + + /* extra tests with the use of RIGHT and LEFT to get to the right place */ + testWithMove(document.getElementById("edit0b"), 2); + testWithMove(document.getElementById("edit1b"), 1); + testWithMove(document.getElementById("edit2b"), 2); + testWithMove(document.getElementById("edit3b"), 1); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug332636.html^headers^ b/editor/libeditor/tests/test_bug332636.html^headers^ new file mode 100644 index 000000000..e853d6cee --- /dev/null +++ b/editor/libeditor/tests/test_bug332636.html^headers^ @@ -0,0 +1 @@ +Content-Type: text/html; charset=UTF-8 diff --git a/editor/libeditor/tests/test_bug366682.html b/editor/libeditor/tests/test_bug366682.html new file mode 100644 index 000000000..bac618941 --- /dev/null +++ b/editor/libeditor/tests/test_bug366682.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=366682 +--> +<head> + <title>Test for Bug 366682</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="spellcheck.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=366682">Mozilla Bug 366682</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 366682 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +var gMisspeltWords; + +function getEdit() { + return document.getElementById('edit'); +} + +function editDoc() { + return getEdit().contentDocument; +} + +function getEditor() { + var Ci = SpecialPowers.Ci; + var win = editDoc().defaultView; + var editingSession = SpecialPowers.wrap(win) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + return editingSession.getEditorForWindow(win); +} + +function runTest() { + editDoc().body.innerHTML = "<div>errror and an other errror</div>"; + gMisspeltWords = ["errror", "errror"]; + editDoc().designMode = "on"; + + SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm") + .onSpellCheck(editDoc().documentElement, evalTest); +} + +function evalTest() { + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "All misspellings accounted for."); + SimpleTest.finish(); +} +</script> +</pre> + +<iframe id="edit" width="200" height="100" src="about:blank"></iframe> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug372345.html b/editor/libeditor/tests/test_bug372345.html new file mode 100644 index 000000000..e9b1ac7a9 --- /dev/null +++ b/editor/libeditor/tests/test_bug372345.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=372345 +--> +<head> + <title>Test for Bug 372345</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=372345">Mozilla Bug 372345</a> +<p id="display"></p> +<div id="content"> + <iframe src="data:text/html,<body>"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 372345 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var iframe = document.querySelector("iframe"); + var doc = iframe.contentDocument; + var content = doc.body; + var link = content.querySelector("a"); + function testCursor(post) { + setTimeout(function() { + var link = document.createElement("a"); + link.href = "http://mozilla.org/"; + link.textContent = "link"; + link.style.cursor = "pointer"; + content.appendChild(link); + is(iframe.contentWindow.getComputedStyle(link, null).cursor, "pointer", "Make sure that the cursor is set to pointer"); + setTimeout(post, 0); + }, 0); + } + testCursor(function() { + doc.designMode = "on"; + testCursor(function() { + doc.designMode = "off"; + testCursor(function() { + content.setAttribute("contenteditable", "true"); + testCursor(function() { + content.removeAttribute("contenteditable"); + testCursor(function() { + SimpleTest.finish(); + }); + }); + }); + }); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug404320.html b/editor/libeditor/tests/test_bug404320.html new file mode 100644 index 000000000..b8249a557 --- /dev/null +++ b/editor/libeditor/tests/test_bug404320.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=404320 +--> +<head> + <title>Test for Bug 404320</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=404320">Mozilla Bug 404320</a> +<p id="display"></p> +<div id="content"> + <iframe id="testIframe"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 404320 **/ + +SimpleTest.waitForExplicitFinish(); + +function runTests() { + var win = document.getElementById("testIframe").contentWindow; + var doc = document.getElementById("testIframe").contentDocument; + + function testFormatBlock(tag, withAngleBrackets, shouldSucceed) + { + win.getSelection().selectAllChildren(doc.body.firstChild); + doc.execCommand("FormatBlock", false, + withAngleBrackets ? tag : "<" + tag + ">"); + var resultNode; + if (shouldSucceed && (tag == "dd" || tag == "dt")) { + is(doc.body.firstChild.tagName, "DL", "tag was changed"); + resultNode = doc.body.firstChild.firstChild; + } + else { + resultNode = doc.body.firstChild; + } + + is(resultNode.tagName, shouldSucceed ? tag.toUpperCase() : "P", "tag was changed"); + } + + function formatBlockTests(tags, shouldSucceed) + { + var html = "<p>Content</p>"; + for (var i = 0; i < tags.length; ++i) { + var tag = tags[i]; + var resultTag = tag.toUpperCase(); + + doc.body.innerHTML = html; + testFormatBlock(tag, false, shouldSucceed); + + doc.body.innerHTML = html; + testFormatBlock(tag, true, shouldSucceed); + } + } + + doc.designMode = "on"; + + var goodTags = [ "address", + "blockquote", + "dd", + "div", + "dl", + "dt", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "p", + "pre" ]; + var badTags = [ "b", + "i", + "span", + "foo" ]; + + formatBlockTests(goodTags, true); + formatBlockTests(badTags, false); + SimpleTest.finish(); +} + +addLoadEvent(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug408231.html b/editor/libeditor/tests/test_bug408231.html new file mode 100644 index 000000000..d365bfa09 --- /dev/null +++ b/editor/libeditor/tests/test_bug408231.html @@ -0,0 +1,250 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=408231 +--> +<head> + <title>Test for Bug 408231</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408231">Mozilla Bug 408231</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 408231 **/ + + var commandEnabledResults = [ + ["contentReadOnly", "true"], + ["copy", "false"], + ["createlink", "true"], + ["cut", "false"], + ["decreasefontsize", "true"], + ["delete", "true"], + ["fontname", "true"], + ["fontsize", "true"], + ["formatblock", "true"], + ["heading", "true"], + ["hilitecolor", "true"], + ["increasefontsize", "true"], + ["indent", "true"], + ["inserthorizontalrule", "true"], + ["inserthtml", "true"], + ["insertimage", "true"], + ["insertorderedlist", "true"], + ["insertunorderedlist", "true"], + ["insertparagraph", "true"], + ["italic", "true"], + ["justifycenter", "true"], + ["justifyfull", "true"], + ["justifyleft", "true"], + ["justifyright", "true"], + ["outdent", "true"], + ["paste", "false"], + ["redo", "false"], + ["removeformat", "true"], + ["selectall", "true"], + ["strikethrough", "true"], + ["styleWithCSS", "true"], + ["subscript", "true"], + ["superscript", "true"], + ["underline", "true"], + ["undo", "false"], + ["unlink", "true"], + ["not-a-command", "false"] + ]; + + var commandIndetermResults = [ + ["contentReadOnly", "false"], + ["copy", "false"], + ["createlink", "false"], + ["cut", "false"], + ["decreasefontsize", "false"], + ["delete", "false"], + ["fontname", "false"], + ["fontsize", "false"], + ["formatblock", "false"], + ["heading", "false"], + ["hilitecolor", "false"], + ["increasefontsize", "false"], + ["indent", "false"], + ["inserthorizontalrule", "false"], + ["inserthtml", "false"], + ["insertimage", "false"], + ["insertorderedlist", "false"], + ["insertunorderedlist", "false"], + ["insertparagraph", "false"], + ["italic", "false"], + ["justifycenter", "false"], + ["justifyfull", "false"], + ["justifyleft", "false"], + ["justifyright", "false"], + ["outdent", "false"], + //["paste", "false"], + ["redo", "false"], + ["removeformat", "false"], + ["selectall", "false"], + ["strikethrough", "false"], + ["styleWithCSS", "false"], + ["subscript", "false"], + ["superscript", "false"], + ["underline", "false"], + ["undo", "false"], + ["unlink", "false"], + ["not-a-command", "false"] + ]; + + var commandStateResults = [ + ["contentReadOnly", "false"], + ["copy", "false"], + ["createlink", "false"], + ["cut", "false"], + ["decreasefontsize", "false"], + ["delete", "false"], + ["fontname", "false"], + ["fontsize", "false"], + ["formatblock", "false"], + ["heading", "false"], + ["hilitecolor", "false"], + ["increasefontsize", "false"], + ["indent", "false"], + ["inserthorizontalrule", "false"], + ["inserthtml", "false"], + ["insertimage", "false"], + ["insertorderedlist", "false"], + ["insertunorderedlist", "false"], + ["insertparagraph", "false"], + ["italic", "false"], + ["justifycenter", "false"], + ["justifyfull", "false"], + ["justifyleft", "true"], + ["justifyright", "false"], + ["outdent", "false"], + //["paste", "false"], + ["redo", "false"], + ["removeformat", "false"], + ["selectall", "false"], + ["strikethrough", "false"], + ["styleWithCSS", "false"], + ["subscript", "false"], + ["superscript", "false"], + ["underline", "false"], + ["undo", "false"], + ["unlink", "false"], + ["not-a-command", "false"] + ]; + + var commandValueResults = [ + ["contentReadOnly", ""], + ["copy", ""], + ["createlink", ""], + ["cut", ""], + ["decreasefontsize", ""], + ["delete", ""], + ["fontname", "serif"], + ["fontsize", ""], + ["formatblock", ""], + ["heading", ""], + ["hilitecolor", "transparent"], + ["increasefontsize", ""], + ["indent", ""], + ["inserthorizontalrule", ""], + ["inserthtml", ""], + ["insertimage", ""], + ["insertorderedlist", ""], + ["insertunorderedlist", ""], + ["insertparagraph", ""], + ["italic", ""], + ["justifycenter", "left"], + ["justifyfull", "left"], + ["justifyleft", "left"], + ["justifyright", "left"], + ["outdent", ""], + //["paste", ""], + ["redo", ""], + ["removeformat", ""], + ["selectall", ""], + ["strikethrough", ""], + ["styleWithCSS", ""], + ["subscript", ""], + ["superscript", ""], + ["underline", ""], + ["undo", ""], + ["unlink", ""], + ["not-a-command", ""], + ]; + + + function callQueryCommandEnabled(cmdName) { + var result; + try { + result = '' + document.queryCommandEnabled( cmdName ); + } catch( error ) { + result = 'name' in error ? error.name : 'exception'; + } + return result; + } + + function callQueryCommandIndeterm(cmdName) { + var result; + try { + result = '' + document.queryCommandIndeterm( cmdName ); + } catch( error ) { + result = 'name' in error ? error.name : 'exception'; + } + return result; + } + + function callQueryCommandState(cmdName) { + var result; + try { + result = '' + document.queryCommandState( cmdName ); + } catch( error ) { + result = 'name' in error ? error.name : 'exception'; + } + return result; + } + + function callQueryCommandValue(cmdName) { + var result; + try { + result = '' + document.queryCommandValue( cmdName ); + } catch( error ) { + result = 'name' in error ? error.name : 'exception'; + } + return result; + } + + function testQueryCommand(expectedResults, fun, funName) { + for (i=0; i<expectedResults.length; i++) { + var commandName = expectedResults[i][0]; + var expectedResult = expectedResults[i][1]; + var result = fun(commandName); + ok(result == expectedResult, funName + '('+commandName+') result=' +result+ ' expected=' + expectedResult); + } + } + + function runTests() { + document.designMode='on'; + window.getSelection().collapse(document.body, 0); + testQueryCommand(commandEnabledResults, callQueryCommandEnabled, "queryCommandEnabled"); + testQueryCommand(commandIndetermResults, callQueryCommandIndeterm, "queryCommandIndeterm"); + testQueryCommand(commandStateResults, callQueryCommandState, "queryCommandState"); + testQueryCommand(commandValueResults, callQueryCommandValue, "queryCommandValue"); + document.designMode='off'; + SimpleTest.finish(); + } + + window.onload = runTests; + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/editor/libeditor/tests/test_bug410986.html b/editor/libeditor/tests/test_bug410986.html new file mode 100644 index 000000000..a3f3a5602 --- /dev/null +++ b/editor/libeditor/tests/test_bug410986.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=410986 +--> +<head> + <title>Test for Bug 410986</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=410986">Mozilla Bug 410986</a> +<p id="display"></p> +<div id="content"> + <div id="contents"><span style="color: green;">green text</span></div> + <div id="editor" contenteditable="true"></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 410986 **/ + +var gPasteEvents = 0; +document.getElementById("editor").addEventListener("paste", function() { + ++gPasteEvents; +}, false); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + getSelection().selectAllChildren(document.getElementById("contents")); + SimpleTest.waitForClipboard("green text", + function() { + synthesizeKey("C", {accelKey: true}); + }, + function() { + var ed = document.getElementById("editor"); + ed.focus(); + if (navigator.platform.indexOf("Mac") >= 0) { + synthesizeKey("V", {accelKey: true, shiftKey: true, altKey: true}); + } else { + synthesizeKey("V", {accelKey: true, shiftKey: true}); + } + is(ed.innerHTML, "green text", "Content should be pasted in plaintext format"); + is(gPasteEvents, 1, "One paste event must be fired"); + + ed.innerHTML = ""; + ed.blur(); + getSelection().selectAllChildren(document.getElementById("contents")); + SimpleTest.waitForClipboard("green text", + function() { + synthesizeKey("C", {accelKey: true}); + }, + function() { + var ed = document.getElementById("editor"); + ed.focus(); + synthesizeKey("V", {accelKey: true}); + isnot(ed.innerHTML.indexOf("<span style=\"color: green;\">green text</span>"), -1, + "Content should be pasted in HTML format"); + is(gPasteEvents, 2, "Two paste events must be fired"); + + SimpleTest.finish(); + }, + function() { + ok(false, "Failed to copy the second item to the clipboard"); + SimpleTest.finish(); + } + ); + }, + function() { + ok(false, "Failed to copy the first item to the clipboard"); + SimpleTest.finish(); + } + ); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug414526.html b/editor/libeditor/tests/test_bug414526.html new file mode 100644 index 000000000..0975b6a5a --- /dev/null +++ b/editor/libeditor/tests/test_bug414526.html @@ -0,0 +1,247 @@ +<html> +<head> + <title>Test for backspace key and delete key shouldn't remove another editing host's text</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="display"></div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() +{ + + var container = document.getElementById("display"); + + function reset() + { + document.execCommand("Undo", false, null); + } + + var selection = window.getSelection(); + function moveCaretToStartOf(aEditor) + { + selection.selectAllChildren(aEditor); + selection.collapseToStart(); + } + + function moveCaretToEndOf(aEditor) + { + selection.selectAllChildren(aEditor); + selection.collapseToEnd(); + } + + /* TestCase #1 + */ + const kTestCase1 = + "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" + + "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" + + "<div id=\"editor3\" contenteditable=\"true\"><div>editor3</div></div>" + + "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" + + "non-editable text" + + "<p id=\"editor5\" contenteditable=\"true\">editor5</p>"; + + const kTestCase1_editor3_deleteAtStart = + "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" + + "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" + + "<div id=\"editor3\" contenteditable=\"true\"><div>ditor3</div></div>" + + "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" + + "non-editable text" + + "<p id=\"editor5\" contenteditable=\"true\">editor5</p>"; + + const kTestCase1_editor3_backspaceAtEnd = + "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" + + "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" + + "<div id=\"editor3\" contenteditable=\"true\"><div>editor</div></div>" + + "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" + + "non-editable text" + + "<p id=\"editor5\" contenteditable=\"true\">editor5</p>"; + + container.innerHTML = kTestCase1; + + var editor1 = document.getElementById("editor1"); + var editor2 = document.getElementById("editor2"); + var editor3 = document.getElementById("editor3"); + var editor4 = document.getElementById("editor4"); + var editor5 = document.getElementById("editor5"); + + /* TestCase #1: + * pressing backspace key at start should not change the content. + */ + editor2.focus(); + moveCaretToStartOf(editor2); + synthesizeKey("VK_BACK_SPACE", { }); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at start of editor2 changes the content"); + reset(); + + editor3.focus(); + moveCaretToStartOf(editor3); + synthesizeKey("VK_BACK_SPACE", { }); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at start of editor3 changes the content"); + reset(); + + editor4.focus(); + moveCaretToStartOf(editor4); + synthesizeKey("VK_BACK_SPACE", { }); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at start of editor4 changes the content"); + reset(); + + editor5.focus(); + moveCaretToStartOf(editor5); + synthesizeKey("VK_BACK_SPACE", { }); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at start of editor5 changes the content"); + reset(); + + /* TestCase #1: + * pressing delete key at end should not change the content. + */ + editor1.focus(); + moveCaretToEndOf(editor1); + synthesizeKey("VK_DELETE", { }); + is(container.innerHTML, kTestCase1, + "Pressing delete key at end of editor1 changes the content"); + reset(); + + editor2.focus(); + moveCaretToEndOf(editor2); + synthesizeKey("VK_DELETE", { }); + is(container.innerHTML, kTestCase1, + "Pressing delete key at end of editor2 changes the content"); + reset(); + + editor3.focus(); + moveCaretToEndOf(editor3); + synthesizeKey("VK_DELETE", { }); + is(container.innerHTML, kTestCase1, + "Pressing delete key at end of editor3 changes the content"); + reset(); + + editor4.focus(); + moveCaretToEndOf(editor4); + synthesizeKey("VK_DELETE", { }); + is(container.innerHTML, kTestCase1, + "Pressing delete key at end of editor4 changes the content"); + reset(); + + /* TestCase #1: cases when the caret is not on text node. + * - pressing delete key at start should remove the first character + * - pressing backspace key at end should remove the first character + * and the adjacent blocks should not be changed. + */ + editor3.focus(); + moveCaretToStartOf(editor3); + synthesizeKey("VK_DELETE", { }); + is(container.innerHTML, kTestCase1_editor3_deleteAtStart, + "Pressing delete key at start of editor3 changes adjacent elements" + + " and/or does not remove the first character."); + reset(); + + // Backspace doesn't work here yet. + editor3.focus(); + moveCaretToEndOf(editor3); + synthesizeKey("VK_BACK_SPACE", { }); + todo_is(container.innerHTML, kTestCase1_editor3_backspaceAtEnd, + "Pressing backspace key at end of editor3 changes adjacent elements" + + " and/or does not remove the last character."); + reset(); + // We can still check that adjacent elements are not affected. + editor3.focus(); + moveCaretToEndOf(editor3); + synthesizeKey("VK_BACK_SPACE", { }); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at end of editor3 changes the content"); + reset(); + + /* TestCase #2: + * two adjacent editable <span> in a table cell. + */ + const kTestCase2 = "<table><tbody><tr><td><span id=\"editor1\" contenteditable=\"true\">test</span>" + + "<span id=\"editor2\" contenteditable=\"true\">test</span></td></tr></tbody></table>"; + + container.innerHTML = kTestCase2; + editor1 = document.getElementById("editor1"); + editor2 = document.getElementById("editor2"); + + editor2.focus(); + moveCaretToStartOf(editor2); + synthesizeKey("VK_BACK_SPACE", { }); + is(container.innerHTML, kTestCase2, + "Pressing backspace key at the start of editor2 changes the content for kTestCase2"); + reset(); + + editor1.focus(); + moveCaretToEndOf(editor1); + synthesizeKey("VK_DELETE", { }); + is(container.innerHTML, kTestCase2, + "Pressing delete key at the end of editor1 changes the content for kTestCase2"); + reset(); + + /* TestCase #3: + * editable <span> in two adjacent table cells. + */ + const kTestCase3 = "<table><tbody><tr><td><span id=\"editor1\" contenteditable=\"true\">test</span></td>" + + "<td><span id=\"editor2\" contenteditable=\"true\">test</span></td></tr></tbody></table>"; + + container.innerHTML = kTestCase3; + editor1 = document.getElementById("editor1"); + editor2 = document.getElementById("editor2"); + + editor2.focus(); + moveCaretToStartOf(editor2); + synthesizeKey("VK_BACK_SPACE", { }); + is(container.innerHTML, kTestCase3, + "Pressing backspace key at the start of editor2 changes the content for kTestCase3"); + reset(); + + editor1.focus(); + moveCaretToEndOf(editor1); + synthesizeKey("VK_DELETE", { }); + is(container.innerHTML, kTestCase3, + "Pressing delete key at the end of editor1 changes the content for kTestCase3"); + reset(); + + /* TestCase #4: + * editable <div> in two adjacent table cells. + */ + const kTestCase4 = "<table><tbody><tr><td><div id=\"editor1\" contenteditable=\"true\">test</div></td>" + + "<td><div id=\"editor2\" contenteditable=\"true\">test</div></td></tr></tbody></table>"; + + container.innerHTML = kTestCase4; + editor1 = document.getElementById("editor1"); + editor2 = document.getElementById("editor2"); + + editor2.focus(); + moveCaretToStartOf(editor2); + synthesizeKey("VK_BACK_SPACE", { }); + is(container.innerHTML, kTestCase4, + "Pressing backspace key at the start of editor2 changes the content for kTestCase4"); + reset(); + + editor1.focus(); + moveCaretToEndOf(editor1); + synthesizeKey("VK_DELETE", { }); + is(container.innerHTML, kTestCase4, + "Pressing delete key at the end of editor1 changes the content for kTestCase4"); + reset(); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug417418.html b/editor/libeditor/tests/test_bug417418.html new file mode 100644 index 000000000..146de0920 --- /dev/null +++ b/editor/libeditor/tests/test_bug417418.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=417418 +--> +<head> + <title>Test for Bug 417418</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=417418">Mozilla Bug 417418</a> +<div id="display" contenteditable="true"> +<p id="coin">first paragraph</p> +<p>second paragraph. <img id="img" src="green.png"></p> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 417418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +function resetSelection() { + window.getSelection().collapse(document.getElementById("coin"), 0); +} + +function runTest() { + var rightClickDown = {type: 'mousedown', button: 2}, + rightClickUp = {type: 'mouseup', button: 2}, + singleClickDown = {type: 'mousedown', button: 0}, + singleClickUp = {type: 'mouseup', button: 0}; + var selection = window.getSelection(); + + var div = document.getElementById('display'); + var img = document.getElementById('img'); + var divRect = div.getBoundingClientRect(); + var imgselected; + + resetSelection(); + synthesizeMouse(div, divRect.width - 1, divRect.height - 1, rightClickDown); + synthesizeMouse(div, divRect.width - 1, divRect.height - 1, rightClickUp); + ok(selection.isCollapsed, "selection is not collapsed"); + + resetSelection(); + synthesizeMouse(div, divRect.width - 1, divRect.height - 1, singleClickDown); + synthesizeMouse(div, divRect.width - 1, divRect.height - 1, singleClickUp); + ok(selection.isCollapsed, "selection is not collapsed"); + + resetSelection(); + synthesizeMouseAtCenter(img, rightClickDown); + synthesizeMouseAtCenter(img, rightClickUp); + imgselected = selection.anchorNode == img.parentNode && + selection.anchorOffset === 1 && + selection.rangeCount === 1; + ok(imgselected, "image is not selected"); + + resetSelection(); + synthesizeMouseAtCenter(img, singleClickDown); + synthesizeMouseAtCenter(img, singleClickUp); + imgselected = selection.anchorNode == img.parentNode && + selection.anchorOffset === 1 && + selection.rangeCount === 1; + ok(imgselected, "image is not selected"); + + SimpleTest.finish(); +} + + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug432225.html b/editor/libeditor/tests/test_bug432225.html new file mode 100644 index 000000000..58d158722 --- /dev/null +++ b/editor/libeditor/tests/test_bug432225.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=432225 +--> +<head> + <title>Test for Bug 432225</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script src="spellcheck.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=432225">Mozilla Bug 432225</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 432225 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +var gMisspeltWords = []; + +function getEdit() { + return document.getElementById('edit'); +} + +function editDoc() { + return getEdit().contentDocument; +} + +function getEditor() { + var Ci = SpecialPowers.Ci; + var win = editDoc().defaultView; + var editingSession = SpecialPowers.wrap(win) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + return editingSession.getEditorForWindow(win); +} + +function runTest() { + editDoc().designMode = "on"; + setTimeout(function() { addWords(100); }, 0); +} + +function addWords(aLimit) { + if (aLimit == 0) { + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "All misspellings accounted for."); + SimpleTest.finish(); + return; + } + getEdit().focus(); + sendString('aa OK '); + gMisspeltWords.push("aa"); + setTimeout(function() { addWords(aLimit-1); }, 0); +} +</script> +</pre> + +<iframe id="edit" width="200" height="100" src="about:blank"></iframe> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug439808.html b/editor/libeditor/tests/test_bug439808.html new file mode 100644 index 000000000..a04d1d4d4 --- /dev/null +++ b/editor/libeditor/tests/test_bug439808.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=439808 +--> +<head> + <title>Test for Bug 439808</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=439808">Mozilla Bug 439808</a> +<p id="display"></p> +<div id="content"> +<span><span contenteditable id="e">twest</span></span> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 439808 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var e = document.getElementById("e"); + e.focus(); + getSelection().collapse(e.firstChild, 1); + synthesizeKey("VK_DELETE", {}); + is(e.textContent, "test", "Delete key worked"); + synthesizeKey("VK_BACK_SPACE", {}); + is(e.textContent, "est", "Backspace key worked"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug442186.html b/editor/libeditor/tests/test_bug442186.html new file mode 100644 index 000000000..eab81e055 --- /dev/null +++ b/editor/libeditor/tests/test_bug442186.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=442186 +--> +<head> + <title>Test for Bug 442186</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=442186">Mozilla Bug 442186</a> +<p id="display"></p> +<div id="content"> + <h2> two <div> containers </h2> + <section contenteditable id="test1"> + <div> First paragraph with some text. </div> + <div> Second paragraph with some text. </div> + </section> + + <h2> two paragraphs </h2> + <section contenteditable id="test2"> + <p> First paragraph with some text. </p> + <p> Second paragraph with some text. </p> + </section> + + <h2> one text node, one paragraph </h2> + <section contenteditable id="test3"> + First paragraph with some text. + <p> Second paragraph with some text. </p> + </section> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 442186 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function justify(textNode, pos) { + if (!pos) pos = 10; + + // put the caret on the requested character + var range = document.createRange(); + var sel = window.getSelection(); + range.setStart(textNode, pos); + range.setEnd(textNode, pos); + sel.addRange(range); + + // align + document.execCommand("justifyright", false, null); +} + +function runTests() { + document.execCommand("stylewithcss", false, "true"); + + const test1 = document.getElementById("test1"); + const test2 = document.getElementById("test2"); + const test3 = document.getElementById("test3"); + + // #test1: two <div> containers + const line1 = test1.querySelector("div").firstChild; + test1.focus(); + justify(line1); + is(test1.querySelectorAll("*").length, 2, + "Aligning the first child should not create nor remove any element."); + is(line1.parentNode.nodeName.toLowerCase(), "div", + "Aligning the first <div> should not modify its node type."); + is(line1.parentNode.style.textAlign, "right", + "Aligning the first <div> should set a 'text-align: right' style rule."); + + // #test2: two paragraphs + const line2 = test2.querySelector("p").firstChild; + test2.focus(); + justify(line2); + is(test2.querySelectorAll("*").length, 2, + "Aligning the first child should not create nor remove any element."); + is(line2.parentNode.nodeName.toLowerCase(), "p", + "Aligning the first paragraph should not modify its node type."); + is(line2.parentNode.style.textAlign, "right", + "Aligning the first paragraph should set a 'text-align: right' style rule."); + + // #test3: one text node, two paragraphs + const line3 = test3.firstChild; + test3.focus(); + justify(line3); + is(test3.querySelectorAll("*").length, 2, + "Aligning the first child should create a block element."); + is(line3.parentNode.nodeName.toLowerCase(), "div", + "Aligning the first child should create a block element."); + is(line3.parentNode.style.textAlign, "right", + "Aligning the first line should set a 'text-align: right' style rule."); + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug449243.html b/editor/libeditor/tests/test_bug449243.html new file mode 100644 index 000000000..77a7c6a7d --- /dev/null +++ b/editor/libeditor/tests/test_bug449243.html @@ -0,0 +1,136 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=449243 +--> +<head> + <title>Test for Bug 449243</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=449243">Mozilla Bug 449243</a> +<p id="display"></p> +<div id="content" contenteditable> + <h2>This is a title</h2> + <ul> + <li>this is a</li> + <li>bullet list</li> + </ul> + <ol> + <li>this is a</li> + <li>numbered list</li> + </ol> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 449243 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +const CARET_BEGIN = 0; +const CARET_MIDDLE = 1; +const CARET_END = 2; + +function split(element, caretPos, nbKeyPresses) { + // put the caret on the requested position + var sel = window.getSelection(); + var len = element.textContent.length; + var pos = -1; + switch (caretPos) { + case CARET_BEGIN: + pos = 0; + break; + case CARET_MIDDLE: + pos = Math.floor(len/2); + break; + case CARET_END: + pos = len; + break; + } + sel.collapse(element.firstChild, pos); + + // simulates a [Return] keypress + for (var i = 0; i < nbKeyPresses; i++) + synthesizeKey("VK_RETURN", {}); +} + +function undo(nbKeyPresses) { + for (var i = 0; i < nbKeyPresses; i++) + document.execCommand("Undo", false, null); +} + +function SameTypeAsPreviousSibling(element) { + var sibling = element.previousSibling; + while (sibling && sibling.nodeType != 1) + sibling = element.previousSibling; + return (element.nodeName == sibling.nodeName); +} + +function isParagraph(element) { + return element.nodeName.toLowerCase() == "p"; +} + +function runTests() { + const content = document.querySelector("[contenteditable]"); + const header = content.querySelector("h2"); + const ulItem = content.querySelector("ul > li:last-child"); + const olItem = content.querySelector("ol > li:last-child"); + content.focus(); + + // beginning of selection: split current node + split(header, CARET_BEGIN, 1); + ok(SameTypeAsPreviousSibling(header), + "Pressing [Return] at the beginning of a header " + + "should create another header."); + split(ulItem, CARET_BEGIN, 2); + ok(SameTypeAsPreviousSibling(ulItem), + "Pressing [Return] at the beginning of an unordered list item " + + "should create another list item."); + split(olItem, CARET_BEGIN, 2); + ok(SameTypeAsPreviousSibling(olItem), + "Pressing [Return] at the beginning of an ordered list item " + + "should create another list item."); + undo(3); + + // middle of selection: split current node + split(header, CARET_MIDDLE, 1); + ok(SameTypeAsPreviousSibling(header), + "Pressing [Return] at the middle of a header " + + "should create another header."); + split(ulItem, CARET_MIDDLE, 2); + ok(SameTypeAsPreviousSibling(ulItem), + "Pressing [Return] at the middle of an unordered list item " + + "should create another list item."); + split(olItem, CARET_MIDDLE, 2); + ok(SameTypeAsPreviousSibling(olItem), + "Pressing [Return] at the middle of an ordered list item " + + "should create another list item."); + undo(3); + + // end of selection: create a new paragraph + split(header, CARET_END, 1); + ok(isParagraph(content.querySelector("h2+*")), + "Pressing [Return] at the end of a header " + + "should create a new paragraph."); + split(ulItem, CARET_END, 2); + ok(isParagraph(content.querySelector("ul+*")), + "Pressing [Return] twice at the end of an unordered list item " + + "should create a new paragraph."); + split(olItem, CARET_END, 2); + ok(isParagraph(content.querySelector("ol+*")), + "Pressing [Return] twice at the end of an ordered list item " + + "should create a new paragraph."); + undo(3); + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug455992.html b/editor/libeditor/tests/test_bug455992.html new file mode 100644 index 000000000..daf362acf --- /dev/null +++ b/editor/libeditor/tests/test_bug455992.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 455992</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> +function runTest() { + + function select(id) { + var e = document.getElementById(id); + e.focus(); + return e; + } + + function setupIframe(id) { + var e = document.getElementById(id); + var doc = e.contentDocument; + doc.body.innerHTML = String.fromCharCode(10)+'<span id="' + id + '_span" style="border:1px solid blue" contenteditable="true">X</span>'+String.fromCharCode(10); + e = doc.getElementById(id + "_span"); + e.focus(); + return e; + } + + function test_begin_bs(e) { + const msg = "BACKSPACE at beginning of contenteditable inline element"; + var before = e.parentNode.childNodes[0].nodeValue; + sendKey("back_space"); + is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, "X", msg + " with id=" + e.id); + } + + function test_begin_space(e) { + const msg = "SPACE at beginning of contenteditable inline element"; + var before = e.parentNode.childNodes[0].nodeValue; + sendChar(" "); + is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, " X", msg + " with id=" + e.id); + } + + function test_end_delete(e) { + const msg = "DEL at end of contenteditable inline element"; + var before = e.parentNode.childNodes[2].nodeValue; + sendKey("right"); + sendKey("delete"); + is(e.parentNode.childNodes[2].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, "X", msg + " with id=" + e.id); + } + + function test_end_space(e) { + const msg = "SPACE at end of contenteditable inline element"; + var before = e.parentNode.childNodes[2].nodeValue; + sendKey("right"); + sendChar(" "); + is(e.parentNode.childNodes[2].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, "X" + (e.tagName=="SPAN" ? " " : " <br>"), msg + " with id=" + e.id); + } + + test_begin_bs(select("t1")); + test_begin_space(select("t2")); + test_end_delete(select("t3")); + test_end_space(select("t4")); + test_end_space(select("t5")); + + test_begin_bs(setupIframe('i1')); + test_begin_space(setupIframe('i2')); + test_end_delete(setupIframe('i3')); + test_end_space(setupIframe('i4')); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=455992">Mozilla Bug 455992</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<div> <span id="t1" style="border:1px solid blue" contenteditable="true">X</span> Y</div> +<div> <span id="t2" style="border:1px solid blue" contenteditable="true">X</span> Y</div> +<div> <span id="t3" style="border:1px solid blue" contenteditable="true">X</span> Y</div> +<div> <span id="t4" style="border:1px solid blue" contenteditable="true">X</span> Y</div> +<div> <div id="t5" style="border:1px solid blue" contenteditable="true">X</div> Y</div> + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i3" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i4" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug456244.html b/editor/libeditor/tests/test_bug456244.html new file mode 100644 index 000000000..03cc2c9e3 --- /dev/null +++ b/editor/libeditor/tests/test_bug456244.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 456244</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> +function runTest() { + + function select(id) { + var e = document.getElementById(id); + e.focus(); + return e; + } + + function setupIframe(id) { + var e = document.getElementById(id); + var doc = e.contentDocument; + doc.body.innerHTML = String.fromCharCode(10)+'<span id="' + id + '_span" style="border:1px solid blue" contenteditable="true">X</span>'+String.fromCharCode(10); + e = doc.getElementById(id + "_span"); + e.focus(); + return e; + } + + function test_end_bs(e) { + const msg = "Deleting all text in contenteditable inline element"; + var before = e.parentNode.childNodes[0].nodeValue; + sendKey("right"); + sendKey("back_space"); + sendKey("back_space"); + is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, "", msg + " with id=" + e.id); + } + + test_end_bs(select("t1")); + test_end_bs(setupIframe('i1',0)); + + { + const msg = "Deleting all text in contenteditable body element"; + var e = document.getElementById('i2'); + var doc = e.contentDocument; + doc.body.setAttribute("contenteditable", "true"); + doc.body.focus(); + sendKey("right"); + sendKey("back_space"); + is(doc.body.innerHTML, "<br>", msg + " with id=" + e.id); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=456244">Mozilla Bug 456244</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<div> <span id="t1" style="border:1px solid blue" contenteditable="true">X</span> Y</div> + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank">X</iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug460740.html b/editor/libeditor/tests/test_bug460740.html new file mode 100644 index 000000000..b9e79c1e0 --- /dev/null +++ b/editor/libeditor/tests/test_bug460740.html @@ -0,0 +1,124 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=460740 +--> +<head> + <title>Test for Bug 460740</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=460740">Mozilla Bug 460740</a> +<p id="display"></p> +<div id="content"> + <ul> + <li contenteditable> + Editable LI + </li> + <li> + <div contenteditable> + Editable DIV inside LI + </div> + </li> + <li> + <div> + <div contenteditable> + Editable DIV inside DIV inside LI + </div> + </div> + </li> + <li> + <h3> + <div contenteditable> + Editable DIV inside H3 inside LI + </div> + </h3> + </li> + </ul> + <div contenteditable> + Editable DIV + </div> + <h3 contenteditable> + Editable H3 + </h3> + <p contenteditable> + Editable P + </p> + <div> + <p contenteditable> + Editable P in a DIV + </p> + </div> + <p><span contenteditable>Editable SPAN in a P</span></p> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 460740 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +const CARET_BEGIN = 0; +const CARET_MIDDLE = 1; +const CARET_END = 2; + +function split(element, caretPos) { + // compute the requested position + var len = element.textContent.length; + var pos = -1; + switch (caretPos) { + case CARET_BEGIN: + pos = 0; + break; + case CARET_MIDDLE: + pos = Math.floor(len/2); + break; + case CARET_END: + pos = len; + break; + } + + // put the caret on the requested position + var range = document.createRange(); + var sel = window.getSelection(); + range.setStart(element.firstChild, pos); + range.setEnd(element.firstChild, pos); + sel.addRange(range); + + // simulates a [Return] keypress + synthesizeKey("VK_RETURN", {}); +} + +// count the number of non-BR elements in #content +function getBlockCount() { + return document.querySelectorAll("#content *:not(br)").length; +} + +// count the number of BRs in element +function checkBR(element) { + return element.querySelectorAll("br").length; +} + +function runTests() { + var count = getBlockCount(); + var nodes = document.querySelectorAll("#content [contenteditable]"); + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + node.focus(); + is(checkBR(node), 0, node.textContent.trim() + ": This node should not have any <br> element yet."); + for (var j = 0; j < 3; j++) { // CARET_BEGIN|MIDDLE|END + split(node, j); + ok(checkBR(node) > 0, node.textContent.trim() + " " + j + ": Pressing [Return] should add (at least) one <br> element."); + is(getBlockCount(), count, node.textContent.trim() + " " + j + ": Pressing [Return] should not change the number of non-<br> elements."); + document.execCommand("Undo", false, null); + } + } + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug46555.html b/editor/libeditor/tests/test_bug46555.html new file mode 100644 index 000000000..3838bdb3b --- /dev/null +++ b/editor/libeditor/tests/test_bug46555.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=46555 +--> + +<head> + <title>Test for Bug 46555</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=46555">Mozilla Bug 46555</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <input type="text" value="" id="t1" /> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 46555 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + const kCmd = "cmd_selectAll"; + + var input = document.getElementById("t1"); + input.focus(); + var controller = + SpecialPowers.wrap(input).controllers.getControllerForCommand(kCmd); + + // Test 1: Select All should be disabled if editor is empty + is(controller.isCommandEnabled(kCmd), false, + "Select All command disabled when editor is empty"); + + SimpleTest.finish(); + }); + </script> + </pre> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug468353.html b/editor/libeditor/tests/test_bug468353.html new file mode 100644 index 000000000..179c41cdc --- /dev/null +++ b/editor/libeditor/tests/test_bug468353.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=468353 +--> +<head> + <title>Test for Bug 468353</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=468353">Mozilla Bug 468353</a> +<p id="display"></p> +<div id="content"> + <iframe></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var styleSheets = null; + +function checkStylesheets() { + // Evidently RemoveStyleSheet is the only method in nsIEditorStyleSheets + // that would throw. RemoveOverrideStyleSheet returns NS_OK even if the + // sheet is not there + var removed = 0; + try + { + styleSheets.removeStyleSheet("resource://gre/res/designmode.css"); + removed++; + } + catch (ex) { } + + try { + styleSheets.removeStyleSheet("resource://gre/res/contenteditable.css"); + removed++; + } + catch (ex) { } + + is(removed, 0, "Should have thrown if stylesheet was not there"); +} + +function runTest() { + const Ci = SpecialPowers.Ci; + + /** Found while fixing bug 440614 **/ + var editframe = window.frames[0]; + var editdoc = editframe.document; + var editor = null; + editdoc.write(''); + editdoc.close(); + + editdoc.designMode='on'; + + // Hold the reference to the editor + editor = SpecialPowers.wrap(editframe) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession) + .getEditorForWindow(editframe); + + styleSheets = editor.QueryInterface(Ci.nsIEditorStyleSheets); + + editdoc.designMode='off'; + + checkStylesheets(); + + // Let go + editor = null; + styleSheets = null; + + editdoc.body.contentEditable = true; + + // Hold the reference to the editor + editor = SpecialPowers.wrap(editframe) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession) + .getEditorForWindow(editframe); + + styleSheets = editor.QueryInterface(Ci.nsIEditorStyleSheets); + + editdoc.body.contentEditable = false; + + checkStylesheets(); + + editdoc.designMode = "on"; + editdoc.body.contentEditable = true; + editdoc.designMode = "off"; + + // Hold the reference to the editor + editor = SpecialPowers.wrap(editframe) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession) + .getEditorForWindow(editframe); + + styleSheets = editor.QueryInterface(Ci.nsIEditorStyleSheets); + + editdoc.body.contentEditable = false; + + checkStylesheets(); + + SimpleTest.finish(); +} + +//XXX I don't know if this is necessary, but we're dealing with iframes... +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug471319.html b/editor/libeditor/tests/test_bug471319.html new file mode 100644 index 000000000..399ba4611 --- /dev/null +++ b/editor/libeditor/tests/test_bug471319.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=471319 +--> + +<head> + <title>Test for Bug 471319</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=471319">Mozilla Bug 471319</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript;version=1.7"> + + /** Test for Bug 471319 **/ + + SimpleTest.waitForExplicitFinish(); + + function doTest() { + let t1 = SpecialPowers.wrap($("t1")); + let editor = null; + + // Test 1: Undo on an empty editor - the editor should not forget about + // the bogus node + t1.QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement); + t1Editor = t1.editor; + + // Did the editor recognize the new bogus node? + t1Editor.undo(1); + ok(!t1.value, "<br> still recognized as bogus node on undo"); + + + // Test 2: Redo on an empty editor - the editor should not forget about + // the bogus node + let t2 = SpecialPowers.wrap($("t2")); + t2.QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement); + t2Editor = t2.editor; + + // Did the editor recognize the new bogus node? + t2Editor.redo(1); + ok(!t2.value, "<br> still recognized as bogus node on redo"); + + + // Test 3: Undoing a batched transaction where both end points of the + // transaction are the bogus node - the bogus node should still be + // recognized as bogus + t1Editor.transactionManager.beginBatch(null); + t1.value = "mozilla"; + t1.value = ""; + t1Editor.transactionManager.endBatch(false); + t1Editor.undo(1); + ok(!t1.value, + "recreated <br> from undo transaction recognized as bogus"); + + + // Test 4: Redoing a batched transaction where both end points of the + // transaction are the bogus node - the bogus node should still be + // recognized as bogus + t1Editor.redo(1); + ok(!t1.value, + "recreated <br> from redo transaction recognized as bogus"); + SimpleTest.finish(); + } + </script> + </pre> + + <input type="text" id="t1" /> + <input type="text" id="t2" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug471722.html b/editor/libeditor/tests/test_bug471722.html new file mode 100644 index 000000000..74ff55307 --- /dev/null +++ b/editor/libeditor/tests/test_bug471722.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=471722 +--> + +<head> + <title>Test for Bug 471722</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=471722">Mozilla Bug 471722</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 471722 **/ + + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + var editor = null; + + if (t1 instanceof SpecialPowers.Ci.nsIDOMNSEditableElement) + editor = SpecialPowers.wrap(t1).editor; + ok(editor, "able to get editor for the element"); + t1.focus(); + t1.select(); + + try { + + // Cut the initial text in the textbox + ok(editor.canCut(), "can cut text"); + editor.cut(); + is(t1.value, "", "initial text was removed"); + + // So now we will have emptied the textfield + // and the editor will have created a bogus node + // Check the transaction is in the undo stack... + var t1Enabled = {}; + var t1CanUndo = {}; + editor.canUndo(t1Enabled, t1CanUndo); + ok(t1CanUndo.value, "undo is enabled"); + + // Undo the cut + editor.undo(1); + is(t1.value, "minefield", "text reinserted"); + + // So now, the cut should be in the redo stack, + // so executing the redo will clear the text once again + // and reinsert the bogus node that was removed after undo. + // This will require the editor to figure out that we have a + // bogus node again... + var t1CanRedo = {}; + editor.canRedo(t1Enabled, t1CanRedo); + ok(t1CanRedo.value, "redo is enabled"); + editor.redo(1); + + // Did the editor notice a bogus node reappeared? + is(t1.value, "", "editor found bogus node"); + } catch (e) { + ok(false, "test failed with error "+e); + } + SimpleTest.finish(); + } + </script> + </pre> + + <input type="text" value="minefield" id="t1" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug478725.html b/editor/libeditor/tests/test_bug478725.html new file mode 100644 index 000000000..8df85dfff --- /dev/null +++ b/editor/libeditor/tests/test_bug478725.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 478725</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + +function runTest() { + function verifyContent(s) { + var e = document.getElementById('i1'); + var doc = e.contentDocument; + is(doc.body.innerHTML, s, ""); + } + + function pasteInto(html,target_id) { + var e = document.getElementById('i1'); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.body.innerHTML = html; + e = doc.getElementById(target_id); + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(e); + selection.collapseToEnd(); + SpecialPowers.wrap(doc).execCommand("paste", false, null); + return e; + } + + function copyToClipBoard(s,asHTML,target_id) { + var e = document.getElementById('i2'); + var doc = e.contentDocument; + if (asHTML) { + doc.body.innerHTML = s; + } else { + var text = doc.createTextNode(s); + doc.body.appendChild(text); + } + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + if (!target_id) { + selection.selectAllChildren(doc.body); + } else { + var range = document.createRange(); + range.selectNode(doc.getElementById(target_id)); + selection.addRange(range); + } + SpecialPowers.wrap(doc).execCommand("copy", false, null); + return e; + } + + copyToClipBoard("<dl><dd>Hello Kitty</dd></dl>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); + verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl></li></ol>'); + + copyToClipBoard("<li>Hello Kitty</li>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); + verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>'); + + copyToClipBoard("<ol><li>Hello Kitty</li></ol>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); + verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>'); + + copyToClipBoard("<ul><li>Hello Kitty</li></ul>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); + verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>'); + + copyToClipBoard("<ul><li>Hello</li><ul><li>Kitty</li></ul></ul>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); + verifyContent('<ol><li id="paste_here">X</li><li>Hello</li><ul><li>Kitty</li></ul></ol>'); + + copyToClipBoard("<dl><dd>Hello</dd><dd>Kitty</dd></dl>", true); + pasteInto('<dl><dd id="paste_here">X</dd></dl>',"paste_here"); + verifyContent('<dl><dd id="paste_here">X</dd><dd>Hello</dd><dd>Kitty</dd></dl>'); + + copyToClipBoard("<dl><dd>Hello</dd><dd>Kitty</dd></dl>", true); + pasteInto('<dl><dt id="paste_here">X</dt></dl>',"paste_here"); + verifyContent('<dl><dt id="paste_here">X</dt><dd>Hello</dd><dd>Kitty</dd></dl>'); + + copyToClipBoard("<dl><dt>Hello</dt><dd>Kitty</dd></dl>", true); + pasteInto('<dl><dd id="paste_here">X</dd></dl>',"paste_here"); + verifyContent('<dl><dd id="paste_here">X</dd><dt>Hello</dt><dd>Kitty</dd></dl>'); + + copyToClipBoard("<pre>Kitty</pre>", true); + pasteInto('<pre id="paste_here">Hello </pre>',"paste_here"); + verifyContent('<pre id="paste_here">Hello Kitty</pre>'); + +// I was expecting these to trigger the special TABLE/TR rules in nsHTMLEditor::InsertHTMLWithContext +// but they don't for some reason... +// copyToClipBoard('<table><tr id="copy_here"><td>Kitty</td></tr></table>', true, "copy_here"); +// pasteInto('<table><tr id="paste_here"><td>Hello</td></tr></table>',"paste_here"); +// verifyContent(''); +// +// copyToClipBoard('<table id="copy_here"><tr><td>Kitty</td></tr></table>', true, "copy_here"); +// pasteInto('<table><tr id="paste_here"><td>Hello</td></tr></table>',"paste_here"); +// verifyContent(''); +// +// copyToClipBoard('<table id="copy_here"><tr><td>Kitty</td></tr></table>', true, "copy_here"); +// pasteInto('<table id="paste_here"><tr><td>Hello</td></tr></table>',"paste_here"); +// verifyContent(''); +// +// copyToClipBoard('<table><tr id="copy_here"><td>Kitty</td></tr></table>', true, "copy_here"); +// pasteInto('<table id="paste_here"><tr><td>Hello</td></tr></table>',"paste_here"); +// verifyContent(''); + + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478725">Mozilla Bug 478725</a> +<p id="display"></p> + +<pre id="test"> +</pre> + + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> + diff --git a/editor/libeditor/tests/test_bug480647.html b/editor/libeditor/tests/test_bug480647.html new file mode 100644 index 000000000..33f088a1b --- /dev/null +++ b/editor/libeditor/tests/test_bug480647.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=480647 +--> +<title>Test for Bug 480647</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=480647">Mozilla Bug 480647</a> +<div contenteditable></div> +<script> +/** Test for Bug 480647 **/ + +var div = document.querySelector("div"); + +function parseFontSize(input, expected) { + parseFontSizeInner(input, expected, is); +} + +function parseFontSizeTodo(input, expected) { + parseFontSizeInner(input, expected, todo_is); +} + +function parseFontSizeInner(input, expected, fn) { + div.innerHTML = "foo"; + getSelection().selectAllChildren(div); + document.execCommand("fontSize", false, input); + if (expected === null) { + fn(div.innerHTML, "foo", + 'execCommand("fontSize", false, "' + input + '") should be no-op'); + } else { + fn(div.innerHTML, '<font size="' + expected + '">foo</font>', + 'execCommand("fontSize", false, "' + input + '") should parse to ' + + expected); + } +} + +// Parse errors +parseFontSize("", null); +parseFontSize("abc", null); +parseFontSize("larger", null); +parseFontSize("smaller", null); +parseFontSize("xx-small", null); +parseFontSize("x-small", null); +parseFontSize("small", null); +parseFontSize("medium", null); +parseFontSize("large", null); +parseFontSize("x-large", null); +parseFontSize("xx-large", null); +parseFontSize("xxx-large", null); +// Bug 747879 +parseFontSizeTodo("1.2em", null); +parseFontSizeTodo("8px", null); +parseFontSizeTodo("-1.2em", null); +parseFontSizeTodo("-8px", null); +parseFontSizeTodo("+1.2em", null); +parseFontSizeTodo("+8px", null); + +// Numbers +parseFontSize("0", 1); +parseFontSize("1", 1); +parseFontSize("2", 2); +parseFontSize("3", 3); +parseFontSize("4", 4); +parseFontSize("5", 5); +parseFontSize("6", 6); +parseFontSize("7", 7); +parseFontSize("8", 7); +parseFontSize("9", 7); +parseFontSize("10", 7); +parseFontSize("1000000000000000000000", 7); +parseFontSize("2.72", 2); +parseFontSize("2.72e9", 2); + +// Minus sign +parseFontSize("-0", 3); +parseFontSize("-1", 2); +parseFontSize("-2", 1); +parseFontSize("-3", 1); +parseFontSize("-4", 1); +parseFontSize("-5", 1); +parseFontSize("-6", 1); +parseFontSize("-7", 1); +parseFontSize("-8", 1); +parseFontSize("-9", 1); +parseFontSize("-10", 1); +parseFontSize("-1000000000000000000000", 1); +parseFontSize("-1.72", 2); +parseFontSize("-1.72e9", 2); + +// Plus sign +parseFontSize("+0", 3); +parseFontSize("+1", 4); +parseFontSize("+2", 5); +parseFontSize("+3", 6); +parseFontSize("+4", 7); +parseFontSize("+5", 7); +parseFontSize("+6", 7); +parseFontSize("+7", 7); +parseFontSize("+8", 7); +parseFontSize("+9", 7); +parseFontSize("+10", 7); +parseFontSize("+1000000000000000000000", 7); +parseFontSize("+1.72", 4); +parseFontSize("+1.72e9", 4); + +// Whitespace +parseFontSize(" \t\n\r\f5 \t\n\r\f", 5); +parseFontSize("\u00a05", null); +parseFontSize("\b5", null); +</script> diff --git a/editor/libeditor/tests/test_bug480972.html b/editor/libeditor/tests/test_bug480972.html new file mode 100644 index 000000000..3eed97100 --- /dev/null +++ b/editor/libeditor/tests/test_bug480972.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 480972</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + +function runTest() { + function verifyContent(s) { + var e = document.getElementById('i1'); + var doc = e.contentDocument; + is(doc.body.innerHTML, s, ""); + } + + function pasteInto(html,target_id) { + var e = document.getElementById('i1'); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.body.innerHTML = html; + doc.defaultView.focus(); + if (target_id) + e = doc.getElementById(target_id); + else + e = doc.body; + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(e); + selection.collapseToEnd(); + SpecialPowers.wrap(doc).execCommand("paste", false, null); + return e; + } + + function copyToClipBoard(s,asHTML,target_id) { + var e = document.getElementById('i2'); + var doc = e.contentDocument; + if (asHTML) { + doc.body.innerHTML = s; + } else { + var text = doc.createTextNode(s); + doc.body.appendChild(text); + } + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + if (!target_id) { + selection.selectAllChildren(doc.body); + } else { + var range = document.createRange(); + range.selectNode(doc.getElementById(target_id)); + selection.addRange(range); + } + SpecialPowers.wrap(doc).execCommand("copy", false, null); + return e; + } + + copyToClipBoard('<span>Hello</span><span>Kitty</span>', true); + pasteInto(''); + verifyContent('<span>Hello</span><span>Kitty</span>'); + + copyToClipBoard("<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); + verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span></li></ol>'); + +// The following test doesn't do what I expected, because the special handling +// of IsList nodes in nsHTMLEditor::InsertHTMLWithContext simply removes +// non-list/item children. See bug 481177. +// copyToClipBoard("<ol><li>Hello Kitty</li><span>Hello</span></ol>", true); +// pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); +// verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li><span>Hello</span></ol>'); + + copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true); + pasteInto('<pre id="paste_here">Hello </pre>',"paste_here"); + verifyContent('<pre id="paste_here">Hello Kitty<span>Hello</span></pre>'); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=480972">Mozilla Bug 480972</a> +<p id="display"></p> + +<pre id="test"> +</pre> + + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> + diff --git a/editor/libeditor/tests/test_bug483651.html b/editor/libeditor/tests/test_bug483651.html new file mode 100644 index 000000000..ee256b807 --- /dev/null +++ b/editor/libeditor/tests/test_bug483651.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=483651 +--> + +<head> + <title>Test for Bug 483651</title> + <script src="/MochiKit/packed.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=483651">Mozilla Bug 483651</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 483651 **/ + + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + var editor = SpecialPowers.wrap(t1).editor; + + ok(editor, "able to get editor for the element"); + t1.focus(); + synthesizeKey("A", {}); + synthesizeKey("VK_BACK_SPACE", {}); + + try { + // Was the trailing br removed? + is(editor.documentIsEmpty, true, "trailing <br> correctly removed"); + } catch (e) { + ok(false, "test failed with error "+e); + } + SimpleTest.finish(); + } + </script> + </pre> + + <textarea id="t1" rows="2" columns="80"></textarea> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug484181.html b/editor/libeditor/tests/test_bug484181.html new file mode 100644 index 000000000..55cd8e806 --- /dev/null +++ b/editor/libeditor/tests/test_bug484181.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=484181 +--> +<head> + <title>Test for Bug 484181</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script src="spellcheck.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=484181">Mozilla Bug 484181</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 484181 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +var gMisspeltWords; + +function getEditor() { + var Ci = SpecialPowers.Ci; + var win = window; + var editingSession = SpecialPowers.wrap(win).QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + return editingSession.getEditorForWindow(win); +} + +function append(str) { + var edit = document.getElementById("edit"); + var editor = getEditor(); + var sel = editor.selection; + sel.selectAllChildren(edit); + sel.collapseToEnd(); + + for (var i = 0; i < str.length; ++i) { + synthesizeKey(str[i], {}); + } +} + +function runTest() { + gMisspeltWords = ["haz", "cheezburger"]; + var edit = document.getElementById("edit"); + edit.focus(); + + SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", window); + onSpellCheck(edit, function () { + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "All misspellings before editing are accounted for."); + + append(" becaz I'm a lulcat!"); + onSpellCheck(edit, function () { + gMisspeltWords.push("becaz"); + gMisspeltWords.push("lulcat"); + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "All misspellings after typing are accounted for."); + + SimpleTest.finish(); + }); + }); +} +</script> +</pre> + +<div><div></div><div id="edit" contenteditable="true">I can haz cheezburger</div></div> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug487524.html b/editor/libeditor/tests/test_bug487524.html new file mode 100644 index 000000000..d4972ba91 --- /dev/null +++ b/editor/libeditor/tests/test_bug487524.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 487524</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> +function runTest() { + + function setupIframe(e,html,focus_id) { + var doc = e.contentDocument; + doc.body.innerHTML = html; + doc.designMode = "on"; + e = doc.getElementById(focus_id); + doc.defaultView.focus(); + if (e) e.focus(); + return e; + } + + var i1 = document.getElementById('i1') + var li1 = setupIframe(i1,'<ul><li id="li1">one</li><li>two</li><ul><li>a</li></ul></ul>','li1') + var doc = li1.ownerDocument; + + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + + var range = doc.createRange(); + range.setStart(li1,0); + range.setEnd(li1.nextSibling,0); + selection.addRange(range); + + sendKey('delete'); + is(doc.body.innerHTML,'<ul><li>two</li><ul><li>a</li></ul></ul>','delete 1st LI'); + + var li2 = setupIframe(i1,'<ul><li id="li2">two</li><ul><li>a</li></ul></ul>','li2') + selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + + range = doc.createRange(); + range.setStart(li2,0); + range.setEnd(li2.nextSibling.firstChild,0); + selection.addRange(range); + + sendKey('delete'); + is(doc.body.innerHTML,'<ul><ul><li>a</li></ul></ul>','delete 2nd LI'); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=487524">Mozilla Bug 487524</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug489202.xul b/editor/libeditor/tests/test_bug489202.xul new file mode 100644 index 000000000..30e2a730d --- /dev/null +++ b/editor/libeditor/tests/test_bug489202.xul @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=489202 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 489202" onload="runTest();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=489202" + target="_blank">Mozilla Bug 489202</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="i1" + type="content" + editortype="htmlmail" + style="width: 400px; height: 100px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + var utils = SpecialPowers.getDOMWindowUtils(window); + var Cc = Components.classes; + var Ci = Components.interfaces; + +function getLoadContext() { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); +} + +function runTest() { + var trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + trans.addDataFlavor("text/html"); + var test_data = '<meta/><a href="http://mozilla.org/">mozilla.org</a>'; + var cstr = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + cstr.data = test_data; + trans.setTransferData("text/html", cstr, test_data.length*2); + + window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Components.interfaces.nsIDocShell) + .appType = Components.interfaces.nsIDocShell.APP_TYPE_EDITOR; + var e = document.getElementById('i1'); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.body.innerHTML = ""; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(doc.body); + selection.collapseToEnd(); + + var point = doc.defaultView.getSelection().getRangeAt(0).startOffset; + ok(point==0, "Cursor should be at editor start before paste"); + + utils.sendContentCommandEvent("pasteTransferable", trans); + + point = doc.defaultView.getSelection().getRangeAt(0).startOffset; + ok(point>0, "Cursor should not be at editor start after paste"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug490879.html b/editor/libeditor/tests/test_bug490879.html new file mode 100644 index 000000000..1e412a7d6 --- /dev/null +++ b/editor/libeditor/tests/test_bug490879.html @@ -0,0 +1,45 @@ +<!doctype html> +<title>Mozilla Bug 490879</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=490879" + target="_blank">Mozilla Bug 490879</a> +<iframe id="i1" width="200" height="100" src="about:blank"></iframe> +<img id="i" src="green.png"> +<script> +function runTest() { + function verifyContent() { + const kExpectedImgSpec = "data:image/png;base64,"; + var e = document.getElementById('i1'); + var doc = e.contentDocument; + is(doc.getElementsByTagName("img")[0].src.substring(0, kExpectedImgSpec.length), + kExpectedImgSpec, "The pasted image is a base64-encoded data: URI"); + } + + function pasteInto() { + var e = document.getElementById('i1'); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(doc.body); + selection.collapseToEnd(); + SpecialPowers.doCommand(window, "cmd_paste"); + } + + function copyToClipBoard() { + SpecialPowers.setCommandNode(window, document.getElementById("i")); + SpecialPowers.doCommand(window, "cmd_copyImageContents"); + } + + copyToClipBoard(); + pasteInto(); + verifyContent(); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> diff --git a/editor/libeditor/tests/test_bug502673.html b/editor/libeditor/tests/test_bug502673.html new file mode 100644 index 000000000..3bee4554a --- /dev/null +++ b/editor/libeditor/tests/test_bug502673.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=502673 +--> + +<head> + <title>Test for Bug 502673</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502673">Mozilla Bug 502673</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 502673 **/ + + SimpleTest.waitForExplicitFinish(); + + function listener() { + } + + listener.prototype = + { + NotifyDocumentWillBeDestroyed: function () { + if (this.input instanceof SpecialPowers.Ci.nsIDOMNSEditableElement) { + var editor = SpecialPowers.wrap(this.input).editor; + editor.removeDocumentStateListener(this); + } + }, + + NotifyDocumentCreated: function () { + }, + + NotifyDocumentStateChanged: function (aNowDirty) { + if (this.input instanceof SpecialPowers.Ci.nsIDOMNSEditableElement) { + var editor = SpecialPowers.wrap(this.input).editor; + editor.removeDocumentStateListener(this); + } + }, + + QueryInterface: SpecialPowers.wrapCallback(function(iid) { + if (iid.equals(SpecialPowers.Ci.nsIDocumentStateListener) || + iid.equals(SpecialPowers.Ci.nsISupports)) + return this; + throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE; + }), + }; + + function doTest() { + var input = document.getElementById("ip"); + if (input instanceof SpecialPowers.Ci.nsIDOMNSEditableElement) { + // Add multiple listeners to the same editor + var editor = SpecialPowers.wrap(input).editor; + var listener1 = new listener(); + listener1.input = input; + var listener2 = new listener(); + listener2.input = input; + var listener3 = new listener(); + listener3.input = input; + editor.addDocumentStateListener(listener1); + editor.addDocumentStateListener(listener2); + editor.addDocumentStateListener(listener3); + + // Test 1. Fire NotifyDocumentStateChanged notifications where the + // listeners remove themselves + input.value = "mozilla"; + editor.undo(1); + + // Report success if we get here - clearly we didn't crash + ok(true, "Multiple listeners removed themselves after " + + "NotifyDocumentStateChanged notifications - didn't crash"); + + // Add the listeners again for the next test + editor.addDocumentStateListener(listener1); + editor.addDocumentStateListener(listener2); + editor.addDocumentStateListener(listener3); + + } + + // Test 2. Fire NotifyDocumentWillBeDestroyed notifications where the + // listeners remove themselves (though in the real world, listeners + // shouldn't do this as nsEditor::PreDestroy removes them as + // listeners anyway) + document.body.removeChild(input); + ok(true, "Multiple listeners removed themselves after " + + "NotifyDocumentWillBeDestroyed notifications - didn't crash"); + + // TODO: Test for NotifyDocumentCreated + + SimpleTest.finish(); + } + </script> + </pre> + + <input type="text" id="ip" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug514156.html b/editor/libeditor/tests/test_bug514156.html new file mode 100644 index 000000000..3594d1c8d --- /dev/null +++ b/editor/libeditor/tests/test_bug514156.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=514156 +--> +<head> + <title>Test for Bug 514156</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="test()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514156">Mozilla Bug 514156</a> +<p id="display"></p> +<div id="content"> +<input type="text" id="input1"> +<input type="text" id="input2"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 514156 **/ + +SimpleTest.waitForExplicitFinish(); + +function test() { + var input1 = $("input1"); + input1.focus(); + synthesizeKey("\u200e", { }); + synthesizeKey("\u05d0", { }); + synthesizeKey("\u05d1", { }); + is(escape(input1.value), escape("\u200e\u05d0\u05d1"), "non-spacing character and direction change shouldn't change content"); + + var input2 = $("input2"); + input2.focus(); + synthesizeKey("\u05b6", { }); + synthesizeKey("a", { }); + synthesizeKey("b", { }); + synthesizeKey("c", { }); + is(escape(input2.value), escape("\u05b6abc"), "non-spacing character and direction change shouldn't change content"); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> + diff --git a/editor/libeditor/tests/test_bug520189.html b/editor/libeditor/tests/test_bug520189.html new file mode 100644 index 000000000..d1b429000 --- /dev/null +++ b/editor/libeditor/tests/test_bug520189.html @@ -0,0 +1,621 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=520182 +--> +<head> + <title>Test for Bug 520182</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=520182">Mozilla Bug 520182</a> +<p id="display"></p> +<div id="content"> + <iframe id="a" src="about:blank"></iframe> + <iframe id="b" src="about:blank"></iframe> + <iframe id="c" src="about:blank"></iframe> + <div id="d" contenteditable="true"></div> + <div id="e" contenteditable="true"></div> + <div id="f" contenteditable="true"></div> + <iframe id="g" src="about:blank"></iframe> + <iframe id="h" src="about:blank"></iframe> + <div id="i" contenteditable="true"></div> + <div id="j" contenteditable="true"></div> + <iframe id="k" src="about:blank"></iframe> + <div id="l" contenteditable="true"></div> + <iframe id="m" src="about:blank"></iframe> + <div id="n" contenteditable="true"></div> + <iframe id="o" src="about:blank"></iframe> + <div id="p" contenteditable="true"></div> + <iframe id="q" src="about:blank"></iframe> + <div id="r" contenteditable="true"></div> + <iframe id="s" src="about:blank"></iframe> + <div id="t" contenteditable="true"></div> + <iframe id="u" src="about:blank"></iframe> + <div id="v" contenteditable="true"></div> + <iframe id="w" src="about:blank"></iframe> + <div id="x" contenteditable="true"></div> + <iframe id="y" src="about:blank"></iframe> + <div id="z" contenteditable="true"></div> + <iframe id="aa" src="about:blank"></iframe> + <div id="bb" contenteditable="true"></div> + <iframe id="cc" src="about:blank"></iframe> + <div id="dd" contenteditable="true"></div> + <iframe id="ee" src="about:blank"></iframe> + <div id="ff" contenteditable="true"></div> + <iframe id="gg" src="about:blank"></iframe> + <div id="hh" contenteditable="true"></div> + <iframe id="ii" src="about:blank"></iframe> + <div id="jj" contenteditable="true"></div> + <iframe id="kk" src="about:blank"></iframe> + <div id="ll" contenteditable="true"></div> + <iframe id="mm" src="about:blank"></iframe> + <div id="nn" contenteditable="true"></div> + <iframe id="oo" src="about:blank"></iframe> + <div id="pp" contenteditable="true"></div> + <iframe id="qq" src="about:blank"></iframe> + <div id="rr" contenteditable="true"></div> + <iframe id="ss" src="about:blank"></iframe> + <div id="tt" contenteditable="true"></div> + <iframe id="uu" src="about:blank"></iframe> + <div id="vv" contenteditable="true"></div> + <div id="sss" contenteditable="true"></div> + <iframe id="ssss" src="about:blank"></iframe> + <div id="ttt" contenteditable="true"></div> + <iframe id="tttt" src="about:blank"></iframe> + <div id="uuu" contenteditable="true"></div> + <iframe id="uuuu" src="about:blank"></iframe> + <div id="vvv" contenteditable="true"></div> + <iframe id="vvvv" src="about:blank"></iframe> + <div id="www" contenteditable="true"></div> + <iframe id="wwww" src="about:blank"></iframe> + <div id="xxx" contenteditable="true"></div> + <iframe id="xxxx" src="about:blank"></iframe> + <div id="yyy" contenteditable="true"></div> + <iframe id="yyyy" src="about:blank"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 520182 **/ + +const dataPayload = "foo<iframe src=\"data:text/html,bar\"></iframe>baz"; +const jsPayload = "foo<iframe src=\"javascript:void('bar');\"></iframe>baz"; +const httpPayload = "foo<iframe src=\"http://mochi.test:8888/\"></iframe>baz"; +const scriptPayload ="foo<script>document.write(\"<iframe></iframe>\");</sc" + "ript>baz"; +const scriptExternalPayload = "foo<script src=\"data:text/javascript,document.write('<iframe></iframe>');\"></sc" + "ript>baz"; +const validStyle1Payload = "foo<style>#bar{color:red;}</style>baz"; +const validStyle2Payload = "foo<span style=\"color:red\">bar</span>baz"; +const validStyle3Payload = "foo<style>@font-face{font-family:xxx;src:'xxx.ttf';}</style>baz"; +const validStyle4Payload = "foo<style>@namespace xxx url(http://example.com/);</style>baz"; +const invalidStyle1Payload = "foo<style>#bar{-moz-binding:url('data:text/xml,<?xml version=\"1.0\"><binding xmlns=\"http://www.mozilla.org/xbl\"/>');}</style>baz"; +const invalidStyle2Payload = "foo<span style=\"-moz-binding:url('data:text/xml,<?xml version="1.0"><binding xmlns="http://www.mozilla.org/xbl"/>');\">bar</span>baz"; +const invalidStyle3Payload = "foo<style>@import 'xxx.css';</style>baz"; +const invalidStyle4Payload = "foo<span style=\"@import 'xxx.css';\">bar</span>baz"; +const invalidStyle5Payload = "foo<span style=\"@font-face{font-family:xxx;src:'xxx.ttf';}\">bar</span>baz"; +const invalidStyle6Payload = "foo<span style=\"@namespace xxx url(http://example.com/);\">bar</span>baz"; +const invalidStyle7Payload = "<html><head><title>xxx</title></head><body>foo</body></html>"; +const invalidStyle8Payload = "foo<style>@-moz-document url(http://example.com/) {};</style>baz"; +const invalidStyle9Payload = "foo<style>@-moz-keyframes bar {};</style>baz"; +const nestedStylePayload = "foo<style>#bar1{-moz-binding:url('data:text/xml,<?xml version="1.0"><binding xmlns="http://www.mozilla.org/xbl" id="binding-1"/>');<style></style>#bar2{-moz-binding:url('data:text/xml,<?xml version="1.0"><binding xmlns="http://www.mozilla.org/xbl" id="binding-2"/>');</style>baz"; +const validImgSrc1Payload = "foo<img src=\"data:image/png,bar\">baz"; +const validImgSrc2Payload = "foo<img src=\"javascript:void('bar');\">baz"; +const validImgSrc3Payload = "foo<img src=\"file:///bar.png\">baz"; +const validDataFooPayload = "foo<span data-bar=\"value\">baz</span>"; +const validDataFoo2Payload = "foo<span _bar=\"value\">baz</span>"; +const svgPayload = "foo<svg><title>svgtitle</title></svg>bar"; +const svg2Payload = "foo<svg><bogussvg/></svg>bar"; +const mathPayload = "foo<math><bogusmath/></math>bar"; +const math2Payload = "foo<math><style>@import \"yyy.css\";</style</math>bar"; +const math3Payload = "foo<math><mi></mi></math>bar"; +const videoPayload = "foo<video></video>bar"; +const microdataPayload = "<head><meta name=foo content=bar><link rel=stylesheet href=url></head><body><meta itemprop=foo content=bar><link itemprop=bar href=url></body>"; + +var tests = [ + { + id: "a", + isIFrame: true, + payload: dataPayload, + iframeCount: 0, + rootElement() { return document.getElementById("a").contentDocument.documentElement; }, + }, + { + id: "b", + isIFrame: true, + payload: jsPayload, + iframeCount: 0, + rootElement() { return document.getElementById("b").contentDocument.documentElement; }, + }, + { + id: "c", + isIFrame: true, + payload: httpPayload, + iframeCount: 0, + rootElement() { return document.getElementById("c").contentDocument.documentElement; }, + }, + { + id: "g", + isIFrame: true, + payload: scriptPayload, + rootElement() { return document.getElementById("g").contentDocument.documentElement; }, + iframeCount: 0 + }, + { + id: "h", + isIFrame: true, + payload: scriptExternalPayload, + rootElement() { return document.getElementById("h").contentDocument.documentElement; }, + iframeCount: 0 + }, + { + id: "d", + payload: dataPayload, + iframeCount: 0, + rootElement() { return document.getElementById("d"); }, + }, + { + id: "e", + payload: jsPayload, + iframeCount: 0, + rootElement() { return document.getElementById("e"); }, + }, + { + id: "f", + payload: httpPayload, + iframeCount: 0, + rootElement() { return document.getElementById("f"); }, + }, + { + id: "i", + payload: scriptPayload, + rootElement() { return document.getElementById("i"); }, + iframeCount: 0 + }, + { + id: "j", + payload: scriptExternalPayload, + rootElement() { return document.getElementById("j"); }, + iframeCount: 0 + }, + { + id: "k", + isIFrame: true, + payload: validStyle1Payload, + rootElement() { return document.getElementById("k").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); }, + }, + { + id: "l", + payload: validStyle1Payload, + rootElement() { return document.getElementById("l"); }, + checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); }, + }, + { + id: "m", + isIFrame: true, + payload: validStyle2Payload, + rootElement() { return document.getElementById("m").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); }, + }, + { + id: "n", + payload: validStyle2Payload, + rootElement() { return document.getElementById("n"); }, + checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); }, + }, + { + id: "o", + isIFrame: true, + payload: invalidStyle1Payload, + rootElement() { return document.getElementById("o").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("binding"), -1, "Should not have retained the binding style"); }, + }, + { + id: "p", + payload: invalidStyle1Payload, + rootElement() { return document.getElementById("p"); }, + checkResult(html) { is(html.indexOf("binding"), -1, "Should not have retained the binding style"); }, + }, + { + id: "q", + isIFrame: true, + payload: invalidStyle2Payload, + rootElement() { return document.getElementById("q").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("binding"), -1, "Should not have retained the binding style"); }, + }, + { + id: "r", + payload: invalidStyle2Payload, + rootElement() { return document.getElementById("r"); }, + checkResult(html) { is(html.indexOf("binding"), -1, "Should not have retained the binding style"); }, + }, + { + id: "s", + isIFrame: true, + payload: invalidStyle1Payload, + rootElement() { return document.getElementById("s").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); }, + }, + { + id: "t", + payload: invalidStyle1Payload, + rootElement() { return document.getElementById("t"); }, + checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); }, + }, + { + id: "u", + isIFrame: true, + payload: invalidStyle2Payload, + rootElement() { return document.getElementById("u").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); }, + }, + { + id: "v", + payload: invalidStyle2Payload, + rootElement() { return document.getElementById("v"); }, + checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); }, + }, + { + id: "w", + isIFrame: true, + payload: validStyle3Payload, + rootElement() { return document.getElementById("w").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the font-face style"); }, + }, + { + id: "x", + payload: validStyle3Payload, + rootElement() { return document.getElementById("x"); }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the font-face style"); }, + }, + { + id: "y", + isIFrame: true, + payload: invalidStyle5Payload, + rootElement() { return document.getElementById("y").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the font-face style"); }, + }, + { + id: "z", + payload: invalidStyle5Payload, + rootElement() { return document.getElementById("z"); }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the font-face style"); }, + }, + { + id: "aa", + isIFrame: true, + payload: nestedStylePayload, + rootElement() { return document.getElementById("aa").contentDocument.documentElement; }, + checkResult: function(html, text) { + is(html.indexOf("binding-1"), -1, "Should not have retained the binding-1 style"); + isnot(text.indexOf("#bar2"), -1, "Should have retained binding-2 as text content"); + is(text.indexOf("binding-2"), -1, "Should not have retained binding-2 as a tag"); + } + }, + { + id: "bb", + payload: nestedStylePayload, + rootElement() { return document.getElementById("bb"); }, + checkResult: function(html, text) { + is(html.indexOf("binding-1"), -1, "Should not have retained the binding-1 style"); + isnot(text.indexOf("#bar2"), -1, "Should have retained binding-2 as text content"); + is(text.indexOf("binding-2"), -1, "Should not have retained binding-2 as a tag"); + } + }, + { + id: "cc", + isIFrame: true, + payload: validStyle4Payload, + rootElement() { return document.getElementById("cc").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the namespace style"); }, + }, + { + id: "dd", + payload: validStyle4Payload, + rootElement() { return document.getElementById("dd"); }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the namespace style"); }, + }, + { + id: "ee", + isIFrame: true, + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("ee").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the namespace style"); }, + }, + { + id: "ff", + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("ff"); }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the namespace style"); }, + }, + { + id: "gg", + isIFrame: true, + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("gg").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "hh", + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("hh"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "ii", + isIFrame: true, + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("ii").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "jj", + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("jj"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "kk", + isIFrame: true, + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("kk").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "ll", + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("ll"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "mm", + isIFrame: true, + indirectPaste: true, + payload: invalidStyle7Payload, + rootElement() { return document.getElementById("mm").contentDocument.documentElement; }, + checkResult: function(html) { + is(html.indexOf("xxx"), -1, "Should not have retained the title text"); + isnot(html.indexOf("foo"), -1, "Should have retained the body text"); + } + }, + { + id: "nn", + indirectPaste: true, + payload: invalidStyle7Payload, + rootElement() { return document.getElementById("nn"); }, + checkResult: function(html) { + is(html.indexOf("xxx"), -1, "Should not have retained the title text"); + isnot(html.indexOf("foo"), -1, "Should have retained the body text"); + } + }, + { + id: "oo", + isIFrame: true, + payload: validDataFooPayload, + rootElement() { return document.getElementById("oo").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the data-bar attribute"); }, + }, + { + id: "pp", + payload: validDataFooPayload, + rootElement() { return document.getElementById("pp"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the data-bar attribute"); }, + }, + { + id: "qq", + isIFrame: true, + payload: validDataFoo2Payload, + rootElement() { return document.getElementById("qq").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the _bar attribute"); }, + }, + { + id: "rr", + payload: validDataFoo2Payload, + rootElement() { return document.getElementById("rr"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the _bar attribute"); }, + }, + { + id: "ss", + isIFrame: true, + payload: invalidStyle8Payload, + rootElement() { return document.getElementById("ss").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("@-moz-document"), -1, "Should not have retained the @-moz-document rule"); }, + }, + { + id: "tt", + payload: invalidStyle8Payload, + rootElement() { return document.getElementById("tt"); }, + checkResult(html) { is(html.indexOf("@-moz-document"), -1, "Should not have retained the @-moz-document rule"); }, + }, + { + id: "uu", + isIFrame: true, + payload: invalidStyle9Payload, + rootElement() { return document.getElementById("uu").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("@-moz-keyframes"), -1, "Should not have retained the @-moz-keyframes rule"); }, + }, + { + id: "vv", + payload: invalidStyle9Payload, + rootElement() { return document.getElementById("vv"); }, + checkResult(html) { is(html.indexOf("@-moz-keyframes"), -1, "Should not have retained the @-moz-keyframes rule"); }, + }, + { + id: "sss", + payload: svgPayload, + rootElement() { return document.getElementById("sss"); }, + checkResult(html) { isnot(html.indexOf("svgtitle"), -1, "Should have retained SVG title"); }, + }, + { + id: "ssss", + isIFrame: true, + payload: svgPayload, + rootElement() { return document.getElementById("ssss").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("svgtitle"), -1, "Should have retained SVG title"); }, + }, + { + id: "ttt", + payload: svg2Payload, + rootElement() { return document.getElementById("ttt"); }, + checkResult(html) { is(html.indexOf("bogussvg"), -1, "Should have dropped bogussvg element"); }, + }, + { + id: "tttt", + isIFrame: true, + payload: svg2Payload, + rootElement() { return document.getElementById("tttt").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("bogussvg"), -1, "Should have dropped bogussvg element"); }, + }, + { + id: "uuu", + payload: mathPayload, + rootElement() { return document.getElementById("uuu"); }, + checkResult(html) { is(html.indexOf("bogusmath"), -1, "Should have dropped bogusmath element"); }, + }, + { + id: "uuuu", + isIFrame: true, + payload: mathPayload, + rootElement() { return document.getElementById("uuuu").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("bogusmath"), -1, "Should have dropped bogusmath element"); }, + }, + { + id: "vvv", + payload: math2Payload, + rootElement() { return document.getElementById("vvv"); }, + checkResult(html) { is(html.indexOf("yyy.css"), -1, "Should have dropped MathML style element"); }, + }, + { + id: "vvvv", + isIFrame: true, + payload: math2Payload, + rootElement() { return document.getElementById("vvvv").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("yyy.css"), -1, "Should have dropped MathML style element"); }, + }, + { + id: "www", + payload: math3Payload, + rootElement() { return document.getElementById("www"); }, + checkResult(html) { isnot(html.indexOf("<mi"), -1, "Should not have dropped MathML mi element"); }, + }, + { + id: "wwww", + isIFrame: true, + payload: math3Payload, + rootElement() { return document.getElementById("wwww").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("<mi"), -1, "Should not have dropped MathML mi element"); }, + }, + { + id: "xxx", + payload: videoPayload, + rootElement() { return document.getElementById("xxx"); }, + checkResult(html) { isnot(html.indexOf("controls="), -1, "Should have added the controls attribute"); }, + }, + { + id: "xxxx", + isIFrame: true, + payload: videoPayload, + rootElement() { return document.getElementById("xxxx").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("controls="), -1, "Should have added the controls attribute"); }, + }, + { + id: "yyy", + payload: microdataPayload, + rootElement() { return document.getElementById("yyy"); }, + checkResult: function(html) { is(html.indexOf("name"), -1, "Should have dropped name."); is(html.indexOf("rel"), -1, "Should have dropped rel."); isnot(html.indexOf("itemprop"), -1, "Should not have dropped itemprop."); } + }, + { + id: "yyyy", + isIFrame: true, + payload: microdataPayload, + rootElement() { return document.getElementById("yyyy").contentDocument.documentElement; }, + checkResult: function(html) { is(html.indexOf("name"), -1, "Should have dropped name."); is(html.indexOf("rel"), -1, "Should have dropped rel."); isnot(html.indexOf("itemprop"), -1, "Should not have dropped itemprop."); } + } +]; + +function doNextTest() { + if (typeof testCounter == "undefined") + testCounter = 0; + else if (++testCounter == tests.length) { + SimpleTest.finish(); + return; + } + + runTest(tests[testCounter]); + + doNextTest(); +} + +function getLoadContext() { + const Ci = SpecialPowers.Ci; + return SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); +} + +function runTest(test) { + var elem = document.getElementById(test.id); + if ("isIFrame" in test) { + elem.contentDocument.designMode = "on"; + elem.contentWindow.focus(); + } else + elem.focus(); + + var trans = SpecialPowers.Cc["@mozilla.org/widget/transferable;1"] + .createInstance(SpecialPowers.Ci.nsITransferable); + trans.init(getLoadContext()); + var data = SpecialPowers.Cc["@mozilla.org/supports-string;1"] + .createInstance(SpecialPowers.Ci.nsISupportsString); + data.data = test.payload; + trans.addDataFlavor("text/html"); + trans.setTransferData("text/html", data, data.data.length * 2); + + if ("indirectPaste" in test) { + var editor, win; + if ("isIFrame" in test) { + win = elem.contentDocument.defaultView; + } else { + getSelection().collapse(elem, 0); + win = window; + } + editor = SpecialPowers.wrap(win).QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsIWebNavigation) + .QueryInterface(SpecialPowers.Ci.nsIDocShell) + .editor; + editor.pasteTransferable(trans); + } else { + var clipboard = SpecialPowers.Cc["@mozilla.org/widget/clipboard;1"] + .getService(SpecialPowers.Ci.nsIClipboard); + + clipboard.setData(trans, null, SpecialPowers.Ci.nsIClipboard.kGlobalClipboard); + + synthesizeKey("V", {accelKey: true}); + } + + if ("checkResult" in test) { + if ("isIFrame" in test) { + test.checkResult(elem.contentDocument.documentElement.innerHTML, + elem.contentDocument.documentElement.textContent); + } else { + test.checkResult(elem.innerHTML, elem.textContent); + } + } else { + var iframes = test.rootElement().querySelectorAll("iframe"); + var expectedIFrameCount = ("iframeCount" in test) ? test.iframeCount : 1; + is(iframes.length, expectedIFrameCount, "Only " + expectedIFrameCount + " iframe should be pasted"); + if (expectedIFrameCount > 0) { + ok(!iframes[0].hasAttribute("src"), "iframe should not have a src attrib"); + } + } +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doNextTest); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug525389.html b/editor/libeditor/tests/test_bug525389.html new file mode 100644 index 000000000..43916eb51 --- /dev/null +++ b/editor/libeditor/tests/test_bug525389.html @@ -0,0 +1,198 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 525389</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + + var utils = SpecialPowers.wrap(window) + .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).getInterface(SpecialPowers.Ci.nsIDOMWindowUtils); + var Cc = SpecialPowers.Cc; + var Ci = SpecialPowers.Ci; + +function getLoadContext() { + return SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); +} + +function runTest() { + var pasteCount = 0; + var pasteFunc = function (event) { pasteCount++; }; + + function verifyContent(s) { + var e = document.getElementById('i1'); + var doc = e.contentDocument; + if (navigator.platform.indexOf("Win") >= 0) { + // On Windows ignore \n which got left over from the removal of the fragment tags + // <html><body>\n<!--StartFragment--> and <!--EndFragment-->\n</body>\n</html>. + is(doc.body.innerHTML.replace(/\n/g, ""), s, ""); + } else { + is(doc.body.innerHTML, s, ""); + } + } + + function pasteInto(trans, html, target_id) { + var e = document.getElementById('i1'); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.body.innerHTML = html; + doc.defaultView.focus(); + if (target_id) + e = doc.getElementById(target_id); + else + e = doc.body; + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(e); + selection.collapseToEnd(); + + pasteCount = 0; + e.addEventListener("paste", pasteFunc, false); + utils.sendContentCommandEvent("pasteTransferable", trans); + e.removeEventListener("paste", pasteFunc, false); + + return e; + } + + function getTransferableFromClipboard(asHTML) { + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + if (asHTML) { + trans.addDataFlavor("text/html"); + } else { + trans.addDataFlavor("text/unicode"); + } + var clip = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); + clip.getData(trans, Ci.nsIClipboard.kGlobalClipboard); + return trans; + } + + function makeTransferable(s,asHTML,target_id) { + var e = document.getElementById('i2'); + var doc = e.contentDocument; + if (asHTML) { + doc.body.innerHTML = s; + } else { + var text = doc.createTextNode(s); + doc.body.appendChild(text); + } + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + if (!target_id) { + selection.selectAllChildren(doc.body); + } else { + var range = document.createRange(); + range.selectNode(doc.getElementById(target_id)); + selection.addRange(range); + } + + // We cannot use plain strings, we have to use nsSupportsString. + var supportsStringClass = SpecialPowers.Components.classes["@mozilla.org/supports-string;1"]; + var ssData = supportsStringClass.createInstance(Ci.nsISupportsString); + + // Create the transferable. + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + + // Add the data to the transferable. + if (asHTML) { + trans.addDataFlavor("text/html"); + ssData.data = doc.body.innerHTML; + trans.setTransferData("text/html", ssData, ssData.length * 2); + } else { + trans.addDataFlavor("text/unicode"); + ssData.data = doc.body.innerHTML; + trans.setTransferData("text/unicode", ssData, ssData.length * 2); + } + + return trans; + } + + function copyToClipBoard(s,asHTML,target_id) { + var e = document.getElementById('i2'); + var doc = e.contentDocument; + if (asHTML) { + doc.body.innerHTML = s; + } else { + var text = doc.createTextNode(s); + doc.body.appendChild(text); + } + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + if (!target_id) { + selection.selectAllChildren(doc.body); + } else { + var range = document.createRange(); + range.selectNode(doc.getElementById(target_id)); + selection.addRange(range); + } + SpecialPowers.wrap(doc).execCommand("copy", false, null); + return e; + } + + copyToClipBoard('<span>Hello</span><span>Kitty</span>', true); + var trans = getTransferableFromClipboard(true); + pasteInto(trans, ''); + verifyContent('<span>Hello</span><span>Kitty</span>'); + is(pasteCount, 1, "paste event was not triggered"); + + // this test is not working out exactly like the clipboard test + // has to do with generating the nsITransferable above + //trans = makeTransferable('<span>Hello</span><span>Kitty</span>', true); + //pasteInto(trans, ''); + //verifyContent('<span>Hello</span><span>Kitty</span>'); + + copyToClipBoard("<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span>", true); + trans = getTransferableFromClipboard(true); + pasteInto(trans, '<ol><li id="paste_here">X</li></ol>',"paste_here"); + verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span></li></ol>'); + is(pasteCount, 1, "paste event was not triggered"); + +// The following test doesn't do what I expected, because the special handling +// of IsList nodes in nsHTMLEditor::InsertHTMLWithContext simply removes +// non-list/item children. See bug 481177. +// copyToClipBoard("<ol><li>Hello Kitty</li><span>Hello</span></ol>", true); +// pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); +// verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li><span>Hello</span></ol>'); + + copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true); + trans = getTransferableFromClipboard(true); + pasteInto(trans, '<pre id="paste_here">Hello </pre>',"paste_here"); + verifyContent('<pre id="paste_here">Hello Kitty<span>Hello</span></pre>'); + is(pasteCount, 1, "paste event was not triggered"); + + // test that we can preventDefault pastes + pasteFunc = function (event) { event.preventDefault(); return false; }; + copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true); + trans = getTransferableFromClipboard(true); + pasteInto(trans, '<pre id="paste_here">Hello </pre>',"paste_here"); + verifyContent('<pre id="paste_here">Hello </pre>'); + is(pasteCount, 0, "paste event was triggered"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=525389">Mozilla Bug 525389</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug537046.html b/editor/libeditor/tests/test_bug537046.html new file mode 100644 index 000000000..6c3c07b29 --- /dev/null +++ b/editor/libeditor/tests/test_bug537046.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=537046 +--> +<head> + <title>Test for Bug 537046</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=537046">Mozilla Bug 537046</a> +<p id="display"></p> +<div id="content"> + <div id="editor" contenteditable="true"> + Some editable content + </div> + <div id="source" contenteditable="true"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 537046 **/ + +SimpleTest.expectAssertions(1); + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var ed = document.getElementById("editor"); + var src = document.getElementById("source"); + ed.addEventListener("DOMSubtreeModified", function() { + src.textContent = ed.innerHTML; + }, false); + src.addEventListener("DOMSubtreeModified", function() { + ed.innerHTML = ed.textContent; + }, false); + + // Simulate pressing Enter twice + ed.focus(); + synthesizeKey("VK_RETURN", {}); + synthesizeKey("VK_RETURN", {}); + + ok(true, "Didn't crash!"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug549262.html b/editor/libeditor/tests/test_bug549262.html new file mode 100644 index 000000000..fa1cbabc4 --- /dev/null +++ b/editor/libeditor/tests/test_bug549262.html @@ -0,0 +1,132 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=549262 +--> +<head> + <title>Test for Bug 549262</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549262">Mozilla Bug 549262</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 549262 **/ + +var smoothScrollPref = "general.smoothScroll"; +SimpleTest.waitForExplicitFinish(); +var win = window.open("file_bug549262.html", "_blank", + "width=600,height=600,scrollbars=yes"); + +// grab the timer right at the start +var cwu = SpecialPowers.getDOMWindowUtils(win); +function step() { + cwu.advanceTimeAndRefresh(100); +} +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[[smoothScrollPref, false]]}, startTest); +}, win); +function startTest() { + // Make sure that pressing Space when a contenteditable element is not focused + // will scroll the page. + var ed = win.document.getElementById("editor"); + var sc = win.document.querySelector("a"); + sc.focus(); + is(win.scrollY, 0, "Sanity check"); + synthesizeKey(" ", {}, win); + + step(); + + isnot(win.scrollY, 0, "Page is scrolled down"); + is(ed.textContent, "abc", "The content of the editable element has not changed"); + var oldY = win.scrollY; + synthesizeKey(" ", {shiftKey: true}, win); + + step(); + + ok(win.scrollY < oldY, "Page is scrolled up"); + is(ed.textContent, "abc", "The content of the editable element has not changed"); + + // Make sure that pressing Space when a contenteditable element is focused + // will not scroll the page, and will edit the element. + ed.focus(); + win.getSelection().collapse(ed.firstChild, 1); + oldY = win.scrollY; + synthesizeKey(" ", {}, win); + + step(); + + ok(win.scrollY <= oldY, "Page is not scrolled down"); + is(ed.textContent, "a bc", "The content of the editable element has changed"); + sc.focus(); + synthesizeKey(" ", {}, win); + + step(); + + isnot(win.scrollY, 0, "Page is scrolled down"); + is(ed.textContent, "a bc", "The content of the editable element has not changed"); + ed.focus(); + win.getSelection().collapse(ed.firstChild, 3); + synthesizeKey(" ", {shiftKey: true}, win); + + step(); + + isnot(win.scrollY, 0, "Page is not scrolled up"); + is(ed.textContent, "a b c", "The content of the editable element has changed"); + + // Now let's test the down/up keys + sc = document.body; + + step(); + + ed.blur(); + sc.focus(); + oldY = win.scrollY; + synthesizeKey("VK_UP", {}, win); + + step(); + + ok(win.scrollY < oldY, "Page is scrolled up"); + oldY = win.scrollY; + ed.focus(); + win.getSelection().collapse(ed.firstChild, 3); + synthesizeKey("VK_UP", {}, win); + + step(); + + is(win.scrollY, oldY, "Page is not scrolled up"); + is(win.getSelection().focusNode, ed.firstChild, "Correct element selected"); + is(win.getSelection().focusOffset, 0, "Selection should be moved to the beginning"); + win.getSelection().removeAllRanges(); + synthesizeMouse(sc, 300, 300, {}, win); + synthesizeKey("VK_DOWN", {}, win); + + step(); + + ok(win.scrollY > oldY, "Page is scrolled down"); + ed.focus(); + win.getSelection().collapse(ed.firstChild, 3); + oldY = win.scrollY; + synthesizeKey("VK_DOWN", {}, win); + + step(); + + is(win.scrollY, oldY, "Page is not scrolled down"); + is(win.getSelection().focusNode, ed.firstChild, "Correct element selected"); + is(win.getSelection().focusOffset, ed.textContent.length, "Selection should be moved to the end"); + + win.close(); + cwu.restoreNormalRefresh(); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug550434.html b/editor/libeditor/tests/test_bug550434.html new file mode 100644 index 000000000..0fa3ad159 --- /dev/null +++ b/editor/libeditor/tests/test_bug550434.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=550434 +--> +<head> + <title>Test for Bug 550434</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550434">Mozilla Bug 550434</a> +<p id="display"></p> +<div id="content"> + <div id="editor" contenteditable="true" + style="height: 250px; height: 200px; border: 4px solid red; outline: none;"></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 550434 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var ed = document.getElementById("editor"); + + // Simulate click twice + synthesizeMouse(ed, 10, 10, {}); + synthesizeMouse(ed, 50, 50, {}); + setTimeout(function() { + synthesizeKey("x", {}); + + is(ed.innerHTML, "x", "Editor should work after being clicked twice"); + SimpleTest.finish(); + }, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug551704.html b/editor/libeditor/tests/test_bug551704.html new file mode 100644 index 000000000..8f335276f --- /dev/null +++ b/editor/libeditor/tests/test_bug551704.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=551704 +--> +<head> + <title>Test for Bug 551704</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=551704">Mozilla Bug 551704</a> +<p id="display"></p> +<div id="content"> + <div id="preformatted" style="white-space: pre" contenteditable>a b</div> + <div id="test1" contenteditable><br></div> + <div id="test2" contenteditable>a<br></div> + <div id="test3" contenteditable style="white-space: pre"><br></div> + <div id="test4" contenteditable style="white-space: pre">a<br></div> + <div id="test5" contenteditable></div> + <div id="test6" contenteditable>a</div> + <div id="test7" contenteditable style="white-space: pre"></div> + <div id="test8" contenteditable style="white-space: pre">a</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +function testLineBreak(div, type, expectedText, expectedHTML, callback) +{ + div.focus(); + getSelection().collapse(div, 0); + type(); + is(div.innerHTML, expectedHTML, "The expected HTML after editing should be correct"); + SimpleTest.waitForClipboard(expectedText, + function() { + getSelection().selectAllChildren(div); + synthesizeKey("C", {accelKey: true}); + }, + function() { + var t = document.createElement("textarea"); + document.body.appendChild(t); + t.focus(); + synthesizeKey("V", {accelKey: true}); + is(t.value, expectedText, "The expected text should be copied to the clipboard"); + callback(); + }, + function() { + SimpleTest.finish(); + } + ); +} + +function typeABCDEF() { + synthesizeKey("a", {}); + typeBCDEF_chars(); +} + +function typeBCDEF() { + synthesizeKey("VK_RIGHT", {}); + typeBCDEF_chars(); +} + +function typeBCDEF_chars() { + synthesizeKey("b", {}); + synthesizeKey("c", {}); + synthesizeKey("VK_RETURN", {}); + synthesizeKey("d", {}); + synthesizeKey("e", {}); + synthesizeKey("f", {}); +} + +/** Test for Bug 551704 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var preformatted = document.getElementById("preformatted"); + is(preformatted.innerHTML, "a\nb", "No BR node should be injected for preformatted editable fields"); + + var iframe = document.createElement("iframe"); + iframe.addEventListener("load", function() { + var sel = iframe.contentWindow.getSelection(); + is(sel.rangeCount, 0, "There should be no range in the selection initially"); + iframe.contentDocument.designMode = "on"; + sel = iframe.contentWindow.getSelection(); + is(sel.rangeCount, 1, "There should be a single range in the selection after setting designMode"); + var range = sel.getRangeAt(0); + ok(range.collapsed, "The range should be collapsed"); + is(range.startContainer, iframe.contentDocument.body.firstChild, "The range should start on the text"); + is(range.startOffset, 0, "The start offset should be zero"); + + continueTest(); + }, false); + iframe.src = "data:text/html,foo"; + document.getElementById("content").appendChild(iframe); +}); + +function continueTest() { + var divs = []; + for (var i = 0; i < 8; ++i) { + divs[i] = document.getElementById("test" + (i+1)); + } + var current = 0; + function doNextTest() { + if (current == divs.length) { + SimpleTest.finish(); + return; + } + var div = divs[current++]; + if (div.textContent == "a") { + var type = typeBCDEF; + } else { + var type = typeABCDEF; + } + var expectedHTML = "abc<br>def<br>"; + var expectedText = "abc\ndef"; + testLineBreak(div, type, expectedText, expectedHTML, doNextTest); + } + + doNextTest(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug552782.html b/editor/libeditor/tests/test_bug552782.html new file mode 100644 index 000000000..5c53e92c1 --- /dev/null +++ b/editor/libeditor/tests/test_bug552782.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=552782 +--> +<head> + <title>Test for Bug 552782</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=290026">Mozilla Bug 552782</a> +<p id="display"></p> +<div id="editor" contenteditable></div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 552782 **/ +SimpleTest.waitForExplicitFinish(); + +var original = '<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li><li>Item 4</li></ol></ol>'; +var editor = document.getElementById("editor"); +editor.innerHTML = original; +editor.focus(); + +addLoadEvent(function() { + + var sel = window.getSelection(); + sel.removeAllRanges(); + var lis = document.getElementsByTagName("li"); + sel.selectAllChildren(lis[2]); + document.execCommand("outdent", false, false); + var expected = '<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li><ol><li>Item 4</li></ol></ol>'; + is(editor.innerHTML, expected, "outdenting third item in a partially indented numbered list"); + document.execCommand("indent", false, false); + todo_is(editor.innerHTML, original, "re-indenting third item in a partially indented numbered list"); + + // done + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug567213.html b/editor/libeditor/tests/test_bug567213.html new file mode 100644 index 000000000..22418f9e2 --- /dev/null +++ b/editor/libeditor/tests/test_bug567213.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=567213 +--> + +<head> + <title>Test for Bug 567213</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567213">Mozilla Bug 567213</a> + <p id="display"></p> + <div id="content"> + <div id="target" contenteditable="true">test</div> + <button id="thief">theif</button> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 567213 **/ + + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + var target = document.getElementById("target"); + var thief = document.getElementById("thief"); + var sel = window.getSelection(); + + // select the contents of the editable area + sel.removeAllRanges(); + sel.selectAllChildren(target); + target.focus(); + + // press some key + synthesizeKey("X", {}); + is(target.textContent, "X", "Text input should work (sanity check)"); + + // select the contents of the editable area again + sel.removeAllRanges(); + sel.selectAllChildren(target); + thief.focus(); + + // press some key with the thief having focus + synthesizeKey("Y", {}); + is(target.textContent, "X", "Text entry should not work with another element focused"); + + SimpleTest.finish(); + }); + + </script> + </pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug569988.html b/editor/libeditor/tests/test_bug569988.html new file mode 100644 index 000000000..e42bbb985 --- /dev/null +++ b/editor/libeditor/tests/test_bug569988.html @@ -0,0 +1,99 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=569988 +--> +<head> + <title>Test for Bug 569988</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=569988">Mozilla Bug 569988</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 569988 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + + +function runTest() +{ + var script = SpecialPowers.loadChromeScript(function() { + var gPromptInput = null; + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + + os.addObserver(onPromptLoad, "common-dialog-loaded", false); + os.addObserver(onPromptLoad, "tabmodal-dialog-loaded", false); + + function onPromptLoad(subject, topic, data) { + sendAsyncMessage("ok", [true, "onPromptLoad is called"]); + gPromptInput = subject.Dialog.ui.loginTextbox; + gPromptInput.addEventListener("focus", onPromptFocus, false); + // shift focus to ensure it fires. + subject.Dialog.ui.button0.focus(); + gPromptInput.focus(); + } + + function onPromptFocus() { + sendAsyncMessage("ok", [true, "onPromptFocus is called"]); + gPromptInput.removeEventListener("focus", onPromptFocus, false); + + var listenerService = + Components.classes["@mozilla.org/eventlistenerservice;1"] + .getService(Components.interfaces.nsIEventListenerService); + + var listener = { + handleEvent: function _hv(aEvent) { + var isPrevented = aEvent.defaultPrevented; + sendAsyncMessage("ok", [!isPrevented, + "ESC key event is prevented by editor"]); + listenerService.removeSystemEventListener(gPromptInput, "keypress", + listener, false); + } + }; + listenerService.addSystemEventListener(gPromptInput, "keypress", + listener, false); + + sendAsyncMessage("info", "sending key"); + var EventUtils = {}; + EventUtils.window = {}; + EventUtils._EU_Ci = Components.interfaces; + EventUtils._EU_Cc = Components.classes; + Components.classes['@mozilla.org/moz/jssubscript-loader;1'] + .getService(Components.interfaces.mozIJSSubScriptLoader) + .loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils); + EventUtils.synthesizeKey("VK_ESCAPE", {}, + gPromptInput.ownerDocument.defaultView); + } + + addMessageListener("destroy", function() { + os.removeObserver(onPromptLoad, "tabmodal-dialog-loaded"); + os.removeObserver(onPromptLoad, "common-dialog-loaded"); + }); + }); + script.addMessageListener("ok", ([val, msg]) => ok(val, msg)); + script.addMessageListener("info", msg => info(msg)); + + info("opening prompt..."); + prompt("summary", "text"); + info("prompt is closed"); + + script.sendSyncMessage("destroy"); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug570144.html b/editor/libeditor/tests/test_bug570144.html new file mode 100644 index 000000000..e3b98b8d6 --- /dev/null +++ b/editor/libeditor/tests/test_bug570144.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=570144 +--> +<head> + <title>Test for Bug 570144</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=570144">Mozilla Bug 570144</a> +<p id="display"></p> +<div id="content"> + <!-- editable paragraphs in list item --> + <section id="test1"> + <ol> + <li><p contenteditable>foo</p></li> + </ol> + <ul> + <li><p contenteditable>foo</p></li> + </ul> + <dl> + <dt>foo</dt> + <dd><p contenteditable>bar</p></dd> + </dl> + </section> + <!-- paragraphs in editable list item --> + <section id="test2"> + <ol> + <li contenteditable><p>foo</p></li> + </ol> + <ul> + <li contenteditable><p>foo</p></li> + </ul> + <dl> + <dt>foo</dt> + <dd contenteditable><p>bar</p></dd> + </dl> + </section> + <!-- paragraphs in editable list --> + <section id="test3"> + <ol contenteditable> + <li><p>foo</p></li> + </ol> + <ul contenteditable> + <li><p>foo</p></li> + </ul> + <dl contenteditable> + <dt>foo</dt> + <dd><p>bar</p></dd> + </dl> + </section> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 570144 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function try2split(list) { + var editor = list.hasAttribute("contenteditable") + ? list : list.querySelector("*[contenteditable]"); + editor.focus(); + // put the caret at the end of the paragraph + var selection = window.getSelection(); + if (editor.nodeName.toLowerCase() == "p") + selection.selectAllChildren(editor); + else + selection.selectAllChildren(editor.querySelector("p")); + selection.collapseToEnd(); + // simulate a [Return] keypress + synthesizeKey("VK_RETURN", {}); +} + +function testSection(element, context, shouldCreateLI, shouldCreateP) { + var nbLI = shouldCreateLI ? 2 : 1; // number of expected list items + var nbP = shouldCreateP ? 2 : 1; // number of expected paragraphs + + function message(nodeName, dup) { + return context + ":[Return] should " + (dup ? "" : "not ") + + "create another <" + nodeName + ">." + } + var msgP = message("p", shouldCreateP); + var msgLI = message("li", shouldCreateLI); + var msgDT = message("dt", shouldCreateLI); + var msgDD = message("dd", false); + + const ol = element.querySelector("ol"); + try2split(ol); + is(ol.querySelectorAll("li").length, nbLI, msgLI); + is(ol.querySelectorAll("p").length, nbP, msgP); + + const ul = element.querySelector("ul"); + try2split(ul); + is(ul.querySelectorAll("li").length, nbLI, msgLI); + is(ul.querySelectorAll("p").length, nbP, msgP); + + const dl = element.querySelector("dl"); + try2split(dl); + is(dl.querySelectorAll("dt").length, nbLI, msgDT); + is(dl.querySelectorAll("dd").length, 1, msgDD); + is(dl.querySelectorAll("p").length, nbP, msgP); +} + +function runTests() { + testSection(document.getElementById("test1"), "editable paragraph in list item", false, false); + testSection(document.getElementById("test2"), "paragraph in editable list item", false, true); + testSection(document.getElementById("test3"), "paragraph in editable list", true, false); + /* Note: concerning #test3, it would be preferrable that [Return] creates + * another paragraph in another list item (i.e. last argument = 'true'). + * Currently it just creates an empty list item, which is acceptable. + */ + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug578771.html b/editor/libeditor/tests/test_bug578771.html new file mode 100644 index 000000000..09f163c51 --- /dev/null +++ b/editor/libeditor/tests/test_bug578771.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=578771 +--> + +<head> + <title>Test for Bug 578771</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=578771">Mozilla Bug 578771</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 578771 **/ + SimpleTest.waitForExplicitFinish(); + + function testElem(elem, elemTag) { + var ce = document.getElementById("ce"); + ce.focus(); + + synthesizeMouse(elem, 5, 5, {clickCount: 2 }); + ok(elem.selectionStart == 0 && elem.selectionEnd == 7, + " Double-clicking on another " + elemTag + " works correctly"); + + ce.focus(); + synthesizeMouse(elem, 5, 5, {clickCount: 3 }); + ok(elem.selectionStart == 0 && elem.selectionEnd == 14, + "Triple-clicking on another " + elemTag + " works correctly"); + } + // Avoid platform selection differences + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["layout.word_select.eat_space_to_next_word", false]]}, startTest); + }); + + function startTest() { + var input = document.getElementById("ip"); + testElem(input, "input"); + + var textarea = document.getElementById("ta"); + testElem(textarea, "textarea"); + + SimpleTest.finish(); + } + </script> + </pre> + + <input id="ip" type="text" value="Mozilla editor" /> + <textarea id="ta">Mozilla editor</textarea> + <div id="ce" contenteditable="true">Contenteditable div that could interfere with focus</div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug586662.html b/editor/libeditor/tests/test_bug586662.html new file mode 100644 index 000000000..36c56d759 --- /dev/null +++ b/editor/libeditor/tests/test_bug586662.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=586662 +--> + +<head> + <title>Test for Bug 586662</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586662">Mozilla Bug 586662</a> + <p id="display"><textarea onkeypress="this.style.overflow = 'hidden'"></textarea></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.focus(); + synthesizeKey("a", {}); + is(getComputedStyle(t, null).overflow, "hidden", "The event handler should be executed"); + is(t.value, "a", "The key entry should result in a character being added to the field"); + + var win = window.open("file_bug586662.html", "_blank", + "width=600,height=600,scrollbars=yes"); + SimpleTest.waitForFocus(function() { + // Make sure that focusing the textarea will cause the page to scroll + var ed = win.document.getElementById("editor"); + ed.focus(); + setTimeout(function() { + isnot(win.scrollY, 0, "Page is scrolled down"); + // Scroll back up + win.scrollTo(0, 0); + setTimeout(function() { + is(win.scrollY, 0, "Page is scrolled back up"); + // Make sure that typing something into the textarea will cause the + // page to scroll down + synthesizeKey("a", {}, win); + setTimeout(function() { + isnot(win.scrollY, 0, "Page is scrolled down again"); + + win.close(); + SimpleTest.finish(); + }, 0); + }, 0); + }, 0); + }, win); +}); + + </script> + </pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug587461.html b/editor/libeditor/tests/test_bug587461.html new file mode 100644 index 000000000..2cf9f29fc --- /dev/null +++ b/editor/libeditor/tests/test_bug587461.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=587461 +--> +<title>Test for Bug 587461</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=587461">Mozilla Bug 587461</a> +<div contenteditable><b>foobar</b></div> +<script> +var div = document.querySelector("div"); +getSelection().collapse(div.firstChild.firstChild, 3); +document.execCommand("inserthtml", false, "a"); +is(div.innerHTML, "<b>fooabar</b>", "innerHTML"); +</script> diff --git a/editor/libeditor/tests/test_bug590554.html b/editor/libeditor/tests/test_bug590554.html new file mode 100644 index 000000000..bc98503ed --- /dev/null +++ b/editor/libeditor/tests/test_bug590554.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=590554 +--> + +<head> + <title>Test for Bug 590554</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + + <script type="application/javascript"> + + /** Test for Bug 590554 **/ + + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.focus(); + synthesizeKey("VK_RETURN", {}); + is(t.value, "\n", "Pressing enter should work the first time"); + synthesizeKey("VK_RETURN", {}); + is(t.value, "\n", "Pressing enter should not work the second time"); + SimpleTest.finish(); + }); + + </script> + + <textarea maxlength="1"></textarea> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug592592.html b/editor/libeditor/tests/test_bug592592.html new file mode 100644 index 000000000..834ecbe1d --- /dev/null +++ b/editor/libeditor/tests/test_bug592592.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=592592 +--> +<head> + <title>Test for Bug 592592</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=592592">Mozilla Bug 592592</a> +<p id="display"></p> +<div id="content"> + <div id="editor" contenteditable="true" style="white-space:pre-wrap">a b</div> + <div id="editor2" contenteditable="true" style="white-space:pre-wrap">a b</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 592592 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var ed = document.getElementById("editor"); + + // Put the selection right after "a" + ed.focus(); + window.getSelection().collapse(ed.firstChild, 1); + + // Press space + synthesizeKey(" ", {}); + + // Make sure we haven't added an nbsp + is(ed.innerHTML, "a b", "We should not be adding an for preformatted text"); + + // Remove the preformatted style + ed.removeAttribute("style"); + + // Reset the DOM + ed.innerHTML = "a b"; + + // Reset the selection + ed.focus(); + window.getSelection().collapse(ed.firstChild, 1); + + // Press space + synthesizeKey(" ", {}); + + // Make sure that we have added an nbsp + is(ed.innerHTML, "a b", "We should add an for non-preformatted text"); + + ed = document.getElementById("editor2"); + + // Put the selection after the second space in the second editable field + ed.focus(); + window.getSelection().collapse(ed.firstChild, 3); + + // Press the back-space key + synthesizeKey("VK_BACK_SPACE", {}); + + // Make sure that we've only deleted a single space + is(ed.innerHTML, "a b", "We should only be deleting a single space"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug596001.html b/editor/libeditor/tests/test_bug596001.html new file mode 100644 index 000000000..c677df359 --- /dev/null +++ b/editor/libeditor/tests/test_bug596001.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=596001 +--> +<head> + <title>Test for Bug 596001</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596001">Mozilla Bug 596001</a> +<p id="display"></p> +<div id="content"> +<textarea id="src">a	b</textarea> +<textarea id="dst"></textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 596001 **/ + +function testTab(prefix, callback) { + var src = document.getElementById("src"); + var dst = document.getElementById("dst"); + dst.value = prefix; + src.focus(); + src.select(); + SimpleTest.waitForClipboard("a\tb", + function() { + synthesizeKey("c", {accelKey: true}); + }, + function() { + dst.focus(); + var inputReceived = false; + dst.addEventListener("input", function() { inputReceived = true; }, false); + synthesizeKey("v", {accelKey: true}); + ok(inputReceived, "An input event should be raised"); + is(dst.value, prefix + src.value, "The value should be pasted verbatim"); + callback(); + }, + callback + ); +} + +testTab("", function() { + testTab("foo", function() { + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug596333.html b/editor/libeditor/tests/test_bug596333.html new file mode 100644 index 000000000..a94726325 --- /dev/null +++ b/editor/libeditor/tests/test_bug596333.html @@ -0,0 +1,124 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=596333 +--> +<head> + <title>Test for Bug 596333</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script src="spellcheck.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596333">Mozilla Bug 596333</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 596333 **/ +const Ci = SpecialPowers.Ci; + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +var gMisspeltWords; + +function getEditor() { + return SpecialPowers.wrap(document.getElementById("edit")).editor; +} + +function append(str) { + var edit = document.getElementById("edit"); + edit.focus(); + edit.selectionStart = edit.selectionEnd = edit.value.length; + var editor = getEditor(); + + for (var i = 0; i < str.length; ++i) { + synthesizeKey(str[i], {}); + } +} + +function getLoadContext() { + return SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); +} + +function paste(str) { + var edit = document.getElementById("edit"); + var Cc = SpecialPowers.Cc, Ci = SpecialPowers.Ci; + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + var s = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + s.data = str; + trans.setTransferData("text/unicode", s, str.length * 2); + + getEditor().pasteTransferable(trans); +} + +function runOnFocus() { + var edit = document.getElementById("edit"); + + gMisspeltWords = ["haz", "cheezburger"]; + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "All misspellings before editing are accounted for."); + append(" becaz I'm a lulcat!"); + onSpellCheck(edit, function () { + gMisspeltWords.push("becaz"); + gMisspeltWords.push("lulcat"); + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "All misspellings after typing are accounted for."); + + // Now, type an invalid word, and instead of hitting "space" at the end, just blur + // the textarea and see if the spell check after the blur event catches it. + append(" workd"); + edit.blur(); + onSpellCheck(edit, function () { + gMisspeltWords.push("workd"); + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "All misspellings after blur are accounted for."); + + // Also, test the case when we're entering the first word in a textarea + gMisspeltWords = ["workd"]; + edit.value = ""; + append("workd "); + onSpellCheck(edit, function () { + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "Misspelling in the first entered word is accounted for."); + + // Make sure that pasting would also trigger spell checking for the previous word + gMisspeltWords = ["workd"]; + edit.value = ""; + append("workd"); + paste(" x"); + onSpellCheck(edit, function () { + ok(isSpellingCheckOk(getEditor(), gMisspeltWords), + "Misspelling is accounted for after pasting."); + + SimpleTest.finish(); + }); + }); + }); + }); +} + +function runTest() +{ + var edit = document.getElementById("edit"); + edit.focus(); + + SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", window); + onSpellCheck(edit, runOnFocus); +} +</script> +</pre> + +<textarea id="edit">I can haz cheezburger</textarea> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug596506.html b/editor/libeditor/tests/test_bug596506.html new file mode 100644 index 000000000..0fc1adabf --- /dev/null +++ b/editor/libeditor/tests/test_bug596506.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=596506 +--> +<head> + <title>Test for Bug 596506</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596506">Mozilla Bug 596506</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 596506 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +const kIsMac = navigator.platform.indexOf("Mac") == 0; + +function append(str) { + for (var i = 0; i < str.length; ++i) { + synthesizeKey(str[i], {}); + } +} + +function runTest() { + var edit = document.getElementById("edit"); + edit.focus(); + + append("First"); + synthesizeKey("VK_RETURN", {}); + append("Second"); + synthesizeKey("VK_UP", {}); + synthesizeKey("VK_UP", {}); + if (kIsMac) { + synthesizeKey("VK_RIGHT", {accelKey: true}); + } else { + synthesizeKey("VK_END", {}); + } + append("ly"); + is(edit.value, "Firstly\nSecond", + "Pressing end should position the cursor before the terminating newline"); + SimpleTest.finish(); +} + +</script> +</pre> + +<textarea id="edit"></textarea> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug597331.html b/editor/libeditor/tests/test_bug597331.html new file mode 100644 index 000000000..f35413cb6 --- /dev/null +++ b/editor/libeditor/tests/test_bug597331.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=597331 +--> +<head> + <title>Test for Bug 597331</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597331">Mozilla Bug 597331</a> +<p id="display"></p> +<div id="content"> +<textarea>line1 +line2 +line3 +</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 597331 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + SimpleTest.executeSoon(function() { + var t = document.querySelector("textarea"); + t.focus(); + t.selectionStart = 4; + t.selectionEnd = 4; + SimpleTest.executeSoon(function() { + t.getBoundingClientRect(); // flush layout + var before = snapshotWindow(window, true); + t.selectionStart = 5; + t.selectionEnd = 5; + t.addEventListener("keydown", function() { + t.removeEventListener("keydown", arguments.callee, false); + + SimpleTest.executeSoon(function() { + t.style.display = 'block'; + document.body.offsetWidth; + t.style.display = ''; + document.body.offsetWidth; + + is(t.selectionStart, 4, "Cursor should be moved correctly"); + is(t.selectionEnd, 4, "Cursor should be moved correctly"); + + var after = snapshotWindow(window, true); + + var result = compareSnapshots(before, after, true); + var msg = "The caret should be displayed correctly after reframing"; + if (!result[0]) { + msg += "\nRESULT:\n" + result[2]; + msg += "\nREFERENCE:\n" + result[1]; + } + ok(result[0], msg); + + SimpleTest.finish(); + }); + }, false); + synthesizeKey("VK_LEFT", {}); + }); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug597784.html b/editor/libeditor/tests/test_bug597784.html new file mode 100644 index 000000000..321f2ad1c --- /dev/null +++ b/editor/libeditor/tests/test_bug597784.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=597784 +--> +<head> + <title>Test for Bug 597784</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597784">Mozilla Bug 597784</a> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 597784 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + document.designMode = "on"; + var content = document.getElementById("content"); + getSelection().collapse(content, 0); + var html = "<test:tag>test:tag</test:tag>" + + "<a href=\"http://mozilla.org/\" test:attr=\"test:attr\" custom=\"value\">link</a>"; + document.execCommand("insertHTML", false, html); + is(content.innerHTML, html, + "The custom tags and attributes should be inserted into the document using the insertHTML command"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug599322.html b/editor/libeditor/tests/test_bug599322.html new file mode 100644 index 000000000..578bcb11a --- /dev/null +++ b/editor/libeditor/tests/test_bug599322.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=599322.patch +--> +<head> + <title>Test for Bug 599322.patch</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=599322.patch">Mozilla Bug 599322.patch</a> +<p id="display"></p> +<div id="content"> +<div id="src">src<img src="/tests/editor/libeditor/tests/green.png"></div> +<iframe id="dst" src="javascript:;"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 599322.patch **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var src = document.getElementById("src"); + var dst = document.getElementById("dst"); + var doc = dst.contentDocument; + doc.open(); + doc.write("<html><head><base href='http://mochi.test:8888/'></head><body></body></html>"); + doc.close(); + SimpleTest.waitForFocus(function() { + getSelection().selectAllChildren(src); + SimpleTest.waitForClipboard("src", + function() { + synthesizeKey("c", {accelKey: true}); + }, + function() { + dst.contentDocument.designMode = "on"; + dst.focus(); + dst.contentDocument.body.focus(); + synthesizeKey("v", {accelKey: true}); + is(dst.contentDocument.querySelector("img").src, + document.querySelector("img").src, + "The source should be correctly set based on the base URI"); + SimpleTest.finish(); + }, + function() { + SimpleTest.finish(); + } + ); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug599983.html b/editor/libeditor/tests/test_bug599983.html new file mode 100644 index 000000000..08fc9a228 --- /dev/null +++ b/editor/libeditor/tests/test_bug599983.html @@ -0,0 +1,16 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=599983 +--> +<title>Test for Bug 599983</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=599983">Mozilla Bug 599983</a> +<div contenteditable>foo</div> +<script> +getSelection().selectAllChildren(document.querySelector("div")); +document.execCommand("bold"); +is(document.querySelector("[_moz_dirty]"), null, + "No _moz_dirty allowed in webpages"); +</script> diff --git a/editor/libeditor/tests/test_bug599983.xul b/editor/libeditor/tests/test_bug599983.xul new file mode 100644 index 000000000..8b5d52a8c --- /dev/null +++ b/editor/libeditor/tests/test_bug599983.xul @@ -0,0 +1,70 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=599983 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 599983" onload="runTest()"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=599983" + target="_blank">Mozilla Bug 599983</a> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + editortype="html" + src="about:blank" /> + </body> + <script type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + const kAllowInteraction = Components.interfaces.nsIPlaintextEditor + .eEditorAllowInteraction; + const kMailMask = Components.interfaces.nsIPlaintextEditor.eEditorMailMask; + + function runTest() { + testEditor(false, false); + testEditor(false, true); + testEditor(true, false); + testEditor(true, true); + + SimpleTest.finish(); + } + + function testEditor(setAllowInteraction, setMailMask) { + var desc = " with " + (setAllowInteraction ? "" : "no ") + + "eEditorAllowInteraction and " + + (setMailMask ? "" : "no ") + "eEditorMailMask"; + + var editorElem = document.getElementById("editor"); + + var editorObj = editorElem.getEditor(editorElem.contentWindow); + editorObj.flags = (setAllowInteraction ? kAllowInteraction : 0) | + (setMailMask ? kMailMask : 0); + + var editorDoc = editorElem.contentDocument; + editorDoc.body.innerHTML = "<p>foo<p>bar"; + editorDoc.getSelection().selectAllChildren(editorDoc.body.firstChild); + editorDoc.execCommand("bold"); + + var createsDirty = !setAllowInteraction || setMailMask; + + (createsDirty ? isnot : is)(editorDoc.querySelector("[_moz_dirty]"), null, + "Elements with _moz_dirty" + desc); + + // Even if we do create _moz_dirty, we should strip it for innerHTML. + is(editorDoc.body.innerHTML, "<p><b>foo</b></p><p>bar</p>", + "innerHTML" + desc); + } + + ]]> + </script> +</window> diff --git a/editor/libeditor/tests/test_bug600570.html b/editor/libeditor/tests/test_bug600570.html new file mode 100644 index 000000000..0a5a814f8 --- /dev/null +++ b/editor/libeditor/tests/test_bug600570.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=600570 +--> +<head> + <title>Test for Bug 600570</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=600570">Mozilla Bug 600570</a> +<p id="display"></p> +<div id="content"> +<textarea spellcheck="false"> +aaa +[bbb]</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 600570 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.value = "[aaa\nbbb]"; + t.focus(); + synthesizeKey("A", {accelKey: true}); + + SimpleTest.executeSoon(function() { + t.getBoundingClientRect(); // flush layout + var afterSetValue = snapshotWindow(window); + + t.value = t.defaultValue; + + t.selectionStart = 0; + t.selectionEnd = 4; + SimpleTest.waitForClipboard("aaa\n", + function() { + synthesizeKey("X", {accelKey: true}); + }, + function() { + t.addEventListener("input", function() { + t.removeEventListener("input", arguments.callee, false); + + setTimeout(function() { // Avoid the assertion in bug 649797 + is(t.value, "[aaa\nbbb]", "The value of the textarea should be correct"); + synthesizeKey("A", {accelKey: true}); + is(t.selectionStart, 0, "Select all should set the selection start to the beginning of textarea"); + is(t.selectionEnd, 9, "Select all should set the selection end to the end of textarea"); + + var afterPaste = snapshotWindow(window); + + var res = compareSnapshots(afterSetValue, afterPaste, true); + var msg = "Pasting and setting the value directly should result in the same rendering"; + if (!res[0]) { + msg += "\nRESULT:\n" + res[2] + "\nREFERENCE:\n" + res[1]; + } + ok(res[0], msg); + + SimpleTest.finish(); + }, 0); + }, false); + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("V", {accelKey: true}); + }, + function() { + SimpleTest.finish(); + } + ); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug602130.html b/editor/libeditor/tests/test_bug602130.html new file mode 100644 index 000000000..a61e5c9c3 --- /dev/null +++ b/editor/libeditor/tests/test_bug602130.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=602130 +--> +<head> + <title>Test for Bug 602130</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602130">Mozilla Bug 602130</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 602130 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var i = document.createElement("input"); + document.body.appendChild(i); + SpecialPowers.wrap(i).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement); + i.select(); + i.focus(); + is(SpecialPowers.wrap(i).editor.transactionManager.numberOfUndoItems, 0, + "The number of undo items should be 0 after initing the editor"); + i.style.display = "none"; + document.offsetWidth; + i.style.display = ""; + document.offsetWidth; + i.select(); + i.focus(); + is(SpecialPowers.wrap(i).editor.transactionManager.numberOfUndoItems, 0, + "The number of undo items should be 0 after re-initing the editor"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug603556.html b/editor/libeditor/tests/test_bug603556.html new file mode 100644 index 000000000..0e0a70464 --- /dev/null +++ b/editor/libeditor/tests/test_bug603556.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=603556 +--> +<head> + <title>Test for Bug 603556</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=603556">Mozilla Bug 603556</a> +<p id="display"></p> +<div id="content"> + <div id="src">testing</div> + <input maxlength="4"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 603556 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var i = document.querySelector("input"); + var src = document.getElementById("src"); + SimpleTest.waitForClipboard(src.textContent, + function() { + getSelection().selectAllChildren(src); + synthesizeKey("C", {accelKey: true}); + }, + function() { + i.focus(); + synthesizeKey("V", {accelKey: true}); + is(i.value, src.textContent.substr(0, i.maxLength), + "Pasting should paste maxlength chars worth of the clipboard contents"); + SimpleTest.finish(); + }, + function() { + SimpleTest.finish(); + } + ); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug604532.html b/editor/libeditor/tests/test_bug604532.html new file mode 100644 index 000000000..519a179b1 --- /dev/null +++ b/editor/libeditor/tests/test_bug604532.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=604532 +--> +<head> + <title>Test for Bug 604532</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=604532">Mozilla Bug 604532</a> +<p id="display"></p> +<div id="content"> +<input> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 604532 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var i = document.querySelector("input"); + i.focus(); + i.value = "foo"; + synthesizeKey("A", {accelKey: true}); + is(i.selectionStart, 0, "Selection should start at 0 before appending"); + is(i.selectionEnd, 3, "Selection should end at 3 before appending"); + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("x", {}); + is(i.value, "foox", "The text should be appended correctly"); + synthesizeKey("A", {accelKey: true}); + is(i.selectionStart, 0, "Selection should start at 0 after appending"); + is(i.selectionEnd, 4, "Selection should end at 4 after appending"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug607584.html b/editor/libeditor/tests/test_bug607584.html new file mode 100644 index 000000000..aa22b6f30 --- /dev/null +++ b/editor/libeditor/tests/test_bug607584.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=607584 +--> +<head> + <title>Test for Bug 607584</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=607584">Mozilla Bug 607584</a> +<p id="display"></p> +<div id="content" contenteditable> +<p id="foo">Hello world</p> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 607584 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var content = document.getElementById("content"); + content.focus(); + var sel = getSelection(); + sel.collapse(document.getElementById("foo").firstChild, 5); + synthesizeKey("VK_RETURN", {}); + var paragraphs = content.querySelectorAll("p"); + is(paragraphs.length, 2, "The paragraph should be split in two"); + is(paragraphs[0].textContent, "Hello", "The first paragraph should have the correct content"); + is(paragraphs[1].textContent, " world", "The second paragraph should have the correct content"); + is(paragraphs[0].getAttribute("id"), "foo", "The id of the first paragraph should be retained"); + is(paragraphs[1].hasAttribute("id"), false, "The second paragraph shouldn't have an ID"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug607584.xul b/editor/libeditor/tests/test_bug607584.xul new file mode 100644 index 000000000..fb16cee83 --- /dev/null +++ b/editor/libeditor/tests/test_bug607584.xul @@ -0,0 +1,115 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=607584 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 607584" onload="runTest();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=607584" + target="_blank">Mozilla Bug 607584</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content-primary" + editortype="html" + style="width: 400px; height: 100px; border: thin solid black"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function EditorContentListener(aEditor) + { + this.init(aEditor); + } + + EditorContentListener.prototype = { + init : function(aEditor) + { + this.mEditor = aEditor; + }, + + QueryInterface : function(aIID) + { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) + { + var editor = this.mEditor.getEditor(this.mEditor.contentWindow); + if (editor) { + this.mEditor.focus(); + editor instanceof Components.interfaces.nsIHTMLEditor; + editor.returnInParagraphCreatesNewParagraph = true; + editor.insertHTML("<p id='foo'>this is a paragraph carrying id 'foo'</p>"); + var p = editor.document.getElementById('foo') + editor.beginningOfDocument(); + sendKey("return"); + var firstP = p.parentNode.firstElementChild; + var lastP = p.parentNode.lastElementChild; + var isOk = firstP.nodeName.toLowerCase() == "p" && + firstP.id == "foo" && + lastP.id == ""; + ok(isOk, "CR in a paragraph with an ID should not create two paragraphs of same ID"); + progress.removeProgressListener(this); + SimpleTest.finish(); + } + } + + }, + + + onProgressChange : function(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange : function(aWebProgress, aRequest, aState) + { + }, + + mEditor: null + }; + + var progress, progressListener; + + function runTest() { + var newEditorElement = document.getElementById("editor"); + newEditorElement.makeEditable("html", true); + var docShell = newEditorElement.boxObject.docShell; + progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress); + progressListener = new EditorContentListener(newEditorElement); + progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL); + newEditorElement.setAttribute("src", "data:text/html,"); + } +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug611182.html b/editor/libeditor/tests/test_bug611182.html new file mode 100644 index 000000000..e6ecc6716 --- /dev/null +++ b/editor/libeditor/tests/test_bug611182.html @@ -0,0 +1,239 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=611182 +--> +<head> + <title>Test for Bug 611182</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=611182">Mozilla Bug 611182</a> +<p id="display"></p> +<div id="content"> + <iframe></iframe> + <iframe id="ref" src="data:text/html,foo bar"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 611182 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var iframe = document.querySelector("iframe"); + var refElem = document.querySelector("#ref"); + var ref = snapshotWindow(refElem.contentWindow, false); + + function findTextNode(doc) { + var body = doc.documentElement; + var result = findTextNodeWorker(body); + ok(result, "Failed to find the text node"); + return result; + } + + function findTextNodeWorker(root) { + if (root.isContentEditable) { + root.focus(); + } + for (var i = 0; i < root.childNodes.length; ++i) { + var node = root.childNodes[i]; + if (node.nodeType == node.TEXT_NODE && + node.nodeValue == "fooz bar") { + return node; + } + if (node.nodeType == node.ELEMENT_NODE) { + node = findTextNodeWorker(node); + if (node) { + return node; + } + } + } + return null; + } + + function testBackspace(src, callback) { + ok(true, "Testing " + src); + iframe.addEventListener("load", function() { + iframe.removeEventListener("load", arguments.callee, false); + + var doc = iframe.contentDocument; + var win = iframe.contentWindow; + doc.body.setAttribute("spellcheck", "false"); + + iframe.focus(); + var textNode = findTextNode(doc); + var sel = win.getSelection(); + sel.collapse(textNode, 4); + synthesizeKey("VK_BACK_SPACE", {}); + is(textNode.textContent, "foo bar", "Backspace should work correctly"); + + var snapshot = snapshotWindow(win, false); + ok(compareSnapshots(snapshot, ref, true)[0], "No bogus node should exist in the document"); + + callback(); + }, false); + iframe.src = src; + } + + const TEST_URIS = [ + "data:text/html,<html contenteditable>fooz bar</html>", + "data:text/html,<html contenteditable><body>fooz bar</body></html>", + "data:text/html,<body contenteditable>fooz bar</body>", + "data:text/html,<body contenteditable><p>fooz bar</p></body>", + "data:text/html,<body contenteditable><div>fooz bar</div></body>", + "data:text/html,<body contenteditable><span>fooz bar</span></body>", + "data:text/html,<p contenteditable style='outline:none'>fooz bar</p>", + "data:text/html,<!DOCTYPE html><html><body contenteditable>fooz bar</body></html>", + "data:text/html,<!DOCTYPE html><html contenteditable><body>fooz bar</body></html>", + 'data:application/xhtml+xml,<html xmlns="http://www.w3.org/1999/xhtml"><body contenteditable="true">fooz bar</body></html>', + 'data:application/xhtml+xml,<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true"><body>fooz bar</body></html>', + "data:text/html,<body onload=\"document.designMode='on'\">fooz bar</body>", + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'var old = document.body;' + + 'old.parentNode.removeChild(old);' + + 'var r = document.documentElement;' + + 'var b = document.createElement("body");' + + 'r.appendChild(b);' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + 'b.contentEditable = "true";' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'var old = document.body;' + + 'old.parentNode.removeChild(old);' + + 'var r = document.documentElement;' + + 'var b = document.createElement("body");' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + 'b.contentEditable = "true";' + + 'r.appendChild(b);' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'var old = document.body;' + + 'old.parentNode.removeChild(old);' + + 'var r = document.documentElement;' + + 'var b = document.createElement("body");' + + 'r.appendChild(b);' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + 'b.setAttribute("contenteditable", "true");' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'var old = document.body;' + + 'old.parentNode.removeChild(old);' + + 'var r = document.documentElement;' + + 'var b = document.createElement("body");' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + 'b.setAttribute("contenteditable", "true");' + + 'r.appendChild(b);' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'var old = document.body;' + + 'old.parentNode.removeChild(old);' + + 'var r = document.documentElement;' + + 'var b = document.createElement("body");' + + 'r.appendChild(b);' + + 'b.contentEditable = "true";' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'var old = document.body;' + + 'old.parentNode.removeChild(old);' + + 'var r = document.documentElement;' + + 'var b = document.createElement("body");' + + 'b.contentEditable = "true";' + + 'r.appendChild(b);' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'var old = document.body;' + + 'old.parentNode.removeChild(old);' + + 'var r = document.documentElement;' + + 'var b = document.createElement("body");' + + 'r.appendChild(b);' + + 'b.setAttribute("contenteditable", "true");' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'var old = document.body;' + + 'old.parentNode.removeChild(old);' + + 'var r = document.documentElement;' + + 'var b = document.createElement("body");' + + 'b.setAttribute("contenteditable", "true");' + + 'r.appendChild(b);' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'document.open();' + + 'document.write("<body contenteditable>fooz bar</body>");' + + 'document.close();' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'document.open();' + + 'document.write("<body contenteditable><div>fooz bar</div></body>");' + + 'document.close();' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'document.open();' + + 'document.write("<body contenteditable><span>fooz bar</span></body>");' + + 'document.close();' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'document.open();' + + 'document.write("<p contenteditable style=\\"outline: none\\">fooz bar</p>");' + + 'document.close();' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'document.open();' + + 'document.write("<html contenteditable>fooz bar</html>");' + + 'document.close();' + + '};' + + '<\/script><body></body></html>', + 'data:text/html,<html><script>' + + 'onload = function() {' + + 'document.open();' + + 'document.write("<html contenteditable><body>fooz bar</body></html>");' + + 'document.close();' + + '};' + + '<\/script><body></body></html>', + ]; + var currentTest = 0; + function runAllTests() { + if (currentTest == TEST_URIS.length) { + SimpleTest.finish(); + return; + } + testBackspace(TEST_URIS[currentTest++], runAllTests); + } + runAllTests(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug612128.html b/editor/libeditor/tests/test_bug612128.html new file mode 100644 index 000000000..b23d6f12a --- /dev/null +++ b/editor/libeditor/tests/test_bug612128.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=612128 +--> +<head> + <title>Test for Bug 612128</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612128">Mozilla Bug 612128</a> +<p id="display"></p> +<div id="content"> +<input> +<div contenteditable></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 612128 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + document.querySelector("input").focus(); + var threw = false; + try { + is(document.execCommand("inserthtml", null, "<span>f" + "oo</span>"), + false, "The insertHTML command should return false"); + } catch (e) { + ok(false, "insertHTML should not throw here"); + } + is(document.querySelectorAll("span").length, 0, "No span element should be injected inside the page"); + is(document.body.innerHTML.indexOf("f" + "oo"), -1, "No text should be injected inside the page"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug612447.html b/editor/libeditor/tests/test_bug612447.html new file mode 100644 index 000000000..b06739288 --- /dev/null +++ b/editor/libeditor/tests/test_bug612447.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=612447 +--> +<head> + <title>Test for Bug 612447</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612447">Mozilla Bug 612447</a> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 612447 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + function editorCommandsEnabled() { + var caught = false; + try { + doc.execCommand("justifyfull", false, null); + } catch (e) { + caught = true; + } + return !caught; + } + + var i = document.querySelector("iframe"); + var doc = i.contentDocument; + var win = i.contentWindow; + var b = doc.body; + doc.designMode = "on"; + i.focus(); + b.focus(); + var beforeA = snapshotWindow(win, true); + synthesizeKey("X", {}); + var beforeB = snapshotWindow(win, true); + is(b.textContent, "X", "Typing should work"); + while (b.firstChild) { + b.removeChild(b.firstChild); + } + ok(editorCommandsEnabled(), "The editor commands should work"); + + i.style.display = "block"; + document.clientWidth; + + i.focus(); + b.focus(); + var afterA = snapshotWindow(win, true); + synthesizeKey("X", {}); + var afterB = snapshotWindow(win, true); + is(b.textContent, "X", "Typing should work"); + while (b.firstChild) { + b.removeChild(b.firstChild); + } + ok(editorCommandsEnabled(), "The editor commands should work"); + + ok(compareSnapshots(beforeA, afterA, true)[0], "The iframes should look the same before typing"); + ok(compareSnapshots(beforeB, afterB, true)[0], "The iframes should look the same after typing"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug616590.xul b/editor/libeditor/tests/test_bug616590.xul new file mode 100644 index 000000000..57c29a028 --- /dev/null +++ b/editor/libeditor/tests/test_bug616590.xul @@ -0,0 +1,105 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=616590 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 616590" onload="runTest();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=616590" + target="_blank">Mozilla Bug 616590</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + editortype="htmlmail" + style="width: 400px; height: 100px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function EditorContentListener(aEditor) + { + this.init(aEditor); + } + + EditorContentListener.prototype = { + init : function(aEditor) + { + this.mEditor = aEditor; + }, + + QueryInterface : function(aIID) + { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) + { + var editor = this.mEditor.getEditor(this.mEditor.contentWindow); + if (editor) { + editor.QueryInterface(Components.interfaces.nsIEditorMailSupport); + editor.insertAsCitedQuotation("<html><body><div contenteditable>foo</div></body></html>", "", true); + document.documentElement.clientWidth; + progress.removeProgressListener(this); + ok(true, "Test complete"); + SimpleTest.finish(); + } + } + }, + + + onProgressChange : function(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange : function(aWebProgress, aRequest, aState) + { + }, + + mEditor: null + }; + + var progress, progressListener; + + function runTest() { + var editorElement = document.getElementById("editor"); + editorElement.makeEditable("htmlmail", true); + var docShell = editorElement.boxObject.docShell; + progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress); + progressListener = new EditorContentListener(editorElement); + progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL); + editorElement.setAttribute("src", "data:text/html,"); + } +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug620906.html b/editor/libeditor/tests/test_bug620906.html new file mode 100644 index 000000000..208bdfd28 --- /dev/null +++ b/editor/libeditor/tests/test_bug620906.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=620906 +--> +<head> + <title>Test for Bug 620906</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620906">Mozilla Bug 620906</a> +<p id="display"></p> +<div id="content"> + <iframe src="data:text/html, + <body contenteditable + onmousedown=' + document.designMode="on"; + document.designMode="off"; + ' + > + <div style='height: 1000px;'></div> + </body>"> + </iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 620906 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var iframe = document.querySelector("iframe"); + is(iframe.contentWindow.scrollY, 0, "Sanity check"); + var rect = iframe.getBoundingClientRect(); + setTimeout(function() { + var onscroll = function () { + iframe.contentWindow.removeEventListener("scroll", onscroll, false); + isnot(iframe.contentWindow.scrollY, 0, "The scrollbar should work"); + SimpleTest.finish(); + } + iframe.contentWindow.addEventListener("scroll", onscroll, false); + synthesizeMouse(iframe, rect.width - 5, rect.height / 2, {}); + }, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug622371.html b/editor/libeditor/tests/test_bug622371.html new file mode 100644 index 000000000..d08ba8214 --- /dev/null +++ b/editor/libeditor/tests/test_bug622371.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=622371 +--> +<head> + <title>Test for Bug 622371</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622371">Mozilla Bug 622371</a> +<p id="display"></p> +<div id="content"> + <iframe src="data:text/html,<body contenteditable>abc</body>"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 622371 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var i = document.querySelector("iframe"); + var sel = i.contentWindow.getSelection(); + var doc = i.contentDocument; + var body = doc.body; + i.focus(); + sel.collapse(body, 1); + doc.designMode = "on"; + doc.designMode = "off"; + is(sel.getRangeAt(0).startOffset, 1, "The start offset of the selection shouldn't change"); + is(sel.getRangeAt(0).endOffset, 1, "The end offset of the selection shouldn't change"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug625452.html b/editor/libeditor/tests/test_bug625452.html new file mode 100644 index 000000000..e2292d753 --- /dev/null +++ b/editor/libeditor/tests/test_bug625452.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=625452 +--> +<head> + <title>Test for Bug 625452</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=625452">Mozilla Bug 625452</a> +<p id="display"></p> +<div id="content"> +<input> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 625452 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var i = document.querySelector("input"); + var inputCount = 0; + i.addEventListener("input", function() { inputCount++; }, false); + + // test cut + i.focus(); + i.value = "foo bar"; + i.selectionStart = 0; + i.selectionEnd = 4; + synthesizeKey("X", {accelKey: true}); + is(i.value, "bar", "Cut should work correctly"); + is(inputCount, 1, "input event should be raised correctly"); + + // test undo + synthesizeKey("Z", {accelKey: true}); + is(i.value, "foo bar", "Undo should work correctly"); + is(inputCount, 2, "input event should be raised correctly"); + + // test redo + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + is(i.value, "bar", "Redo should work correctly"); + is(inputCount, 3, "input event should be raised correctly"); + + // test delete + i.selectionStart = 0; + i.selectionEnd = 2; + synthesizeKey("VK_DELETE", {}); + is(i.value, "r", "Delete should work correctly"); + is(inputCount, 4, "input event should be raised correctly"); + + // test DeleteSelection(eNone) + i.value = "retest"; // the "r" common prefix is crucial here + is(inputCount, 4, "input event should not have been raised"); + + // paste is tested in test_bug596001.html + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug629845.html b/editor/libeditor/tests/test_bug629845.html new file mode 100644 index 000000000..9eb24f904 --- /dev/null +++ b/editor/libeditor/tests/test_bug629845.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=629845 +--> +<head> + <title>Test for Bug 629845</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=629845">Mozilla Bug 629845</a> +<p id="display"></p> + +<script> +function initFrame(frame) +{ + frame.contentWindow.document.designMode="on"; + frame.contentWindow.document.writeln("<body></body>"); + + document.getElementsByTagName('button')[0].click(); +} + +function command(aName) +{ + var frame = document.getElementsByTagName('iframe')[0]; + + is(frame.contentDocument.designMode, "on", "design mode should be on!"); + var caught = false; + try { + frame.contentDocument.execCommand(aName, false, null); + } catch (e) { + ok(false, "exception " + e + " was thrown"); + caught = true; + } + + ok(!caught, "No exception should have been thrown."); + + // Stop the document load before finishing, just to be clean. + document.getElementsByTagName('iframe')[0].contentWindow.document.close(); + SimpleTest.finish(); +} +</script> + +<div id="content"> + <button type="button" onclick="command('bold');">Bold</button> + <iframe onload="initFrame(this);"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 629845 **/ + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug635636.html b/editor/libeditor/tests/test_bug635636.html new file mode 100644 index 000000000..e5bbb5322 --- /dev/null +++ b/editor/libeditor/tests/test_bug635636.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=635636 +--> +<head> + <title>Test for Bug 635636</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635636">Mozilla Bug 635636</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 635636 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var w, d; + + function b1() + { + w = window.open('data:application/xhtml+xml,<html xmlns="http://www.w3.org/1999/xhtml"><div>1</div></html>'); + SimpleTest.waitForFocus(b2, w); + } + + function b2() + { + w.document.designMode = 'on'; + w.location = "data:text/plain,2"; + d = w.document.getElementsByTagName("div")[0]; + const Ci = SpecialPowers.Ci; + var mainWindow = SpecialPowers.wrap(w) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + var browser = mainWindow.gBrowser.selectedBrowser; + browser.addEventListener("pageshow", function() { + setTimeout(b3, 0); + }, false); + } + + function b3() + { + d.parentNode.removeChild(d); + ok(true, "Should not crash"); + // Not needed for the crash + w.close(); + SimpleTest.finish(); + } + + b1(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug636465.html b/editor/libeditor/tests/test_bug636465.html new file mode 100644 index 000000000..37ceebe5a --- /dev/null +++ b/editor/libeditor/tests/test_bug636465.html @@ -0,0 +1,54 @@ +<!doctype html> +<title>Mozilla bug 636465</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/WindowSnapshot.js"></script> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=636465" + target="_blank">Mozilla Bug 636465</a> +<input id="x" value="foobarbaz" spellcheck="true" style="background-color: transparent; border: transparent;"> +<script> +SimpleTest.waitForExplicitFinish(); + +function runTest() { + SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", + window); + var x = document.getElementById("x"); + x.focus(); + onSpellCheck(x, function () { + x.blur(); + var spellCheckTrue = snapshotWindow(window); + x.setAttribute("spellcheck", "false"); + var spellCheckFalse = snapshotWindow(window); + x.setAttribute("spellcheck", "true"); + x.focus(); + onSpellCheck(x, function () { + x.blur(); + var spellCheckTrueAgain = snapshotWindow(window); + x.removeAttribute("spellcheck"); + var spellCheckNone = snapshotWindow(window); + var after = snapshotWindow(window); + var ret = compareSnapshots(spellCheckTrue, spellCheckFalse, false)[0]; + ok(ret, + "Setting the spellcheck attribute to false should work"); + if (!ret) { + ok(false, "\nspellCheckTrue: " + spellCheckTrue.toDataURL() + "\nspellCheckFalse: " + spellCheckFalse.toDataURL()); + } + ret = compareSnapshots(spellCheckTrue, spellCheckTrueAgain, true)[0]; + ok(ret, + "Setting the spellcheck attribute back to true should work"); + if (!ret) { + ok(false, "\nspellCheckTrue: " + spellCheckTrue.toDataURL() + "\nspellCheckTrueAgain: " + spellCheckTrueAgain.toDataURL()); + } + ret = compareSnapshots(spellCheckNone, spellCheckFalse, true)[0]; + ok(ret, + "Unsetting the spellcheck attribute should work"); + if (!ret) { + ok(false, "\spellCheckNone: " + spellCheckNone.toDataURL() + "\nspellCheckFalse: " + spellCheckFalse.toDataURL()); + } + SimpleTest.finish(); + }); + }); +} +addLoadEvent(runTest); +</script> diff --git a/editor/libeditor/tests/test_bug638596.html b/editor/libeditor/tests/test_bug638596.html new file mode 100644 index 000000000..62ef103f0 --- /dev/null +++ b/editor/libeditor/tests/test_bug638596.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=638596 +--> +<head> + <title>Test for Bug 638596</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=638596">Mozilla Bug 638596</a> +<p id="display"></p> +<div id="content"> + <input type="password" style="font-size: 0"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 638596 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var i = document.querySelector("input"); + i.focus(); + synthesizeKey("t", {}); + synthesizeKey("e", {}); + synthesizeKey("s", {}); + synthesizeKey("t", {}); + is(i.value, "test", "The correct value should be stored in the field"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug640321.html b/editor/libeditor/tests/test_bug640321.html new file mode 100644 index 000000000..984ea295a --- /dev/null +++ b/editor/libeditor/tests/test_bug640321.html @@ -0,0 +1,190 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=640321 +--> +<head> + <title>Test for Bug 640321</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=640321">Mozilla Bug 640321</a> +<p id="display"></p> +<div id="content" contenteditable style="text-align: center"> + <img src="green.png"> +</div> +<div id="clickaway" style="width: 10px; height: 10px"></div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 640321 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var img = document.querySelector("img"); + + function cancel(e) { e.stopPropagation(); } + var content = document.getElementById("content"); + content.addEventListener("mousedown", cancel, false); + content.addEventListener("mousemove", cancel, false); + content.addEventListener("mouseup", cancel, false); + + /** + * This function is a generic resizer test. + * We have 8 resizers that we'd like to test, and each can be moved in 8 different directions. + * In specifying baseX, W can be considered to be the width of the image, and for baseY, H + * can be considered to be the height of the image. deltaX and deltaY are regular pixel values + * which can be positive or negative. + */ + const W = 1; + const H = 1; + function testResizer(baseX, baseY, deltaX, deltaY, expectedDeltaX, expectedDeltaY) { + ok(true, "testResizer(" + [baseX, baseY, deltaX, deltaY, expectedDeltaX, expectedDeltaY].join(", ") + ")"); + + // Reset the dimensions of the image + img.style.width = "100px"; + img.style.height = "100px"; + var rect = img.getBoundingClientRect(); + is(rect.width, 100, "Sanity check the length"); + is(rect.height, 100, "Sanity check the height"); + + // Click on the image to show the resizers + synthesizeMouseAtCenter(img, {}); + + // Determine which resizer we're dealing with + var basePosX = rect.width * baseX; + var basePosY = rect.height * baseY; + + // Click on the correct resizer + synthesizeMouse(img, basePosX, basePosY, {type: "mousedown"}); + // Drag it delta pixels to the right and bottom (or maybe left and top!) + synthesizeMouse(img, basePosX + deltaX, basePosY + deltaY, {type: "mousemove"}); + // Release the mouse button + synthesizeMouse(img, basePosX + deltaX, basePosY + deltaY, {type: "mouseup"}); + // Move the mouse delta more pixels to the same direction to make sure that the + // resize operation has stopped. + synthesizeMouse(img, basePosX + deltaX * 2, basePosY + deltaY * 2, {type: "mousemove"}); + // Click outside of the image to hide the resizers + synthesizeMouseAtCenter(document.getElementById("clickaway"), {}); + + // Get the new dimensions for the image + var newRect = img.getBoundingClientRect(); + is(newRect.width, rect.width + expectedDeltaX, "The width should be increased by " + expectedDeltaX + " pixels"); + is(newRect.height, rect.height + expectedDeltaY, "The height should be increased by " + expectedDeltaY + "pixels"); + } + + function runTests(preserveRatio) { + // Account for changes in the resizing behavior when we're trying to preserve + // the aspect ration. + // ignoredGrowth means we don't change the size of a dimension because otherwise + // the aspect ratio would change undesirably. + // needlessGrowth means that we change the size of a dimension perpendecular to + // the mouse movement axis in order to preserve the aspect ratio. + // reversedGrowth means that we change the size of a dimension in the opposite + // direction to the mouse movement in order to maintain the aspect ratio. + const ignoredGrowth = preserveRatio ? 0 : 1; + const needlessGrowth = preserveRatio ? 1 : 0; + const reversedGrowth = preserveRatio ? -1 : 1; + + // top resizer + testResizer(W/2, 0, -10, -10, 0, 10); + testResizer(W/2, 0, -10, 0, 0, 0); + testResizer(W/2, 0, -10, 10, 0, -10); + testResizer(W/2, 0, 0, -10, 0, 10); + testResizer(W/2, 0, 0, 0, 0, 0); + testResizer(W/2, 0, 0, 10, 0, -10); + testResizer(W/2, 0, 10, -10, 0, 10); + testResizer(W/2, 0, 10, 0, 0, 0); + testResizer(W/2, 0, 10, 10, 0, -10); + + // top right resizer + testResizer( W, 0, -10, -10, -10 * reversedGrowth, 10); + testResizer( W, 0, -10, 0, -10 * ignoredGrowth, 0); + testResizer( W, 0, -10, 10, -10, -10); + testResizer( W, 0, 0, -10, 10 * needlessGrowth, 10); + testResizer( W, 0, 0, 0, 0, 0); + testResizer( W, 0, 0, 10, 0, -10 * ignoredGrowth); + testResizer( W, 0, 10, -10, 10, 10); + testResizer( W, 0, 10, 0, 10, 10 * needlessGrowth); + testResizer( W, 0, 10, 10, 10, -10 * reversedGrowth); + + // right resizer + testResizer( W, H/2, -10, -10, -10, 0); + testResizer( W, H/2, -10, 0, -10, 0); + testResizer( W, H/2, -10, 10, -10, 0); + testResizer( W, H/2, 0, -10, 0, 0); + testResizer( W, H/2, 0, 0, 0, 0); + testResizer( W, H/2, 0, 10, 0, 0); + testResizer( W, H/2, 10, -10, 10, 0); + testResizer( W, H/2, 10, 0, 10, 0); + testResizer( W, H/2, 10, 10, 10, 0); + + // bottom right resizer + testResizer( W, H, -10, -10, -10, -10); + testResizer( W, H, -10, 0, -10 * ignoredGrowth, 0); + testResizer( W, H, -10, 10, -10 * reversedGrowth, 10); + testResizer( W, H, 0, -10, 0, -10 * ignoredGrowth); + testResizer( W, H, 0, 0, 0, 0); + testResizer( W, H, 0, 10, 10 * needlessGrowth, 10); + testResizer( W, H, 10, -10, 10, -10 * reversedGrowth); + testResizer( W, H, 10, 0, 10, 10 * needlessGrowth); + testResizer( W, H, 10, 10, 10, 10); + + // bottom resizer + testResizer(W/2, H, -10, -10, 0, -10); + testResizer(W/2, H, -10, 0, 0, 0); + testResizer(W/2, H, -10, 10, 0, 10); + testResizer(W/2, H, 0, -10, 0, -10); + testResizer(W/2, H, 0, 0, 0, 0); + testResizer(W/2, H, 0, 10, 0, 10); + testResizer(W/2, H, 10, -10, 0, -10); + testResizer(W/2, H, 10, 0, 0, 0); + testResizer(W/2, H, 10, 10, 0, 10); + + // bottom left resizer + testResizer( 0, H, -10, -10, 10, -10 * reversedGrowth); + testResizer( 0, H, -10, 0, 10, 10 * needlessGrowth); + testResizer( 0, H, -10, 10, 10, 10); + testResizer( 0, H, 0, -10, 0, -10 * ignoredGrowth); + testResizer( 0, H, 0, 0, 0, 0); + testResizer( 0, H, 0, 10, 10 * needlessGrowth, 10); + testResizer( 0, H, 10, -10, -10, -10); + testResizer( 0, H, 10, 0, -10 * ignoredGrowth, 0); + testResizer( 0, H, 10, 10, -10 * reversedGrowth, 10); + + // left resizer + testResizer( 0, H/2, -10, -10, 10, 0); + testResizer( 0, H/2, -10, 0, 10, 0); + testResizer( 0, H/2, -10, 10, 10, 0); + testResizer( 0, H/2, 0, -10, 0, 0); + testResizer( 0, H/2, 0, 0, 0, 0); + testResizer( 0, H/2, 0, 10, 0, 0); + testResizer( 0, H/2, 10, -10, -10, 0); + testResizer( 0, H/2, 10, 0, -10, 0); + testResizer( 0, H/2, 10, 10, -10, 0); + + // top left resizer + testResizer( 0, 0, -10, -10, 10, 10); + testResizer( 0, 0, -10, 0, 10, 10 * needlessGrowth); + testResizer( 0, 0, -10, 10, 10, -10 * reversedGrowth); + testResizer( 0, 0, 0, -10, 10 * needlessGrowth, 10); + testResizer( 0, 0, 0, 0, 0, 0); + testResizer( 0, 0, 0, 10, 0, -10 * ignoredGrowth); + testResizer( 0, 0, 10, -10, -10 * reversedGrowth, 10); + testResizer( 0, 0, 10, 0, -10 * ignoredGrowth, 0); + testResizer( 0, 0, 10, 10, -10, -10); + } + SpecialPowers.pushPrefEnv({"set": [["editor.resizing.preserve_ratio", false]]}, function() { + runTests(false); + SpecialPowers.pushPrefEnv({"set": [["editor.resizing.preserve_ratio", true]]}, function() { + runTests(true); + SimpleTest.finish(); + }); + }); + }); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug641466.html b/editor/libeditor/tests/test_bug641466.html new file mode 100644 index 000000000..4a77b0b89 --- /dev/null +++ b/editor/libeditor/tests/test_bug641466.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=641466 +--> +<head> + <title>Test for Bug 641466</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=641466">Mozilla Bug 641466</a> +<p id="display"></p> +<div id="content"> +<input value="𐑑𐑧𐑕𐑑"> +<textarea>𐑑𐑧𐑕𐑑</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 641466 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + function doTest(element) { + element.focus(); + element.selectionStart = 4; + element.selectionEnd = 4; + synthesizeKey("VK_BACK_SPACE", {}); + synthesizeKey("VK_BACK_SPACE", {}); + synthesizeKey("VK_BACK_SPACE", {}); + synthesizeKey("VK_BACK_SPACE", {}); + + ok(element.value, "", "4 backspaces should delete all of the characters in the " + element.localName); + } + + doTest(document.querySelector("input")); + doTest(document.querySelector("textarea")); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug645914.html b/editor/libeditor/tests/test_bug645914.html new file mode 100644 index 000000000..cdf799e56 --- /dev/null +++ b/editor/libeditor/tests/test_bug645914.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=645914 +--> +<head> + <title>Test for Bug 645914</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=645914">Mozilla Bug 645914</a> +<p id="display"></p> +<div id="content"> +<textarea>foo +bar</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 645914 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["layout.word_select.eat_space_to_next_word", true], + ["browser.triple_click_selects_paragraph", false]]}, startTest); +}); +function startTest() { + var textarea = document.querySelector("textarea"); + textarea.selectionStart = textarea.selectionEnd = 0; + + // Simulate a double click on foo + synthesizeMouse(textarea, 5, 5, {clickCount: 2}); + + ok(true, "Testing word selection"); + is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text"); + is(textarea.selectionEnd, 3, "The end of the selection should not include a newline character"); + + textarea.selectionStart = textarea.selectionEnd = 0; + + // Simulate a triple click on foo + synthesizeMouse(textarea, 5, 5, {clickCount: 3}); + + ok(true, "Testing line selection"); + is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text"); + is(textarea.selectionEnd, 3, "The end of the selection should not include a newline character"); + + textarea.selectionStart = textarea.selectionEnd = 0; + textarea.value = "Very very long value which would eventually overflow the visible section of the textarea"; + + // Simulate a quadruple click on Very + synthesizeMouse(textarea, 5, 5, {clickCount: 4}); + + ok(true, "Testing paragraph selection"); + is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text"); + is(textarea.selectionEnd, textarea.value.length, "The end of the selection should be the end of the paragraph"); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug646194.html b/editor/libeditor/tests/test_bug646194.html new file mode 100644 index 000000000..8a0e4a829 --- /dev/null +++ b/editor/libeditor/tests/test_bug646194.html @@ -0,0 +1,38 @@ +<!doctype html> +<title>Mozilla Bug 646194</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=646194" + target="_blank">Mozilla Bug 646194</a> +<iframe id="i" src="data:text/html,<div contenteditable=true id=t>test me now</div>"></iframe> +<script> +SimpleTest.expectAssertions(1); + +function runTest() { + var i = document.getElementById("i"); + i.focus(); + var win = i.contentWindow; + var doc = i.contentDocument; + var t = doc.getElementById("t"); + t.focus(); + // put the caret at the end + win.getSelection().collapse(t.firstChild, 11); + + // Simulate pression Option+Delete on Mac + // We do things this way because not every platform can invoke this + // command using the available key bindings. + SpecialPowers.doCommand(window, "cmd_wordPrevious"); + SpecialPowers.doCommand(window, "cmd_wordPrevious"); + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + + // If we reach here, we haven't crashed. Phew! + // But let's check the value too, now that we're here. + is(t.textContent, "me now", "The command has worked correctly"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> diff --git a/editor/libeditor/tests/test_bug668599.html b/editor/libeditor/tests/test_bug668599.html new file mode 100644 index 000000000..8405d08ab --- /dev/null +++ b/editor/libeditor/tests/test_bug668599.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=668599 +--> +<head> + <title>Test for Bug 668599</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668599">Mozilla Bug 668599</a> +<p id="display"></p> +<div id="content"> + <div id="test1"> + block <span contenteditable>type here</span> block + </div> + <div id="test2"> + <p contenteditable> + block <span>type here</span> block + </p> + </div> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 668599 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function select(element) { + // select the element text content + var userSelection = window.getSelection(); + window.getSelection().removeAllRanges(); + var range = document.createRange(); + range.setStart(element.firstChild, 0); + range.setEnd(element.firstChild, element.textContent.length); + userSelection.addRange(range); +}; + +function runTests() { + var span = document.querySelector("#test1 span"); + + // editable <span> => the <span> *content* should be deleted + select(span); + span.focus(); + synthesizeKey("x", {}); + is(span.textContent, "x", "The <span> content should have been replaced by 'x'."); + + // same thing, but using [Del] instead of typing some text + document.execCommand("Undo", false, null); + select(span); + span.focus(); + synthesizeKey("VK_DELETE", {}); + is(span.textContent, "", "The <span> content should have been deleted."); + + // <span> in editable block => the <span> *element* should be deleted + select(document.querySelector("#test2 span")); + document.querySelector("#test2 [contenteditable]").focus(); + synthesizeKey("VK_DELETE", {}); + is(document.querySelector("#test2 span"), null, + "The <span> element should have been deleted."); + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug674770-1.html b/editor/libeditor/tests/test_bug674770-1.html new file mode 100644 index 000000000..4ba65f507 --- /dev/null +++ b/editor/libeditor/tests/test_bug674770-1.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=674770 +--> +<head> + <title>Test for Bug 674770</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674770">Mozilla Bug 674770</a> +<p id="display"></p> +<div id="content"> +<a href="file_bug674770-1.html" id="link1">test</a> +<div contenteditable> +<a href="file_bug674770-1.html" id="link2">test</a> +</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["middlemouse.paste", true], ["dom.ipc.processCount", 1]]}, startTests); +}); + +function startTests() { + var tests = [ + { description: "Testing link in <div>: ", + target: function () { return document.querySelector("#link1"); }, + linkShouldWork: true }, + { description: "Testing link in <div contenteditable>: ", + target: function () { return document.querySelector("#link2"); }, + linkShouldWork: false }, + ]; + var currentTest; + function runNextTest() { + localStorage.removeItem("clicked"); + currentTest = tests.shift(); + if (!currentTest) { + SimpleTest.finish(); + return; + } + ok(true, currentTest.description + "Starting to test..."); + synthesizeMouseAtCenter(currentTest.target(), { button: 1 }); + } + + + addEventListener("storage", function(e) { + is(e.key, "clicked", currentTest.description + "Key should always be 'clicked'"); + is(e.newValue, "true", currentTest.description + "Value should always be 'true'"); + ok(currentTest.linkShouldWork, currentTest.description + "The click operation on the link " + (currentTest.linkShouldWork ? "should work" : "shouldn't work")); + SimpleTest.executeSoon(runNextTest); + }, false); + + SpecialPowers.addSystemEventListener(window, "click", function (aEvent) { + // When the click event should cause default action, e.g., opening the link, + // the event shouldn't have been consumed except the link handler. + // However, in e10s mode, it's not consumed during propagating the event but + // in non-e10s mode, it's consumed during the propagation. Therefore, + // we cannot check defaultPrevented value when the link should work as is + // if there is no way to detect if it's running in e10s mode or not. + // So, let's skip checking Event.defaultPrevented value when the link should + // work. In such case, we should receive "storage" event later. + if (currentTest.linkShouldWork) { + return; + } + + ok(SpecialPowers.defaultPreventedInAnyGroup(aEvent), + currentTest.description + "The default action should be consumed because the link should work as is"); + if (SpecialPowers.defaultPreventedInAnyGroup(aEvent)) { + // In this case, "storage" event won't be fired. + SimpleTest.executeSoon(runNextTest); + } + }, false); + + SimpleTest.executeSoon(runNextTest); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug674770-2.html b/editor/libeditor/tests/test_bug674770-2.html new file mode 100644 index 000000000..c69311e95 --- /dev/null +++ b/editor/libeditor/tests/test_bug674770-2.html @@ -0,0 +1,395 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=674770 +--> +<head> + <title>Test for Bug 674770</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674770">Mozilla Bug 674770</a> +<p id="display"></p> +<div id="content"> +<iframe id="iframe" style="display: block; height: 14em;" + src="data:text/html,<input id='text' value='pasted'><input id='input' style='display: block;'><p id='editor1' contenteditable>editor1:</p><p id='editor2' contenteditable>editor2:</p>"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 674770 **/ +SimpleTest.waitForExplicitFinish(); + +var iframe = document.getElementById("iframe"); +var frameWindow, frameDocument; + +var gClicked = false; +var gClicking = null; +var gDoPreventDefault1 = null; +var gDoPreventDefault2 = null; + +function clickEventHandler(aEvent) +{ + if (aEvent.button == 1 && aEvent.target == gClicking) { + gClicked = true; + } + if (gDoPreventDefault1 == aEvent.target) { + aEvent.preventDefault(); + } + if (gDoPreventDefault2 == aEvent.target) { + aEvent.preventDefault(); + } +} + +// NOTE: tests need to check the result *after* the content is actually +// modified. Sometimes, the modification is delayed. Therefore, there +// are a lot of functions and SimpleTest.executeSoon()s. +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["middlemouse.contentLoadURL", false], + ["middlemouse.paste", true]]}, startTest); +}); +function startTest() { + frameWindow = iframe.contentWindow; + frameDocument = iframe.contentDocument; + + frameDocument.getElementById("input").addEventListener("click", clickEventHandler, false); + frameDocument.getElementById("editor1").addEventListener("click", clickEventHandler, false); + frameDocument.getElementById("editor2").addEventListener("click", clickEventHandler, false); + + var text = frameDocument.getElementById("text"); + + text.focus(); + if (navigator.platform.indexOf("Linux") == 0) { + synthesizeKey("a", { altKey: true }, frameWindow); + } else { + synthesizeKey("a", { accelKey: true }, frameWindow); + } + // Windows and Mac don't have primary selection, we should copy the text to + // the global clipboard. + if (!SpecialPowers.supportsSelectionClipboard()) { + SimpleTest.waitForClipboard("pasted", + function() { synthesizeKey("c", { accelKey: true }, frameWindow); }, + function() { SimpleTest.executeSoon(runInputTests1) }, + cleanup); + } else { + // Otherwise, don't call waitForClipboard since it breaks primary + // selection. + runInputTests1(); + } +} + +function runInputTests1() +{ + var input = frameDocument.getElementById("input"); + + // first, copy text. + + // when middle clicked in focused input element, text should be pasted. + input.value = ""; + input.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = input; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(input, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runInputTests1"); + is(input.value, "pasted", "failed to paste in input element"); + + SimpleTest.executeSoon(runInputTests2); + }); + }); +} + +function runInputTests2() +{ + var input = frameDocument.getElementById("input"); + + // even if the input element hasn't had focus, middle click should set focus + // to it and paste the text. + input.value = ""; + input.blur(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = input; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(input, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runInputTests2"); + is(input.value, "pasted", + "failed to paste in input element when it hasn't had focus yet"); + + SimpleTest.executeSoon(runInputTests3); + }); + }); +} + +function runInputTests3() +{ + var input = frameDocument.getElementById("input"); + var editor1 = frameDocument.getElementById("editor1"); + + // preventDefault() of HTML editor's click event handler shouldn't prevent + // middle click pasting in input element. + input.value = ""; + input.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = input; + gDoPreventDefault1 = editor1; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(input, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runInputTests3"); + is(input.value, "pasted", + "failed to paste in input element when editor1 does preventDefault()"); + + SimpleTest.executeSoon(runInputTests4); + }); + }); +} + +function runInputTests4() +{ + var input = frameDocument.getElementById("input"); + var editor1 = frameDocument.getElementById("editor1"); + + // preventDefault() of input element's click event handler should prevent + // middle click pasting in it. + input.value = ""; + input.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = input; + gDoPreventDefault1 = input; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(input, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runInputTests4"); + todo_is(input.value, "", + "pasted in input element when it does preventDefault()"); + + SimpleTest.executeSoon(runContentEditableTests1); + }); + }); +} + +function runContentEditableTests1() +{ + var editor1 = frameDocument.getElementById("editor1"); + + // when middle clicked in focused contentediable editor, text should be + // pasted. + editor1.innerHTML = "editor1:"; + editor1.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor1; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests1"); + is(editor1.innerHTML, "editor1:pasted", + "failed to paste text in contenteditable editor"); + SimpleTest.executeSoon(runContentEditableTests2); + }); + }); +} + +function runContentEditableTests2() +{ + var editor1 = frameDocument.getElementById("editor1"); + + // even if the contenteditable editor hasn't had focus, middle click should + // set focus to it and paste the text. + editor1.innerHTML = "editor1:"; + editor1.blur(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor1; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests2"); + is(editor1.innerHTML, "editor1:pasted", + "failed to paste in contenteditable editor #1 when it hasn't had focus yet"); + SimpleTest.executeSoon(runContentEditableTests3); + }); + }); +} + +function runContentEditableTests3() +{ + var editor1 = frameDocument.getElementById("editor1"); + var editor2 = frameDocument.getElementById("editor2"); + + // When editor1 has focus but editor2 is middle clicked, should be pasted + // in the editor2. + editor1.innerHTML = "editor1:"; + editor2.innerHTML = "editor2:"; + editor1.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor2; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor2, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests3"); + is(editor1.innerHTML, "editor1:", + "pasted in contenteditable editor #1 when editor2 is clicked"); + is(editor2.innerHTML, "editor2:pasted", + "failed to paste in contenteditable editor #2 when editor2 is clicked"); + SimpleTest.executeSoon(runContentEditableTests4); + }); + }); +} + +function runContentEditableTests4() +{ + var editor1 = frameDocument.getElementById("editor1"); + + // preventDefault() of editor1's click event handler should prevent + // middle click pasting in it. + editor1.innerHTML = "editor1:"; + editor1.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor1; + gDoPreventDefault1 = editor1; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests4"); + todo_is(editor1.innerHTML, "editor1:", + "pasted in contenteditable editor #1 when it does preventDefault()"); + SimpleTest.executeSoon(runContentEditableTests5); + }); + }); +} + +function runContentEditableTests5() +{ + var editor1 = frameDocument.getElementById("editor1"); + var editor2 = frameDocument.getElementById("editor2"); + + // preventDefault() of editor1's click event handler shouldn't prevent + // middle click pasting in editor2. + editor1.innerHTML = "editor1:"; + editor2.innerHTML = "editor2:"; + editor2.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor2; + gDoPreventDefault1 = editor1; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor2, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests5"); + is(editor1.innerHTML, "editor1:", + "pasted in contenteditable editor #1?"); + is(editor2.innerHTML, "editor2:pasted", + "failed to paste in contenteditable editor #2"); + + SimpleTest.executeSoon(initForBodyEditableDocumentTests); + }); + }); +} + +function initForBodyEditableDocumentTests() +{ + frameDocument.getElementById("input").removeEventListener("click", clickEventHandler, false); + frameDocument.getElementById("editor1").removeEventListener("click", clickEventHandler, false); + frameDocument.getElementById("editor2").removeEventListener("click", clickEventHandler, false); + + iframe.onload = + function (aEvent) { SimpleTest.executeSoon(runBodyEditableDocumentTests1); }; + iframe.src = + "data:text/html,<body contenteditable>body:</body>"; +} + +function runBodyEditableDocumentTests1() +{ + frameWindow = iframe.contentWindow; + frameDocument = iframe.contentDocument; + + var body = frameDocument.body; + + is(body.innerHTML, "body:", + "failed to initialize at runBodyEditableDocumentTests1"); + + // middle click on html element should cause pasting text in its body. + synthesizeMouseAtCenter(frameDocument.documentElement, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + is(body.innerHTML, + "body:pasted", + "failed to paste in editable body element when clicked on html element"); + + SimpleTest.executeSoon(runBodyEditableDocumentTests2); + }); +} + +function runBodyEditableDocumentTests2() +{ + frameDocument.body.innerHTML = "body:<span id='span' contenteditable='false'>non-editable</span>"; + + var body = frameDocument.body; + + is(body.innerHTML, "body:<span id=\"span\" contenteditable=\"false\">non-editable</span>", + "failed to initialize at runBodyEditableDocumentTests2"); + + synthesizeMouseAtCenter(frameDocument.getElementById("span"), {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + is(body.innerHTML, + "body:<span id=\"span\" contenteditable=\"false\">non-editable</span>", + "pasted when middle clicked in non-editable element"); + + SimpleTest.executeSoon(cleanup); + }); +} + +function cleanup() +{ + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug674861.html b/editor/libeditor/tests/test_bug674861.html new file mode 100644 index 000000000..5974b4aed --- /dev/null +++ b/editor/libeditor/tests/test_bug674861.html @@ -0,0 +1,185 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=674861 +--> +<head> + <title>Test for Bug 674861</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674861">Mozilla Bug 674861</a> +<p id="display"></p> +<div id="content"> + <section id="test1"> + <h2> Editable Bullet List </h2> + <ul contenteditable> + <li> item A </li> + <li> item B </li> + <li> item C </li> + </ul> + + <h2> Editable Ordered List </h2> + <ol contenteditable> + <li> item A </li> + <li> item B </li> + <li> item C </li> + </ol> + + <h2> Editable Definition List </h2> + <dl contenteditable> + <dt> term A </dt> + <dd> definition A </dd> + <dt> term B </dt> + <dd> definition B </dd> + <dt> term C </dt> + <dd> definition C </dd> + </dl> + </section> + + <section id="test2" contenteditable> + <h2> Bullet List In Editable Section </h2> + <ul> + <li> item A </li> + <li> item B </li> + <li> item C </li> + </ul> + + <h2> Ordered List In Editable Section </h2> + <ol> + <li> item A </li> + <li> item B </li> + <li> item C </li> + </ol> + + <h2> Definition List In Editable Section </h2> + <dl> + <dt> term A </dt> + <dd> definition A </dd> + <dt> term B </dt> + <dd> definition B </dd> + <dt> term C </dt> + <dd> definition C </dd> + </dl> + </section> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 674861 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +const CARET_BEGIN = 0; +const CARET_MIDDLE = 1; +const CARET_END = 2; + +function try2split(element, caretPos) { + // compute the requested position + var len = element.textContent.length; + var pos = -1; + switch (caretPos) { + case CARET_BEGIN: + pos = 0; + break; + case CARET_MIDDLE: + pos = Math.floor(len/2); + break; + case CARET_END: + pos = len; + break; + } + + // put the caret on the requested position + var sel = window.getSelection(); + for (var i = 0; i < sel.rangeCount; i++) { + var range = sel.getRangeAt(i); + sel.removeRange(range); + } + range = document.createRange(); + range.setStart(element.firstChild, pos); + range.setEnd(element.firstChild, pos); + sel.addRange(range); + + // simulates two [Return] keypresses + synthesizeKey("VK_RETURN", {}); + synthesizeKey("VK_RETURN", {}); +} + +function runTests() { + const test1 = document.getElementById("test1"); + const test2 = document.getElementById("test2"); + + // ----------------------------------------------------------------------- + // #test1: editable lists should NOT be splittable + // ----------------------------------------------------------------------- + const ul = test1.querySelector("ul"); + const ol = test1.querySelector("ol"); + const dl = test1.querySelector("dl"); + + // bullet list + ul.focus(); + try2split(ul.querySelector("li"), CARET_END); + is(test1.querySelectorAll("ul").length, 1, + "The <ul contenteditable> list should not be splittable."); + is(ul.querySelectorAll("li").length, 5, + "Two new <li> elements should have been created."); + + // ordered list + ol.focus(); + try2split(ol.querySelector("li"), CARET_END); + is(test1.querySelectorAll("ol").length, 1, + "The <ol contenteditable> list should not be splittable."); + is(ol.querySelectorAll("li").length, 5, + "Two new <li> elements should have been created."); + + // definition list + dl.focus(); + try2split(dl.querySelector("dd"), CARET_END); + is(test1.querySelectorAll("dl").length, 1, + "The <dl contenteditable> list should not be splittable."); + is(dl.querySelectorAll("dt").length, 5, + "Two new <dt> elements should have been created."); + + // ----------------------------------------------------------------------- + // #test2: lists in editable blocks should be splittable + // ----------------------------------------------------------------------- + test2.focus(); + + // bullet list + try2split(test2.querySelector("ul li"), CARET_END); + is(test2.querySelectorAll("ul").length, 2, + "The <ul> list should have been splitted."); + is(test2.querySelectorAll("ul li").length, 3, + "No new <li> element should have been created."); + is(test2.querySelectorAll("ul+p").length, 1, + "A new paragraph should have been created."); + + // ordered list + try2split(test2.querySelector("ol li"), CARET_END); + is(test2.querySelectorAll("ol").length, 2, + "The <ol> list should have been splitted."); + is(test2.querySelectorAll("ol li").length, 3, + "No new <li> element should have been created."); + is(test2.querySelectorAll("ol+p").length, 1, + "A new paragraph should have been created."); + + // definition list + try2split(test2.querySelector("dl dd"), CARET_END); + is(test2.querySelectorAll("dl").length, 2, + "The <dl> list should have been splitted."); + is(test2.querySelectorAll("dt").length, 3, + "No new <dt> element should have been created."); + is(test2.querySelectorAll("dl+p").length, 1, + "A new paragraph should have been created."); + + // done + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug676401.html b/editor/libeditor/tests/test_bug676401.html new file mode 100644 index 000000000..aa468fdc6 --- /dev/null +++ b/editor/libeditor/tests/test_bug676401.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=676401 +--> +<head> + <title>Test for Bug 676401</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=676401">Mozilla Bug 676401</a> +<p id="display"></p> +<div id="content"> + <!-- we need a blockquote to test the "outdent" command --> + <section> + <blockquote> not editable </blockquote> + </section> + <section contenteditable> + <blockquote> editable </blockquote> + </section> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 676401 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +var gBlock1, gBlock2; + +var alwaysEnabledCommands = [ + "contentReadOnly", + "copy", + "cut", + "enableInlineTableEditing", + "enableObjectResizing", + "insertBrOnReturn", + "selectAll", + "styleWithCSS", +]; + +function IsCommandEnabled(command) { + var enabled; + + // non-editable div: should return false unless alwaysEnabled + window.getSelection().selectAllChildren(gBlock1); + enabled = document.queryCommandEnabled(command); + is(enabled, alwaysEnabledCommands.indexOf(command) != -1, + "'" + command + "' should not be enabled on a non-editable block."); + + // editable div: should return true + window.getSelection().selectAllChildren(gBlock2); + enabled = document.queryCommandEnabled(command); + is(enabled, true, "'" + command + "' should be enabled on an editable block."); +} + +function runTests() { + var i, commands; + gBlock1 = document.querySelector("#content section blockquote"); + gBlock2 = document.querySelector("#content [contenteditable] blockquote"); + + // common commands: test with and without "styleWithCSS" + commands = [ + "bold", "italic", "underline", "strikeThrough", + "subscript", "superscript", "foreColor", "backColor", "hiliteColor", + "fontName", "fontSize", + "justifyLeft", "justifyCenter", "justifyRight", "justifyFull", + "indent", "outdent", + "insertOrderedList", "insertUnorderedList", "insertParagraph", + "heading", "formatBlock", + "contentReadOnly", "createLink", + "decreaseFontSize", "increaseFontSize", + "insertHTML", "insertHorizontalRule", "insertImage", + "removeFormat", "selectAll", "styleWithCSS" + ]; + document.execCommand("styleWithCSS", false, false); + for (i = 0; i < commands.length; i++) + IsCommandEnabled(commands[i]); + document.execCommand("styleWithCSS", false, true); + for (i = 0; i < commands.length; i++) + IsCommandEnabled(commands[i]); + + // Mozilla-specific stuff + commands = ["enableInlineTableEditing", "enableObjectResizing", "insertBrOnReturn"]; + for (i = 0; i < commands.length; i++) + IsCommandEnabled(commands[i]); + + // These are privileged, and available only to chrome. + commands = ["paste"]; + for (i = 0; i < commands.length; i++) { + is(document.queryCommandEnabled(commands[i]), false, + "Command should not be enabled for non-privileged code"); + is(SpecialPowers.wrap(document).queryCommandEnabled(commands[i]), true, + "Command should be enabled for privileged code"); + is(document.execCommand(commands[i], false, false), false, "Should return false: " + commands[i]); + is(SpecialPowers.wrap(document).execCommand(commands[i], false, false), true, "Should return true: " + commands[i]); + } + + // delete/undo/redo -- we have to execute this commands because: + // * there's nothing to undo if we haven't modified the selection first + // * there's nothing to redo if we haven't undone something first + commands = ["delete", "undo", "redo"]; + for (i = 0; i < commands.length; i++) { + IsCommandEnabled(commands[i]); + document.execCommand(commands[i], false, false); + } + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug677752.html b/editor/libeditor/tests/test_bug677752.html new file mode 100644 index 000000000..8809c1ead --- /dev/null +++ b/editor/libeditor/tests/test_bug677752.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=677752 +--> +<head> + <title>Test for Bug 677752</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=677752">Mozilla Bug 677752</a> +<p id="display"></p> +<div id="content"> + <section contenteditable> foo bar </section> + <div contenteditable> foo bar </div> + <p contenteditable> foo bar </p> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 677752 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function selectEditor(aEditor) { + aEditor.focus(); + var selection = window.getSelection(); + selection.selectAllChildren(aEditor); + selection.collapseToStart(); +} + +function runTests() { + var editor, node, initialHTML; + document.execCommand('styleWithCSS', false, true); + + // editable <section> + editor = document.querySelector("section[contenteditable]"); + initialHTML = editor.innerHTML; + selectEditor(editor); + // editable <section>: justify + document.execCommand("justifyright", false, null); + node = editor.querySelector("*"); + is(node.nodeName.toLowerCase(), "div", "'justifyright' should create a <div> in the editable <section>."); + is(node.style.textAlign, "right", "'justifyright' should create a 'text-align: right' CSS rule."); + document.execCommand("undo", false, null); + // editable <section>: indent + document.execCommand("indent", false, null); + node = editor.querySelector("*"); + is(node.nodeName.toLowerCase(), "div", "'indent' should create a <div> in the editable <section>."); + is(node.style.marginLeft, "40px", "'indent' should create a 'margin-left: 40px' CSS rule."); + // editable <section>: undo with outdent + // this should remove the whole <div> but only removing the CSS rule would be acceptable, too + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "'outdent' should undo the 'indent' action."); + // editable <section>: outdent again + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "another 'outdent' should not modify the <section> element."); + + // editable <div> + editor = document.querySelector("div[contenteditable]"); + initialHTML = editor.innerHTML; + selectEditor(editor); + // editable <div>: justify + document.execCommand("justifyright", false, null); + node = editor.querySelector("*"); + is(node.nodeName.toLowerCase(), "div", "'justifyright' should create a <div> in the editable <div>."); + is(node.style.textAlign, "right", "'justifyright' should create a 'text-align: right' CSS rule."); + document.execCommand("undo", false, null); + // editable <div>: indent + document.execCommand("indent", false, null); + node = editor.querySelector("*"); + is(node.nodeName.toLowerCase(), "div", "'indent' should create a <div> in the editable <div>."); + is(node.style.marginLeft, "40px", "'indent' should create a 'margin-left: 40px' CSS rule."); + // editable <div>: undo with outdent + // this should remove the whole <div> but only removing the CSS rule would be acceptable, too + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "'outdent' should undo the 'indent' action."); + // editable <div>: outdent again + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "another 'outdent' should not modify the <div> element."); + + // editable <p> + // all block-level commands should be ignored (<p><div/></p> is not valid) + editor = document.querySelector("p[contenteditable]"); + initialHTML = editor.innerHTML; + selectEditor(editor); + // editable <p>: justify + document.execCommand("justifyright", false, null); + is(editor.innerHTML, initialHTML, "'justifyright' should have no effect on <p>."); + // editable <p>: indent + document.execCommand("indent", false, null); + is(editor.innerHTML, initialHTML, "'indent' should have no effect on <p>."); + // editable <p>: outdent + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "'outdent' should have no effect on <p>."); + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug681229.html b/editor/libeditor/tests/test_bug681229.html new file mode 100644 index 000000000..6debcfde7 --- /dev/null +++ b/editor/libeditor/tests/test_bug681229.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=681229 +--> +<head> + <title>Test for Bug 681229</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=681229">Mozilla Bug 681229</a> +<p id="display"></p> +<div id="content"> +<textarea spellcheck="false"></textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 681229 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.focus(); + + const kValue = "a\r\nb"; + const kExpectedValue = (navigator.platform.indexOf("Win") == 0) ? + "a\nb" : kValue; + + SimpleTest.waitForClipboard(kExpectedValue, + function() { + SpecialPowers.clipboardCopyString(kValue); + }, + function() { + synthesizeKey("V", {accelKey: true}); + is(t.value, "a\nb", "The carriage return has been correctly sanitized"); + SimpleTest.finish(); + }, + function() { + SimpleTest.finish(); + } + ); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug686203.html b/editor/libeditor/tests/test_bug686203.html new file mode 100644 index 000000000..c1a856aae --- /dev/null +++ b/editor/libeditor/tests/test_bug686203.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=686203 +--> + +<head> + <title>Test for Bug 686203</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=686203">Mozilla Bug 686203</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 686203 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var ce = document.getElementById("ce"); + var input = document.getElementById("input"); + ce.focus(); + + var eventDetails = { button : 2 }; + synthesizeMouseAtCenter(input, eventDetails); + + synthesizeKey("Z", {}); + + /* check values */ + is(input.value, "Z", "input correctly focused after right-click"); + is(ce.textContent, "abc", "contenteditable correctly blurred after right-click on input"); + + SimpleTest.finish(); + }); + </script> + </pre> + + <input type="text" value="" id="input" /> + <div id="ce" contenteditable="true">abc</div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug692520.html b/editor/libeditor/tests/test_bug692520.html new file mode 100644 index 000000000..6dfefd8db --- /dev/null +++ b/editor/libeditor/tests/test_bug692520.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=692520 +--> +<head> + <title>Test for Bug 692520</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=692520">Mozilla Bug 692520</a> +<p id="display"></p> +<div id="content"> +<textarea></textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 692520 **/ +function test(prop, value) { + var t = document.querySelector("textarea"); + t.value = "testing"; + t.selectionStart = 1; + t.selectionEnd = 3; + t.selectionDirection = "backward"; + t.style.display = ""; + document.body.clientWidth; + t.style.display = "none"; + is(t[prop], value, "Correct value for the " + prop + " property"); +} + +test("selectionStart", 1); +test("selectionEnd", 3); +test("selectionDirection", "backward"); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug697842.html b/editor/libeditor/tests/test_bug697842.html new file mode 100644 index 000000000..463ff76dc --- /dev/null +++ b/editor/libeditor/tests/test_bug697842.html @@ -0,0 +1,117 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=697842 +--> +<head> + <title>Test for Bug 697842</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <p id="editor" contenteditable style="min-height: 1.5em;"></p> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 697842 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() +{ + var editor = document.getElementById("editor"); + editor.focus(); + + SimpleTest.executeSoon(function() { + var composingString = ""; + + function handler(aEvent) { + switch (aEvent.type) { + case "compositionstart": + // Selected string at starting composition must be empty in this test. + is(aEvent.data, "", "mismatch selected string"); + break; + case "compositionupdate": + case "compositionend": + is(aEvent.data, composingString, "mismatch composition string"); + break; + default: + break; + } + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + + editor.addEventListener("compositionstart", handler, true); + editor.addEventListener("compositionend", handler, true); + editor.addEventListener("compositionupdate", handler, true); + editor.addEventListener("text", handler, true); + + // input first character + composingString = "\u306B"; + synthesizeCompositionChange( + { "composition": + { "string": composingString, + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + // input second character + composingString = "\u306B\u3085"; + synthesizeCompositionChange( + { "composition": + { "string": composingString, + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + // convert them + synthesizeCompositionChange( + { "composition": + { "string": composingString, + "clauses": + [ + { "length": 2, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + synthesizeComposition({ type: "compositioncommitasis" }); + + is(editor.innerHTML, composingString, + "editor has unexpected result"); + + editor.removeEventListener("compositionstart", handler, true); + editor.removeEventListener("compositionend", handler, true); + editor.removeEventListener("compositionupdate", handler, true); + editor.removeEventListener("text", handler, true); + + SimpleTest.finish(); + }); +} + + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug725069.html b/editor/libeditor/tests/test_bug725069.html new file mode 100644 index 000000000..5096ede3c --- /dev/null +++ b/editor/libeditor/tests/test_bug725069.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=725069 +--> +<head> + <title>Test for Bug 725069</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body contenteditable>abc<!-- XXX -->def<span></span> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=725069">Mozilla Bug 725069</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 725069 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var body = document.querySelector("body"); + is(body.firstChild.nodeType, body.TEXT_NODE, "The first node is a text node"); + is(body.firstChild.nodeValue, "abc", "The first text node is there"); + is(body.firstChild.nextSibling.nodeType, body.COMMENT_NODE, "The second node is a comment node"); + is(body.firstChild.nextSibling.nodeValue, " XXX ", "The value of the comment node is not changed"); + is(body.firstChild.nextSibling.nextSibling.nodeType, body.TEXT_NODE, "The last text node is a text node"); + is(body.firstChild.nextSibling.nextSibling.nodeValue, "def", "The last next node is there"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug735059.html b/editor/libeditor/tests/test_bug735059.html new file mode 100644 index 000000000..3b81ce48b --- /dev/null +++ b/editor/libeditor/tests/test_bug735059.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=735059 +--> +<title>Test for Bug 735059</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=735059">Mozilla Bug 735059</a> +<div id="display" contenteditable>foo</div> +<pre id="test"> +<script> +/** Test for Bug 735059 **/ + +// Value defaults to the empty string, which evaluates to true, so this +// disables CSS styling +document.execCommand("usecss"); +getSelection().selectAllChildren(document.getElementById("display")); +document.execCommand("bold"); +is(document.getElementById("display").innerHTML, "<b>foo</b>", + "execCommand() needs to work with only one parameter"); +</script> +</pre> diff --git a/editor/libeditor/tests/test_bug738366.html b/editor/libeditor/tests/test_bug738366.html new file mode 100644 index 000000000..a54aec7a2 --- /dev/null +++ b/editor/libeditor/tests/test_bug738366.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=738366 +--> +<title>Test for Bug 738366</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=738366">Mozilla Bug 738366</a> +<div id="display" contenteditable>foobarbaz</div> +<script> +/** Test for Bug 738366 **/ + +getSelection().collapse(document.getElementById("display").firstChild, 3); +getSelection().extend(document.getElementById("display").firstChild, 6); +document.execCommand("bold"); +is(document.getElementById("display").innerHTML, "foo<b>bar</b>baz", + "styleWithCSS must default to false"); +document.execCommand("stylewithcss", false, "true"); +document.execCommand("bold"); +document.execCommand("bold"); +is(document.getElementById("display").innerHTML, + 'foo<span style="font-weight: bold;">bar</span>baz', + "styleWithCSS must be settable to true"); +</script> diff --git a/editor/libeditor/tests/test_bug740784.html b/editor/libeditor/tests/test_bug740784.html new file mode 100644 index 000000000..26c918241 --- /dev/null +++ b/editor/libeditor/tests/test_bug740784.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=740784 +--> + +<head> + <title>Test for Bug 740784</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=740784">Mozilla Bug 740784</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 740784 **/ + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var t1 = $("t1"); + + t1.focus(); + synthesizeKey("VK_END", {}); + synthesizeKey("VK_BACK_SPACE", {}); + synthesizeKey("Z", {accelKey: true}); + + // Was the former bogus node changed to a mozBR? + is(t1.value, "a", "trailing <br> correctly ignored"); + + SimpleTest.finish(); + }); + </script> + </pre> + + <textarea id="t1" rows="2" columns="80">a</textarea> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug742261.html b/editor/libeditor/tests/test_bug742261.html new file mode 100644 index 000000000..9ad41dd52 --- /dev/null +++ b/editor/libeditor/tests/test_bug742261.html @@ -0,0 +1,14 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=742261 +--> +<title>Test for Bug 742261</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<body> +<script> +is(document.execCommandShowHelp, undefined, + "execCommandShowHelp shouldn't exist"); +is(document.queryCommandText, undefined, + "queryCommandText shouldn't exist"); +</script> diff --git a/editor/libeditor/tests/test_bug757371.html b/editor/libeditor/tests/test_bug757371.html new file mode 100644 index 000000000..5ca41a595 --- /dev/null +++ b/editor/libeditor/tests/test_bug757371.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=757371 +--> +<title>Test for Bug 757371</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=757371">Mozilla Bug 757371</a> +<div contenteditable></div> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.querySelector("div"); + div.focus(); + getSelection().collapse(div, 0); + document.execCommand("bold"); + sendString("ab"); + sendKey("BACK_SPACE"); + sendChar("b"); + + is(div.innerHTML, "<b>ab</b>"); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug757771.html b/editor/libeditor/tests/test_bug757771.html new file mode 100644 index 000000000..9ef980b66 --- /dev/null +++ b/editor/libeditor/tests/test_bug757771.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=757771 +--> +<title>Test for Bug 757771</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=757771">Mozilla Bug 757771</a> +<input value=foo maxlength=4> +<input type=password value=password> +<script> +/** Test for Bug 757771 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var textInput = document.querySelector("input"); + textInput.focus(); + textInput.select(); + sendString("abcde"); + + var passwordInput = document.querySelector("input + input"); + passwordInput.focus(); + passwordInput.select(); + sendString("hunter2"); + + ok(true, "No real tests, just crashes/asserts"); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug767684.html b/editor/libeditor/tests/test_bug767684.html new file mode 100644 index 000000000..0e65a88a7 --- /dev/null +++ b/editor/libeditor/tests/test_bug767684.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=767684 +--> +<title>Test for Bug 767684</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=767684">Mozilla Bug 767684</a> +<div contenteditable>foo<b>bar</b>baz</div> +<script> +getSelection().selectAllChildren(document.querySelector("div")); +document.execCommand("increaseFontSize"); +is(document.querySelector("div").innerHTML, "<big>foo<b>bar</b>baz</big>", + "All selected text must be embiggened"); +</script> diff --git a/editor/libeditor/tests/test_bug772796.html b/editor/libeditor/tests/test_bug772796.html new file mode 100644 index 000000000..9a15dccd2 --- /dev/null +++ b/editor/libeditor/tests/test_bug772796.html @@ -0,0 +1,223 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=772796 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 772796</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> .pre { white-space: pre } </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 772796</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="editable" contenteditable></div> + +<pre id="test"> + + <script type="application/javascript"> + var tests = [ +/*00*/[ "<div>test</div><pre>foobar\nbaz</pre>", "<div>testfoobar\n</div><pre>baz</pre>" ], +/*01*/[ "<div>test</div><pre><b>foobar\nbaz</b></pre>", "<div>test<b>foobar\n</b></div><pre><b>baz</b></pre>" ], +/*02*/[ "<div>test</div><pre><b>foo</b>bar\nbaz</pre>", "<div>test<b>foo</b>bar\n</div><pre>baz</pre>" ], +/*03*/[ "<div>test</div><pre><b>foo</b>\nbar</pre>", "<div>test<b>foo</b>\n</div><pre>bar</pre>" ], +/*04*/[ "<div>test</div><pre><b>foo\n</b>bar\nbaz</pre>", "<div>test<b>foo\n</b></div><pre>bar\nbaz</pre>" ], + /* The <br> after the foobar is unfortunate but is behaviour that hasn't changed in bug 772796. */ +/*05*/[ "<div>test</div><pre>foobar<br>baz</pre>", "<div>testfoobar<br></div><pre>baz</pre>" ], +/*06*/[ "<div>test</div><pre><b>foobar<br>baz</b></pre>", "<div>test<b>foobar</b><br></div><pre><b>baz</b></pre>" ], + + /* + * Some tests with block elements. + * Tests 07, 09 and 11 don't use "MoveBlock", they use "JoinNodesSmart". + * Test 11 is a pain: <div>foo\bar</div> is be joined to "test", losing the visible line break. + */ +/*07*/[ "<div>test</div><pre><div>foobar</div>baz</pre>", "<div>testfoobar</div><pre>baz</pre>" ], +/*08*/[ "<div>test</div><pre>foobar<div>baz</div></pre>", "<div>testfoobar</div><pre><div>baz</div></pre>" ], +/*09*/[ "<div>test</div><pre><div>foobar</div>baz\nfred</pre>", "<div>testfoobar</div><pre>baz\nfred</pre>" ], +/*10*/[ "<div>test</div><pre>foobar<div>baz</div>\nfred</pre>", "<div>testfoobar</div><pre><div>baz</div>\nfred</pre>" ], +/*11*/[ "<div>test</div><pre><div>foo\nbar</div>baz\nfred</pre>", "<div>testfoo\nbar</div><pre>baz\nfred</pre>" ], // BAD +/*12*/[ "<div>test</div><pre>foo<div>bar</div>baz\nfred</pre>", "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>" ], + + /* + * Repeating all tests above with the <pre> on a new line. + * We know that backspace doesn't work (bug 1190161). Third argument shows the current outcome. + */ +/*13-00*/[ "<div>test</div>\n<pre>foobar\nbaz</pre>", "<div>testfoobar\n</div><pre>baz</pre>", + "<div>test</div>foobar\n<pre>baz</pre>" ], +/*14-01*/[ "<div>test</div>\n<pre><b>foobar\nbaz</b></pre>", "<div>test<b>foobar\n</b></div><pre><b>baz</b></pre>", + "<div>test</div><b>foobar\n</b><pre><b>baz</b></pre>" ], +/*15-02*/[ "<div>test</div>\n<pre><b>foo</b>bar\nbaz</pre>", "<div>test<b>foo</b>bar\n</div><pre>baz</pre>", + "<div>test</div><b>foo</b>bar\n<pre>baz</pre>" ], +/*16-03*/[ "<div>test</div>\n<pre><b>foo</b>\nbar</pre>", "<div>test<b>foo</b>\n</div><pre>bar</pre>", + "<div>test</div><b>foo</b>\n<pre>bar</pre>" ], +/*17-04*/[ "<div>test</div>\n<pre><b>foo\n</b>bar\nbaz</pre>", "<div>test<b>foo\n</b></div><pre>bar\nbaz</pre>", + "<div>test</div><b>foo\n</b><pre>bar\nbaz</pre>" ], +/*18-05*/[ "<div>test</div>\n<pre>foobar<br>baz</pre>", "<div>testfoobar<br></div><pre>baz</pre>", + "<div>test</div>foobar<br><pre>baz</pre>" ], +/*19-06*/[ "<div>test</div>\n<pre><b>foobar<br>baz</b></pre>", "<div>test<b>foobar</b><br></div><pre><b>baz</b></pre>", + "<div>test</div><b>foobar</b><br><pre><b>baz</b></pre>" ], +/*20-07*/[ "<div>test</div>\n<pre><div>foobar</div>baz</pre>", "<div>testfoobar</div><pre>baz</pre>", + "<div>test</div>foobar<pre>baz</pre>" ], +/*21-08*/[ "<div>test</div>\n<pre>foobar<div>baz</div></pre>", "<div>testfoobar</div><pre><div>baz</div></pre>", + "<div>test</div>foobar<pre><div>baz</div></pre>" ], +/*22-09*/[ "<div>test</div>\n<pre><div>foobar</div>baz\nfred</pre>", "<div>testfoobar</div><pre>baz\nfred</pre>", + "<div>test</div>foobar<pre>baz\nfred</pre>" ], +/*23-10*/[ "<div>test</div>\n<pre>foobar<div>baz</div>\nfred</pre>", "<div>testfoobar</div><pre><div>baz</div>\nfred</pre>", + "<div>test</div>foobar<pre><div>baz</div>\nfred</pre>" ], +/*24-11*/[ "<div>test</div>\n<pre><div>foo\nbar</div>baz\nfred</pre>", "<div>testfoo\nbar</div><pre>baz\nfred</pre>", // BAD + "<div>test</div>foo\n<pre><div>bar</div>baz\nfred</pre>" ], +/*25-12*/[ "<div>test</div>\n<pre>foo<div>bar</div>baz\nfred</pre>", "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", + "<div>test</div>foo<pre><div>bar</div>baz\nfred</pre>" ], + + /* Some tests without <div>. These exercise the MoveBlock "right in left" */ +/*26-00*/[ "test<pre>foobar\nbaz</pre>", "testfoobar\n<pre>baz</pre>" ], +/*27-01*/[ "test<pre><b>foobar\nbaz</b></pre>", "test<b>foobar\n</b><pre><b>baz</b></pre>" ], +/*28-02*/[ "test<pre><b>foo</b>bar\nbaz</pre>", "test<b>foo</b>bar\n<pre>baz</pre>" ], +/*29-03*/[ "test<pre><b>foo</b>\nbar</pre>", "test<b>foo</b>\n<pre>bar</pre>" ], +/*30-04*/[ "test<pre><b>foo\n</b>bar\nbaz</pre>", "test<b>foo\n</b><pre>bar\nbaz</pre>" ], +/*31-05*/[ "test<pre>foobar<br>baz</pre>", "testfoobar<br><pre>baz</pre>" ], +/*32-06*/[ "test<pre><b>foobar<br>baz</b></pre>", "test<b>foobar</b><br><pre><b>baz</b></pre>" ], +/*33-07*/[ "test<pre><div>foobar</div>baz</pre>", "testfoobar<pre>baz</pre>" ], +/*34-08*/[ "test<pre>foobar<div>baz</div></pre>", "testfoobar<pre><div>baz</div></pre>" ], +/*35-09*/[ "test<pre><div>foobar</div>baz\nfred</pre>", "testfoobar<pre>baz\nfred</pre>" ], +/*36-10*/[ "test<pre>foobar<div>baz</div>\nfred</pre>", "testfoobar<pre><div>baz</div>\nfred</pre>" ], +/*37-11*/[ "test<pre><div>foo\nbar</div>baz\nfred</pre>", "testfoo\n<pre><div>bar</div>baz\nfred</pre>" ], +/*38-12*/[ "test<pre>foo<div>bar</div>baz\nfred</pre>", "testfoo<pre><div>bar</div>baz\nfred</pre>" ], + + /* + * Some tests with <span class="pre">. Again 07, 09 and 11 use "JoinNodesSmart". + * All these exercise MoveBlock "left in right". The "right" is the surrounding "contenteditable" div. + */ +/*39-00*/[ "<div>test</div><span class=\"pre\">foobar\nbaz</span>", "<div>test<span class=\"pre\">foobar\n</span></div><span class=\"pre\">baz</span>" ], +/*40-01*/[ "<div>test</div><span class=\"pre\"><b>foobar\nbaz</b></span>", "<div>test<span class=\"pre\"><b>foobar\n</b></span></div><span class=\"pre\"><b>baz</b></span>" ], +/*41-02*/[ "<div>test</div><span class=\"pre\"><b>foo</b>bar\nbaz</span>", "<div>test<span class=\"pre\"><b>foo</b>bar\n</span></div><span class=\"pre\">baz</span>" ], +/*42-03*/[ "<div>test</div><span class=\"pre\"><b>foo</b>\nbar</span>", "<div>test<span class=\"pre\"><b>foo</b>\n</span></div><span class=\"pre\">bar</span>" ], +/*43-04*/[ "<div>test</div><span class=\"pre\"><b>foo\n</b>bar\nbaz</span>", "<div>test<span class=\"pre\"><b>foo\n</b></span></div><span class=\"pre\">bar\nbaz</span>" ], +/*44-05*/[ "<div>test</div><span class=\"pre\">foobar<br>baz</span>", "<div>test<span class=\"pre\">foobar</span><br><span class=\"pre\"></span></div><span class=\"pre\">baz</span>" ], +/*45-06*/[ "<div>test</div><span class=\"pre\"><b>foobar<br>baz</b></span>", "<div>test<span class=\"pre\"><b>foobar</b></span><br><span class=\"pre\"></span></div><span class=\"pre\"><b>baz</b></span>" ], +/*46-07*/[ "<div>test</div><span class=\"pre\"><div>foobar</div>baz</span>", "<div>testfoobar</div><span class=\"pre\">baz</span>" ], +/*47-08*/[ "<div>test</div><span class=\"pre\">foobar<div>baz</div></span>", "<div>test<span class=\"pre\">foobar</span></div><span class=\"pre\"><div>baz</div></span>" ], +/*48-09*/[ "<div>test</div><span class=\"pre\"><div>foobar</div>baz\nfred</span>", "<div>testfoobar</div><span class=\"pre\">baz\nfred</span>" ], +/*49-10*/[ "<div>test</div><span class=\"pre\">foobar<div>baz</div>\nfred</span>", "<div>test<span class=\"pre\">foobar</span></div><span class=\"pre\"><div>baz</div>\nfred</span>" ], +/*50-11*/[ "<div>test</div><span class=\"pre\"><div>foo\nbar</div>baz\nfred</span>", "<div>testfoo\nbar</div><span class=\"pre\">baz\nfred</span>" ], // BAD +/*51-12*/[ "<div>test</div><span class=\"pre\">foo<div>bar</div>baz\nfred</span>", "<div>test<span class=\"pre\">foo</span></div><span class=\"pre\"><div>bar</div>baz\nfred</span>" ], + + /* Some tests with <div class="pre">. */ + /* + * The results are pretty ugly, since joining two <divs> sadly carrys the properties of the right to the left, + * but not in all cases: 07, 09, 11 are actually right. All cases use "JoinNodesSmart". + * Here we merely document the ugly behaviour. See bug 1191875 for more information. + */ +/*52-00*/[ "<div>test</div><div class=\"pre\">foobar\nbaz</div>", "<div class=\"pre\">testfoobar\nbaz</div>" ], +/*53-01*/[ "<div>test</div><div class=\"pre\"><b>foobar\nbaz</b></div>", "<div class=\"pre\">test<b>foobar\nbaz</b></div>" ], +/*54-02*/[ "<div>test</div><div class=\"pre\"><b>foo</b>bar\nbaz</div>", "<div class=\"pre\">test<b>foo</b>bar\nbaz</div>" ], +/*55-03*/[ "<div>test</div><div class=\"pre\"><b>foo</b>\nbar</div>", "<div class=\"pre\">test<b>foo</b>\nbar</div>" ], +/*56-04*/[ "<div>test</div><div class=\"pre\"><b>foo\n</b>bar\nbaz</div>", "<div class=\"pre\">test<b>foo\n</b>bar\nbaz</div>" ], +/*57-05*/[ "<div>test</div><div class=\"pre\">foobar<br>baz</div>", "<div class=\"pre\">testfoobar<br>baz</div>" ], +/*58-06*/[ "<div>test</div><div class=\"pre\"><b>foobar<br>baz</b></div>", "<div class=\"pre\">test<b>foobar<br>baz</b></div>" ], +/*59-07*/[ "<div>test</div><div class=\"pre\"><div>foobar</div>baz</div>", "<div>testfoobar</div><div class=\"pre\">baz</div>" ], +/*60-08*/[ "<div>test</div><div class=\"pre\">foobar<div>baz</div></div>", "<div class=\"pre\">testfoobar<div>baz</div></div>" ], +/*61-09*/[ "<div>test</div><div class=\"pre\"><div>foobar</div>baz\nfred</div>", "<div>testfoobar</div><div class=\"pre\">baz\nfred</div>" ], +/*62-10*/[ "<div>test</div><div class=\"pre\">foobar<div>baz</div>\nfred</div>", "<div class=\"pre\">testfoobar<div>baz</div>\nfred</div>" ], +/*63-11*/[ "<div>test</div><div class=\"pre\"><div>foo\nbar</div>baz\nfred</div>", "<div>testfoo\nbar</div><div class=\"pre\">baz\nfred</div>" ], // BAD +/*64-12*/[ "<div>test</div><div class=\"pre\">foo<div>bar</div>baz\nfred</div>", "<div class=\"pre\">testfoo<div>bar</div>baz\nfred</div>" ], + + /* Some tests with lists. These exercise the MoveBlock "left in right". */ +/*65*/[ "<ul><pre><li>test</li>foobar\nbaz</pre></ul>", "<ul><pre><li>testfoobar\n</li>baz</pre></ul>" ], +/*66*/[ "<ul><pre><li>test</li><b>foobar\nbaz</b></pre></ul>", "<ul><pre><li>test<b>foobar\n</b></li><b>baz</b></pre></ul>" ], +/*67*/[ "<ul><pre><li>test</li><b>foo</b>bar\nbaz</pre></ul>", "<ul><pre><li>test<b>foo</b>bar\n</li>baz</pre></ul>" ], +/*68*/[ "<ul><pre><li>test</li><b>foo</b>\nbar</pre></ul>", "<ul><pre><li>test<b>foo</b>\n</li>bar</pre></ul>" ], +/*69*/[ "<ul><pre><li>test</li><b>foo\n</b>bar\nbaz</pre></ul>", "<ul><pre><li>test<b>foo\n</b></li>bar\nbaz</pre></ul>" ], + + /* Last not least, some simple edge case tests. */ +/*70*/[ "<div>test</div><pre>foobar\n</pre>baz", "<div>testfoobar\n</div>baz" ], +/*71*/[ "<div>test</div><pre>\nfoo\nbar</pre>", "<div>testfoo\n</div><pre>bar</pre>" ], +/*72*/[ "<div>test</div><pre>\n\nfoo\nbar</pre>", "<div>test</div><pre>foo\nbar</pre>", "<div>test\n</div><pre>foo\nbar</pre>" ], + ]; + + /** Test for Bug 772796 **/ + + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(function() { + + var sel = window.getSelection(); + var theEdit = document.getElementById("editable"); + var testName; + var theDiv; + + for (i = 0; i < tests.length; i++) { + testName = "test" + i.toString(); + dump (testName+"\n"); + dump (tests[i][0]+"\n"); + + /* Set up the selection. */ + theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>"; + theDiv = document.getElementById(testName); + theDiv.focus(); + sel.collapse(theDiv, 0); + synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */ + + /** First round: Forward delete. **/ + synthesizeKey("VK_DELETE", {}); + is(theDiv.innerHTML, tests[i][1], "delete(collapsed): inner HTML for " + testName); + + /* Set up the selection. */ + theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>"; + theDiv = document.getElementById(testName); + theDiv.focus(); + sel.collapse(theDiv, 0); + synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */ + + /** Second round: Backspace. **/ + synthesizeKey("VK_RIGHT", {}); + synthesizeKey("VK_BACK_SPACE", {}); + if (tests[i].length == 2) { + is(theDiv.innerHTML, tests[i][1], "backspace: inner HTML for " + testName); + } else { + todo_is(theDiv.innerHTML, tests[i][1], "backspace(should be): inner HTML for " + testName); + is(theDiv.innerHTML, tests[i][2], "backspace(currently is): inner HTML for " + testName); + } + + /* Set up the selection. */ + theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>"; + theDiv = document.getElementById(testName); + theDiv.focus(); + sel.collapse(theDiv, 0); + synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */ + + /** Third round: Delete with non-collapsed selection. **/ + if (i == 72) { + // This test doesn't work, since we can't select only one newline using the right arrow key. + continue; + } + synthesizeKey("VK_LEFT", {}); + /* Strangely enough we need to hit "right arrow" three times to select two characters. */ + synthesizeKey("VK_RIGHT", {shiftKey:true}); + synthesizeKey("VK_RIGHT", {shiftKey:true}); + synthesizeKey("VK_RIGHT", {shiftKey:true}); + synthesizeKey("VK_DELETE", {}); + + /* We always expect to the delete the "tf" in "testfoo". */ + var expected = tests[i][1].replace("testfoo", "tesoo") + .replace("test<b>foo", "tes<b>oo") + .replace("test<span class=\"pre\">foo", "tes<span class=\"pre\">oo") + .replace("test<span class=\"pre\"><b>foo", "tes<span class=\"pre\"><b>oo"); + is(theDiv.innerHTML, expected, "delete(non-collapsed): inner HTML for " + testName); + } + + SimpleTest.finish(); + + }); + + </script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug773262.html b/editor/libeditor/tests/test_bug773262.html new file mode 100644 index 000000000..b0dc82755 --- /dev/null +++ b/editor/libeditor/tests/test_bug773262.html @@ -0,0 +1,63 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=773262 +--> +<title>Test for Bug 773262</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=773262">Mozilla Bug 773262</a></p> +<iframe></iframe> +<script> +function runTest(doc, desc) { + is(doc.queryCommandEnabled("undo"), false, + desc + ": Undo shouldn't be enabled yet"); + is(doc.queryCommandEnabled("redo"), false, + desc + ": Redo shouldn't be enabled yet"); + is(doc.body.innerHTML, "<p>Hello</p>", desc + ": Wrong initial innerHTML"); + + doc.getSelection().selectAllChildren(doc.body.firstChild); + doc.execCommand("bold"); + is(doc.queryCommandEnabled("undo"), true, + desc + ": Undo should be enabled after bold"); + is(doc.queryCommandEnabled("redo"), false, + desc + ": Redo still shouldn't be enabled"); + is(doc.body.innerHTML, "<p><b>Hello</b></p>", + desc + ": Wrong innerHTML after bold"); + + doc.execCommand("undo"); + is(doc.queryCommandEnabled("undo"), false, + desc + ": Undo should be disabled again"); + is(doc.queryCommandEnabled("redo"), true, + desc + ": Redo should be enabled now"); + is(doc.body.innerHTML, "<p>Hello</p>", + desc + ": Wrong innerHTML after undo"); + + doc.execCommand("redo"); + is(doc.queryCommandEnabled("undo"), true, + desc + ": Undo should be enabled after redo"); + is(doc.queryCommandEnabled("redo"), false, + desc + ": Redo should be disabled again"); + is(doc.body.innerHTML, "<p><b>Hello</b></p>", + desc + ": Wrong innerHTML after redo"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var doc = document.querySelector("iframe").contentDocument; + + // First turn on designMode and run the test like that, as a sanity check. + doc.body.innerHTML = "<p>Hello</p>"; + doc.designMode = "on"; + runTest(doc, "1"); + + // Now to test the actual bug: repeat all the above, but with designMode + // toggled. This should clear the undo history, so everything should be + // exactly as before. + doc.designMode = "off"; + doc.body.innerHTML = "<p>Hello</p>"; + doc.designMode = "on"; + runTest(doc, "2"); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug780035.html b/editor/libeditor/tests/test_bug780035.html new file mode 100644 index 000000000..7c99b9ff5 --- /dev/null +++ b/editor/libeditor/tests/test_bug780035.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=780035 +--> +<title>Test for Bug 780035</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=780035">Mozilla Bug 780035</a> +<div contenteditable style="font-size: 13.3333px"></div> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + document.querySelector("div").focus(); + document.execCommand("stylewithcss", false, true); + sendKey("RETURN"); + sendChar("x"); + is(document.querySelector("div").innerHTML, "x<br>", + "No <font> tag should be generated"); + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug780908.xul b/editor/libeditor/tests/test_bug780908.xul new file mode 100644 index 000000000..312f02787 --- /dev/null +++ b/editor/libeditor/tests/test_bug780908.xul @@ -0,0 +1,113 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=780908 + +adapted from test_bug607584.xul by Kent James <kent@caspia.com> +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 780908" onload="runTest();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=780908" + target="_blank">Mozilla Bug 780908</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content-primary" + editortype="html" + style="width: 400px; height: 100px; border: thin solid black"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function EditorContentListener(aEditor) + { + this.init(aEditor); + } + + EditorContentListener.prototype = { + init : function(aEditor) + { + this.mEditor = aEditor; + }, + + QueryInterface : function(aIID) + { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) + { + var editor = this.mEditor.getEditor(this.mEditor.contentWindow); + if (editor) { + this.mEditor.focus(); + editor instanceof Components.interfaces.nsIHTMLEditor; + editor.returnInParagraphCreatesNewParagraph = true; + source = "<html><body><table><head></table></body></html>"; + editor.rebuildDocumentFromSource(source); + ok(true, "Don't crash when head appears after body"); + source = "<html></head><head><body></body></html>"; + editor.rebuildDocumentFromSource(source); + ok(true, "Don't crash when /head appears before head"); + SimpleTest.finish(); + progress.removeProgressListener(this); + } + } + + }, + + + onProgressChange : function(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange : function(aWebProgress, aRequest, aState) + { + }, + + mEditor: null + }; + + var progress, progressListener; + + function runTest() { + var newEditorElement = document.getElementById("editor"); + newEditorElement.makeEditable("html", true); + var docShell = newEditorElement.boxObject.docShell; + progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress); + progressListener = new EditorContentListener(newEditorElement); + progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL); + newEditorElement.setAttribute("src", "data:text/html,"); + } +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug787432.html b/editor/libeditor/tests/test_bug787432.html new file mode 100644 index 000000000..c73bb3c7e --- /dev/null +++ b/editor/libeditor/tests/test_bug787432.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=787432 +--> +<title>Test for Bug 787432</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=787432">Mozilla Bug 787432</a> +<div id="test" contenteditable><span class="insert">%</span><br></div> +<script> +var div = document.getElementById("test"); +getSelection().collapse(div.firstChild, 0); +getSelection().extend(div.firstChild, 1); +document.execCommand("inserttext", false, "x"); +is(div.innerHTML, '<span class="insert">x</span><br>', + "Empty <span> needs to not be removed"); +</script> diff --git a/editor/libeditor/tests/test_bug790475.html b/editor/libeditor/tests/test_bug790475.html new file mode 100644 index 000000000..d7685458b --- /dev/null +++ b/editor/libeditor/tests/test_bug790475.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=790475 +--> +<head> + <title>Test for Bug 790475</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=790475">Mozilla Bug 790475</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 790475 + * + * Tests that inline spell checking works properly through adjacent text nodes. + */ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +var gMisspeltWords; + +function getEditor() { + const Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + return editingSession.getEditorForWindow(window); +} + +function getSpellCheckSelection() { + var editor = getEditor(); + var selcon = editor.selectionController; + return selcon.getSelection(selcon.SELECTION_SPELLCHECK); +} + +function runTest() { + gMisspeltWords = []; + var edit = document.getElementById("edit"); + edit.focus(); + + SimpleTest.executeSoon(function() { + gMisspeltWords = []; + is(isSpellingCheckOk(), true, "Should not find any misspellings yet."); + + var newTextNode = document.createTextNode("ing string"); + edit.appendChild(newTextNode); + var editor = getEditor(); + var sel = editor.selection; + sel.collapse(newTextNode, newTextNode.textContent.length); + synthesizeKey("!", {}); + + edit.blur(); + + SimpleTest.executeSoon(function() { + is(isSpellingCheckOk(), true, "Should not have found any misspellings. "); + SimpleTest.finish(); + }); + }); +} + +function isSpellingCheckOk() { + var sel = getSpellCheckSelection(); + var numWords = sel.rangeCount; + + is(numWords, gMisspeltWords.length, "Correct number of misspellings and words."); + + if (numWords != gMisspeltWords.length) + return false; + + for (var i = 0; i < numWords; i++) { + var word = sel.getRangeAt(i); + is (word, gMisspeltWords[i], "Misspelling is what we think it is."); + if (word != gMisspeltWords[i]) + return false; + } + return true; +} + +</script> +</pre> + +<div id="edit" contenteditable="true">This is a test</div> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-2.html b/editor/libeditor/tests/test_bug795418-2.html new file mode 100644 index 000000000..3f44900ee --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-2.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #2 for Bug 772796</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<iframe src="data:application/xhtml+xml,<html contenteditable='' xmlns='http://www.w3.org/1999/xhtml'><span>AB</span></html>"></iframe> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + function checkResult() { + var iframe = document.querySelector("iframe"); + var iframeWindow = iframe.contentWindow; + var theEdit = iframe.contentDocument.firstChild; + theEdit.offsetHeight; + is(theEdit.innerHTML, + "<blockquote xmlns=\"http://www.w3.org/1999/xhtml\" type=\"cite\">Copy this</blockquote><span xmlns=\"http://www.w3.org/1999/xhtml\">AB</span>", + "unexpected HTML for test"); + SimpleTest.finish(); + } + + function pasteQuote() { + var iframe = document.querySelector("iframe"); + var iframeWindow = iframe.contentWindow; + var theEdit = iframe.contentDocument.firstChild; + theEdit.offsetHeight; + iframeWindow.focus(); + SimpleTest.waitForFocus(function() { + var iframeSel = iframeWindow.getSelection(); + iframeSel.removeAllRanges(); + let span = iframe.contentDocument.querySelector('span'); + iframeSel.collapse(span, 1); + + SpecialPowers.doCommand(iframeWindow, "cmd_pasteQuote"); + setTimeout(checkResult, 0); + }, iframeWindow); + } + + SimpleTest.waitForClipboard( + function compare(value) { + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + setTimeout(pasteQuote, 0); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-3.html b/editor/libeditor/tests/test_bug795418-3.html new file mode 100644 index 000000000..bbe1a58b3 --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-3.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #3 for Bug 772796</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<iframe src="data:text/html,<html><body><span>AB</span>"></iframe> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + function checkResult() { + var iframe = document.querySelector("iframe"); + var iframeWindow = iframe.contentWindow; + var theEdit = iframe.contentDocument.body; + theEdit.offsetHeight; + is(theEdit.innerHTML, + "<span>AB<blockquote type=\"cite\">Copy this</blockquote></span>", + "unexpected HTML for test"); + SimpleTest.finish(); + } + + function pasteQuote() { + var iframe = document.querySelector("iframe"); + var iframeWindow = iframe.contentWindow; + var theEdit = iframe.contentDocument.body; + iframe.contentDocument.designMode='on'; + iframe.contentDocument.body.offsetHeight; + iframeWindow.focus(); + SimpleTest.waitForFocus(function() { + var iframeSel = iframeWindow.getSelection(); + iframeSel.removeAllRanges(); + iframeSel.collapse(theEdit.firstChild, 1); + + SpecialPowers.doCommand(iframeWindow, "cmd_pasteQuote"); + setTimeout(checkResult, 0); + }, iframeWindow); + } + + SimpleTest.waitForClipboard( + function compare(value) { + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + setTimeout(pasteQuote, 0); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-4.html b/editor/libeditor/tests/test_bug795418-4.html new file mode 100644 index 000000000..6c1ae05d1 --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-4.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #4 for Bug 795418</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<div id="editable" contenteditable style="display:grid">AB</div> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + SimpleTest.waitForClipboard( + function compare(value) { + var theEdit = document.getElementById("editable"); + sel.collapse(theEdit.firstChild, 2); + + SpecialPowers.doCommand(window, "cmd_paste"); + is(theEdit.innerHTML, + "ABCopy this", + "unexpected HTML for test"); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + SimpleTest.finish(); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-5.html b/editor/libeditor/tests/test_bug795418-5.html new file mode 100644 index 000000000..5ff90b15a --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-5.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #5 for Bug 795418</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<div id="editable" contenteditable style="display:ruby">AB</div> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + SimpleTest.waitForClipboard( + function compare(value) { + var theEdit = document.getElementById("editable"); + sel.collapse(theEdit.firstChild, 2); + + SpecialPowers.doCommand(window, "cmd_paste"); + is(theEdit.innerHTML, + "ABCopy this", + "unexpected HTML for test"); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + SimpleTest.finish(); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-6.html b/editor/libeditor/tests/test_bug795418-6.html new file mode 100644 index 000000000..798a6534b --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-6.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #5 for Bug 795418</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<div id="editable" contenteditable style="display:table">AB</div> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + SimpleTest.waitForClipboard( + function compare(value) { + var theEdit = document.getElementById("editable"); + sel.collapse(theEdit.firstChild, 2); + + SpecialPowers.doCommand(window, "cmd_pasteQuote"); + is(theEdit.innerHTML, + "AB<blockquote type=\"cite\">Copy this</blockquote>", + "unexpected HTML for test"); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + SimpleTest.finish(); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418.html b/editor/libeditor/tests/test_bug795418.html new file mode 100644 index 000000000..1db8cf026 --- /dev/null +++ b/editor/libeditor/tests/test_bug795418.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 795418</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<div id="editable" contenteditable><span>AB</span></div> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + SimpleTest.waitForClipboard( + function compare(value) { + var theEdit = document.getElementById("editable"); + sel.collapse(theEdit.firstChild, 1); + + SpecialPowers.doCommand(window, "cmd_pasteQuote"); + is(theEdit.innerHTML, + "<span>AB<blockquote type=\"cite\">Copy this</blockquote></span>", + "unexpected HTML for test"); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + SimpleTest.finish(); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795785.html b/editor/libeditor/tests/test_bug795785.html new file mode 100644 index 000000000..5f93d5142 --- /dev/null +++ b/editor/libeditor/tests/test_bug795785.html @@ -0,0 +1,168 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795785 +--> +<head> + <title>Test for Bug 795785</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795785">Mozilla Bug 795785</a> +<div id="display"> + <textarea id="textarea" style="overflow: hidden; height: 3em; width: 5em; word-wrap: normal;"></textarea> + <div id="div" contenteditable style="overflow: hidden; height: 3em; width: 5em;"></div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("This test uses setTimeouts in order to fix an intermittent failure."); + +// Turn off spatial navigation because it hijacks arrow key events and VK_RETURN +// events. +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, runTests); +}); +var textarea = document.getElementById("textarea"); +var div = document.getElementById("div"); + +function hitEventLoop(aFunc, aTimes) +{ + if (--aTimes) { + setTimeout(hitEventLoop, 0, aFunc, aTimes); + } else { + setTimeout(aFunc, 100); + } +} + +function doKeyEventTest(aElement, aElementDescription, aCallback) +{ + aElement.focus(); + aElement.scrollTop = 0; + hitEventLoop(function () { + is(aElement.scrollTop, 0, + aElementDescription + "'s scrollTop isn't 0"); + synthesizeKey("VK_RETURN", { }); + synthesizeKey("VK_RETURN", { }); + synthesizeKey("VK_RETURN", { }); + synthesizeKey("VK_RETURN", { }); + synthesizeKey("VK_RETURN", { }); + synthesizeKey("VK_RETURN", { }); + hitEventLoop(function () { + isnot(aElement.scrollTop, 0, + aElementDescription + " was not scrolled by inserting line breaks"); + var scrollTop = aElement.scrollTop; + synthesizeKey("VK_UP", { }); + synthesizeKey("VK_UP", { }); + synthesizeKey("VK_UP", { }); + synthesizeKey("VK_UP", { }); + synthesizeKey("VK_UP", { }); + hitEventLoop(function () { + isnot(aElement.scrollTop, scrollTop, + aElementDescription + " was not scrolled by up key events"); + synthesizeKey("VK_DOWN", { }); + synthesizeKey("VK_DOWN", { }); + synthesizeKey("VK_DOWN", { }); + synthesizeKey("VK_DOWN", { }); + synthesizeKey("VK_DOWN", { }); + hitEventLoop(function () { + is(aElement.scrollTop, scrollTop, + aElementDescription + " was not scrolled by down key events"); + var longWord = "aaaaaaaaaaaaaaaaaaaa"; + sendString(longWord); + hitEventLoop(function () { + isnot(aElement.scrollLeft, 0, + aElementDescription + " was not scrolled by typing long word"); + var scrollLeft = aElement.scrollLeft; + var i; + for (i = 0; i < longWord.length; i++) { + synthesizeKey("VK_LEFT", { }); + } + hitEventLoop(function () { + isnot(aElement.scrollLeft, scrollLeft, + aElementDescription + " was not scrolled by left key events"); + for (i = 0; i < longWord.length; i++) { + synthesizeKey("VK_RIGHT", { }); + } + hitEventLoop(function () { + is(aElement.scrollLeft, scrollLeft, + aElementDescription + " was not scrolled by right key events"); + aCallback(); + }, 20); + }, 20); + }, 20); + }, 20); + }, 20); + }, 20); + }, 20); +} + +function doCompositionTest(aElement, aElementDescription, aCallback) +{ + aElement.focus(); + aElement.scrollTop = 0; + hitEventLoop(function () { + is(aElement.scrollTop, 0, + aElementDescription + "'s scrollTop isn't 0"); + var str = "Web \u958b\u767a\u8005\u306e\u7686\u3055\u3093\u306f\u3001" + + "Firefox \u306b\u5b9f\u88c5\u3055\u308c\u3066\u3044\u308b HTML5" + + " \u3084 CSS \u306e\u65b0\u6a5f\u80fd\u3092\u6d3b\u7528\u3059" + + "\u308b\u3053\u3068\u3067\u3001\u9b45\u529b\u3042\u308b Web " + + "\u30b5\u30a4\u30c8\u3084\u9769\u65b0\u7684\u306a Web \u30a2" + + "\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u3088\u308a" + + "\u77ed\u6642\u9593\u3067\u7c21\u5358\u306b\u4f5c\u6210\u3067" + + "\u304d\u307e\u3059\u3002"; + synthesizeCompositionChange({ + composition: { + string: str, + clauses: [ + { length: str.length, attr: COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + caret: { start: str.length, length: 0 } + }); + hitEventLoop(function () { + isnot(aElement.scrollTop, 0, + aElementDescription + " was not scrolled by composition"); + synthesizeComposition({ type: "compositioncommit", data: "" }); + hitEventLoop(function () { + is(aElement.scrollTop, 0, + aElementDescription + " was not scrolled back to the top by canceling composition"); + aCallback(); + }, 20); + }, 20); + }, 20); +} + +function runTests() +{ + doKeyEventTest(textarea, "textarea", + function () { + textarea.value = ""; + doKeyEventTest(div, "div (contenteditable)", + function () { + div.innerHTML = ""; + doCompositionTest(textarea, "textarea", + function () { + doCompositionTest(div, "div (contenteditable)", + function () { + SimpleTest.finish(); + }); + }); + }); + }); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug796839.html b/editor/libeditor/tests/test_bug796839.html new file mode 100644 index 000000000..be4be316c --- /dev/null +++ b/editor/libeditor/tests/test_bug796839.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=796839 +--> +<title>Test for Bug 796839</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=796839">Mozilla Bug 796839</a> +<div id="test" contenteditable><br></div> +<script> +var div = document.getElementById("test"); +var text = document.createTextNode(""); +div.insertBefore(text, div.firstChild); +getSelection().collapse(text, 0); +document.execCommand("inserthtml", false, "x"); +is(div.textContent, 'x', "Empty textnodes should be editable"); +</script> diff --git a/editor/libeditor/tests/test_bug830600.html b/editor/libeditor/tests/test_bug830600.html new file mode 100644 index 000000000..39ced297a --- /dev/null +++ b/editor/libeditor/tests/test_bug830600.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=830600 +--> +<head> + <title>Test for Bug 830600</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=830600">Mozilla Bug 830600</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <input type="text" id="t1" /> + <pre id="test"> + <script type="application/javascript;version=1.7"> + + /** Test for Bug 830600 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + const Ci = SpecialPowers.Ci; + function test(str, expected, callback) { + var t = document.getElementById("t1"); + SpecialPowers.wrap(t).QueryInterface(Ci.nsIDOMNSEditableElement); + t.focus(); + t.value = ""; + var editor = SpecialPowers.wrap(t).editor; + editor.QueryInterface(Ci.nsIPlaintextEditor); + var origNewlineHandling = editor.newlineHandling; + editor.newlineHandling = Ci.nsIPlaintextEditor.eNewlinesStripSurroundingWhitespace + SimpleTest.waitForClipboard(str, + function() { + SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(str); + }, + function() { + synthesizeKey("V", {accelKey: true}); + is(t.value, expected, "New line handling works correctly"); + t.value = ""; + callback(); + }, + function() { + ok(false, "Failed to copy the string"); + SimpleTest.finish(); + } + ); + } + + function runNextTest() { + if (tests.length) { + var currentTest = tests.shift(); + test(currentTest[0], currentTest[1], runNextTest); + } else { + SimpleTest.finish(); + } + } + + var tests = [ + ["abc", "abc"], + ["\n", ""], + [" \n", ""], + ["\n ", ""], + [" \n ", ""], + [" a", " a"], + ["a ", "a "], + [" a ", " a "], + [" \nabc", "abc"], + ["\n abc", "abc"], + [" \n abc", "abc"], + [" \nabc ", "abc "], + ["\n abc ", "abc "], + [" \n abc ", "abc "], + ["abc\n ", "abc"], + ["abc \n", "abc"], + ["abc \n ", "abc"], + [" abc\n ", " abc"], + [" abc \n", " abc"], + [" abc \n ", " abc"], + [" abc \n def \n ", " abcdef"], + ["\n abc \n def \n ", "abcdef"], + [" \n abc \n def ", "abcdef "], + [" abc\n\ndef ", " abcdef "], + [" abc \n\n def ", " abcdef "], + ]; + + runNextTest(); + }); + + </script> + </pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug832025.html b/editor/libeditor/tests/test_bug832025.html new file mode 100644 index 000000000..40f4f4734 --- /dev/null +++ b/editor/libeditor/tests/test_bug832025.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=832025 +--> +<head> + <title>Test for Bug 832025</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=832025">Mozilla Bug 832025</a> +<div id="test" contenteditable="true">header1</div> +<script type="application/javascript"> + +/** + * Test for Bug 832025 + * + */ + +document.execCommand("stylewithcss", false, "true"); +var test = document.getElementById("test"); +test.focus(); + +// place caret at end of editable area +var sel = getSelection(); +sel.collapse(test, test.childNodes.length); + +// make it a H1 +document.execCommand("heading", false, "H1"); +// simulate a CR key +sendKey("return"); +// insert some text +document.execCommand("insertText", false, "abc"); + +is(test.innerHTML == '<h1>header1</h1><p>abc<br></p>', + true, "A paragraph automatically created after a CR at the end of an H1 should not be bold"); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug850043.html b/editor/libeditor/tests/test_bug850043.html new file mode 100644 index 000000000..b811c86a6 --- /dev/null +++ b/editor/libeditor/tests/test_bug850043.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=850043 +--> +<head> + <title>Test for Bug 850043</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=850043">Mozilla Bug 850043</a> +<div id="display"> +<textarea id="textarea">b邀󠄏辺󠄁</textarea> +<div contenteditable id="edit">b邀󠄏辺󠄁</div> +</div> +<div id="content" style="display: none"> +</div> + +<pre id="test"> +</pre> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]. + getService(SpecialPowers.Ci.nsIFocusManager); + + let element = document.getElementById("textarea"); + element.setSelectionRange(element.value.length, element.value.length); + element.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), element, "failed to move focus"); + + synthesizeKey("VK_END", { }); + synthesizeKey("a", { }); + is(element.value, "b\u{9080}\u{e010f}\u{8fba}\u{e0101}a", "a isn't last character"); + + synthesizeKey("VK_BACK_SPACE", { }); + synthesizeKey("VK_BACK_SPACE", { }); + synthesizeKey("VK_BACK_SPACE", { }); + is(element.value, 'b', "cannot remove all IVS characters"); + + element = document.getElementById("edit"); + element.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), element, "failed to move focus"); + + let sel = window.getSelection(); + sel.collapse(element.childNodes[0], element.textContent.length); + + synthesizeKey("a", { }); + is(element.textContent, "b\u{9080}\u{e010f}\u{8fba}\u{e0101}a", "a isn't last character"); + + synthesizeKey("VK_BACK_SPACE", { }); + synthesizeKey("VK_BACK_SPACE", { }); + synthesizeKey("VK_BACK_SPACE", { }); + is(element.textContent, 'b', "cannot remove all IVS characters"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug857487.html b/editor/libeditor/tests/test_bug857487.html new file mode 100644 index 000000000..a3746d44c --- /dev/null +++ b/editor/libeditor/tests/test_bug857487.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=857487 +--> +<head> + <title>Test for Bug 857487</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=857487">Mozilla Bug 857487</a> +<div id="edit" contenteditable="true"> + <table id="table" border="1" width="100%"> + <tbody> + <tr> + <td>a</td> + <td>b</td> + <td>c</td> + </tr> + <tr> + <td>d</td> + <td id="cell">e</td> + <td>f</td> + </tr> + <tr> + <td>g</td> + <td>h</td> + <td>i</td> + </tr> + </tbody> + </table> +</div> +<script type="application/javascript"> + +/** + * Test for Bug 857487 + * + * Tests that removing a table row through nsIHTMLEditor works + */ + +function getEditor() { + const Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +var cell = document.getElementById("cell"); +cell.focus(); + +// place caret at end of center cell +var sel = getSelection(); +sel.collapse(cell, cell.childNodes.length); + +var editor = getEditor(); +editor.deleteTableRow(1); + +var table = document.getElementById("table"); + +is(table.innerHTML == "\n <tbody>\n <tr>\n <td>a</td>\n <td>b</td>\n <td>c</td>\n </tr>\n \n <tr>\n <td>g</td>\n <td>h</td>\n <td>i</td>\n </tr>\n </tbody>\n ", + true, "editor.deleteTableRow(1) should delete the row containing the selection"); + +</script> + + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug858918.html b/editor/libeditor/tests/test_bug858918.html new file mode 100644 index 000000000..46f841bbc --- /dev/null +++ b/editor/libeditor/tests/test_bug858918.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=858918 +--> +<title>Test for Bug 858918</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858918">Mozilla Bug 858918</a> +<span contenteditable style="display:block;min-height:1em"></span> +<script> +var span = document.querySelector("span"); +getSelection().collapse(span, 0); +document.execCommand("inserthtml", false, "<div>doesn't go in span</div>"); +is(span.innerHTML, "<div>doesn't go in span</div>"); +</script> diff --git a/editor/libeditor/tests/test_bug915962.html b/editor/libeditor/tests/test_bug915962.html new file mode 100644 index 000000000..32968b310 --- /dev/null +++ b/editor/libeditor/tests/test_bug915962.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=915962 +--> +<head> + <title>Test for Bug 915962</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=915962">Mozilla Bug 915962</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 915962 **/ + +var smoothScrollPref = "general.smoothScroll"; +SimpleTest.waitForExplicitFinish(); +var win = window.open("file_bug915962.html", "_blank", + "width=600,height=600,scrollbars=yes"); + +// grab the timer right at the start +var cwu = SpecialPowers.getDOMWindowUtils(win); +function step() { + cwu.advanceTimeAndRefresh(100); +} +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[[smoothScrollPref, false]]}, startTest); +}, win); +function startTest() { + // Make sure that pressing Space when a tabindex=-1 element is focused + // will scroll the page. + var button = win.document.querySelector("button"); + var sc = win.document.querySelector("div"); + sc.focus(); + is(win.scrollY, 0, "Sanity check"); + synthesizeKey(" ", {}, win); + + step(); + + isnot(win.scrollY, 0, "Page is scrolled down"); + var oldY = win.scrollY; + synthesizeKey(" ", {shiftKey: true}, win); + + step(); + + ok(win.scrollY < oldY, "Page is scrolled up"); + + // Make sure that pressing Space when a tabindex=-1 element is focused + // will not scroll the page, and will activate the element. + button.focus(); + var clicked = false; + button.onclick = () => clicked = true; + oldY = win.scrollY; + synthesizeKey(" ", {}, win); + + step(); + + ok(win.scrollY <= oldY, "Page is not scrolled down"); + ok(clicked, "The button should be clicked"); + synthesizeKey("VK_TAB", {}, win); + + step(); + + oldY = win.scrollY; + synthesizeKey(" ", {}, win); + + step() + + ok(win.scrollY >= oldY, "Page is scrolled down"); + + win.close(); + cwu.restoreNormalRefresh(); + + win = window.open("file_bug915962.html", "_blank", + "width=600,height=600,scrollbars=yes"); + cwu = SpecialPowers.getDOMWindowUtils(win); + SimpleTest.waitForFocus(function() { + is(win.scrollY, 0, "Sanity check"); + synthesizeKey(" ", {}, win); + + step(); + + isnot(win.scrollY, 0, "Page is scrolled down without crashing"); + + win.close(); + cwu.restoreNormalRefresh(); + + SimpleTest.finish(); + }, win); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug966155.html b/editor/libeditor/tests/test_bug966155.html new file mode 100644 index 000000000..524b15d69 --- /dev/null +++ b/editor/libeditor/tests/test_bug966155.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=966155 +--> +<head> + <title>Test for Bug 966155</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=966155">Mozilla Bug 966155</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var win = window.open("data:text/html,<input><iframe onload=\"contentDocument.designMode = 'on';\">", "", "test-966155"); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + runTest(win); + }, false); +}); + +function runTest(win) { + SimpleTest.waitForFocus(function() { + var doc = win.document; + var iframe = doc.querySelector("iframe"); + var iframeDoc = iframe.contentDocument; + var input = doc.querySelector("input"); + iframe.focus(); + iframeDoc.body.focus(); + // Type some text + "test".split("").forEach(function(letter) { + synthesizeKey(letter, {}, win); + }); + is(iframeDoc.body.textContent, "test", "entered the text"); + // focus the input box + input.focus(); + // press tab + synthesizeKey("VK_TAB", {}, win); + // Now press Ctrl+Backspace + synthesizeKey("VK_BACK_SPACE", {ctrlKey: true}, win); + is(iframeDoc.body.textContent, "", "deleted the text"); + win.close(); + SimpleTest.finish(); + }, win); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug966552.html b/editor/libeditor/tests/test_bug966552.html new file mode 100644 index 000000000..3d0ec5fe3 --- /dev/null +++ b/editor/libeditor/tests/test_bug966552.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=966552 +--> +<head> + <title>Test for Bug 966552</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=966552">Mozilla Bug 966552</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var win = window.open("data:text/html,<body onload=\"document.designMode='on'\">test</body>", "", "test-966552"); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + runTest(win); + }, false); +}); + +function runTest(win) { + SimpleTest.waitForFocus(function() { + var doc = win.document; + var sel = win.getSelection(); + doc.body.focus(); + sel.collapse(doc.body.firstChild, 2); + synthesizeKey("VK_BACK_SPACE", {ctrlKey: true}, win); + is(doc.body.textContent, "st"); + win.close(); + SimpleTest.finish(); + }, win); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug974309.html b/editor/libeditor/tests/test_bug974309.html new file mode 100644 index 000000000..e3caa87fb --- /dev/null +++ b/editor/libeditor/tests/test_bug974309.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=974309 +--> +<head> + <title>Test for Bug 974309</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=974309">Mozilla Bug 974309</a> +<div id="edit_not_table_parent" contenteditable="true"></div> +<div> + <table id="table" border="1" width="100%"> + <tbody> + <tr> + <td>a</td> + <td>b</td> + <td>c</td> + </tr> + <tr> + <td>d</td> + <td id="cell">e</td> + <td>f</td> + </tr> + <tr> + <td>g</td> + <td>h</td> + <td>i</td> + </tr> + </tbody> + </table> +</div> +<script type="application/javascript"> + +/** + * Test for Bug 974309 + * + * Tests that editing a table row fails when the table or row is _not_ a child of a contenteditable node. + * See bug 857487 for tests that cover when the table or row _is_ a child of a contenteditable node. + */ + +function getEditor() { + const Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +var cell = document.getElementById("cell"); +cell.focus(); + +// place caret at end of center cell +var sel = getSelection(); +sel.collapse(cell, cell.childNodes.length); + +var table = document.getElementById("table"); + +var tableHTML = table.innerHTML; + +var editor = getEditor(); +editor.deleteTableRow(1); + +is(table.innerHTML == tableHTML, true, "editor should not modify non-editable table" ); + +isnot(table.innerHTML == "\n <tbody>\n <tr>\n <td>a</td>\n <td>b</td>\n <td>c</td>\n </tr>\n \n <tr>\n <td>g</td>\n <td>h</td>\n <td>i</td>\n </tr>\n </tbody>\n ", + true, "editor.deleteTableRow(1) should not delete a non-editable row containing the selection"); + +</script> + + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug998188.html b/editor/libeditor/tests/test_bug998188.html new file mode 100644 index 000000000..2d167f0bd --- /dev/null +++ b/editor/libeditor/tests/test_bug998188.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=565392 +--> +<head> + <title>Test for Bug 998188</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998188">Mozilla Bug 998188</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<div id="editor" contenteditable>abc</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 998188 **/ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + var editor = document.getElementById("editor"); + editor.focus(); + + var textNode1 = document.createTextNode("def"); + var textNode2 = document.createTextNode("ghi"); + + editor.appendChild(textNode1); + editor.appendChild(textNode2); + + window.getSelection().collapse(textNode2, 3); + + for (var i = 0; i < 9; i++) { + var caretRect = synthesizeQueryCaretRect(i); + ok(caretRect.succeeded, "QueryCaretRect should succeeded (" + i + ")"); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_composition_event_created_in_chrome.html b/editor/libeditor/tests/test_composition_event_created_in_chrome.html new file mode 100644 index 000000000..18b72ccd4 --- /dev/null +++ b/editor/libeditor/tests/test_composition_event_created_in_chrome.html @@ -0,0 +1,82 @@ +<!doctype html> +<html> + +<head> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + +<input id="input"> + +<script type="application/javascript"> + +// In nsEditorEventListener, when listening event is not created with proper +// event interface, it asserts the fact. +SimpleTest.waitForExplicitFinish(); + +var gInputElement = document.getElementById("input"); + +function getEditorIMESupport(aInputElement) +{ + var editableElement = SpecialPowers.wrap(aInputElement).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement); + ok(editableElement, "The input element doesn't have nsIDOMNSEditableElement interface"); + ok(editableElement.editor, "There is no editor for the input element"); + var editorIMESupport = SpecialPowers.wrap(editableElement).editor.QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport); + ok(editorIMESupport, "The input element doesn't have nsIEditorIMESupport interface"); + return editorIMESupport; +} + +var gEditorIMESupport; + +function testNotGenerateCompositionByCreatedEvents(aEventInterface) +{ + var compositionEvent = document.createEvent(aEventInterface); + if (compositionEvent.initCompositionEvent) { + compositionEvent.initCompositionEvent("compositionstart", true, true, window, "", ""); + } else if (compositionEvent.initMouseEvent) { + compositionEvent.initMouseEvent("compositionstart", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + } + gInputElement.dispatchEvent(compositionEvent); + ok(!gEditorIMESupport.composing, "Composition shouldn't be started with a created compositionstart event (" + aEventInterface + ")"); + + compositionEvent = document.createEvent(aEventInterface); + if (compositionEvent.initCompositionEvent) { + compositionEvent.initCompositionEvent("compositionupdate", true, false, window, "abc", ""); + } else if (compositionEvent.initMouseEvent) { + compositionEvent.initMouseEvent("compositionupdate", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + } + gInputElement.dispatchEvent(compositionEvent); + ok(!gEditorIMESupport.composing, "Composition shouldn't be started with a created compositionupdate event (" + aEventInterface + ")"); + is(gInputElement.value, "", "Input element shouldn't be modified with a created compositionupdate event (" + aEventInterface + ")"); + + compositionEvent = document.createEvent(aEventInterface); + if (compositionEvent.initCompositionEvent) { + compositionEvent.initCompositionEvent("compositionend", true, false, window, "abc", ""); + } else if (compositionEvent.initMouseEvent) { + compositionEvent.initMouseEvent("compositionend", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + } + gInputElement.dispatchEvent(compositionEvent); + ok(!gEditorIMESupport.composing, "Composition shouldn't be committed with a created compositionend event (" + aEventInterface + ")"); + is(gInputElement.value, "", "Input element shouldn't be committed with a created compositionend event (" + aEventInterface + ")"); +} + +function doTests() +{ + gInputElement.focus(); + gEditorIMESupport = getEditorIMESupport(gInputElement); + + testNotGenerateCompositionByCreatedEvents("CompositionEvent"); + testNotGenerateCompositionByCreatedEvents("MouseEvent"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTests); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_contenteditable_focus.html b/editor/libeditor/tests/test_contenteditable_focus.html new file mode 100644 index 000000000..051ac7b2f --- /dev/null +++ b/editor/libeditor/tests/test_contenteditable_focus.html @@ -0,0 +1,209 @@ +<html> +<head> + <title>Test for contenteditable focus</title> + <script type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + First text in this document.<br> + <input id="inputText" type="text"><br> + <input id="inputTextReadonly" type="text" readonly><br> + <input id="inputButton" type="button" value="input[type=button]"><br> + <button id="button">button</button><br> + <div id="editor" contenteditable="true"> + editable contents.<br> + <input id="inputTextInEditor" type="text"><br> + <input id="inputTextReadonlyInEditor" type="text" readonly><br> + <input id="inputButtonInEditor" type="button" value="input[type=button]"><br> + <button id="buttonInEditor">button</button><br> + <div id="noeditableInEditor" contenteditable="false"> + <span id="spanInNoneditableInEditor">span element in noneditable in editor</span><br> + <input id="inputTextInNoneditableInEditor" type="text"><br> + <input id="inputTextReadonlyInNoneditableInEditor" type="text" readonly><br> + <input id="inputButtonInNoneditableInEditor" type="button" value="input[type=button]"><br> + <button id="buttonInNoneditableInEditor">button</button><br> + </div> + <span id="spanInEditor">span element in editor</span><br> + </div> + <div id="otherEditor" contenteditable="true"> + other editor. + </div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +function runTests() +{ + runTestsInternal(); + SimpleTest.finish(); +} + +function runTestsInternal() +{ + var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]. + getService(SpecialPowers.Ci.nsIFocusManager); + // XXX using selCon for checking the visibility of the caret, however, + // selCon is shared in document, cannot get the element of owner of the + // caret from javascript? + var selCon = SpecialPowers.wrap(window). + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsIWebNavigation). + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsISelectionDisplay). + QueryInterface(SpecialPowers.Ci.nsISelectionController); + var selection = window.getSelection(); + + var inputText = document.getElementById("inputText"); + var inputTextReadonly = document.getElementById("inputTextReadonly"); + var inputButton = document.getElementById("inputButton"); + var button = document.getElementById("button"); + var editor = document.getElementById("editor"); + var inputTextInEditor = document.getElementById("inputTextInEditor"); + var inputTextReadonlyInEditor = document.getElementById("inputTextReadonlyInEditor"); + var inputButtonInEditor = document.getElementById("inputButtonInEditor"); + var noeditableInEditor = document.getElementById("noeditableInEditor"); + var spanInNoneditableInEditor = document.getElementById("spanInNoneditableInEditor"); + var inputTextInNoneditableInEditor = document.getElementById("inputTextInNoneditableInEditor"); + var inputTextReadonlyInNoneditableInEditor = document.getElementById("inputTextReadonlyInNoneditableInEditor"); + var inputButtonInNoneditableInEditor = document.getElementById("inputButtonInNoneditableInEditor"); + var buttonInNoneditableInEditor = document.getElementById("buttonInNoneditableInEditor"); + var spanInEditor = document.getElementById("spanInEditor"); + var otherEditor = document.getElementById("otherEditor"); + + // XXX if there is a contenteditable element, HTML editor sets dom selection + // to first editable node, but this makes inconsistency with normal document + // behavior. + todo_is(selection.rangeCount, 0, "unexpected selection range is there"); + ok(!selCon.caretVisible, "caret is visible in the document"); + // Move focus to inputTextInEditor + inputTextInEditor.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), inputTextInEditor, + "inputTextInEditor didn't get focus"); + todo_is(selection.rangeCount, 0, "unexpected selection range is there"); + ok(selCon.caretVisible, "caret isn't visible in the inputTextInEditor"); + // Move focus to the editor + editor.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), editor, + "editor didn't get focus"); + is(selection.rangeCount, 1, + "there is no selection range when editor has focus"); + var range = selection.getRangeAt(0); + ok(range.collapsed, "the selection range isn't collapsed"); + var startNode = range.startContainer; + is(startNode.nodeType, 1, "the caret isn't set to the div node"); + is(startNode, editor, "the caret isn't set to the editor"); + ok(selCon.caretVisible, "caret isn't visible in the editor"); + // Move focus to other editor + otherEditor.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), otherEditor, + "the other editor didn't get focus"); + is(selection.rangeCount, 1, + "there is no selection range when the other editor has focus"); + range = selection.getRangeAt(0); + ok(range.collapsed, "the selection range isn't collapsed"); + var startNode = range.startContainer; + is(startNode.nodeType, 1, "the caret isn't set to the div node"); + is(startNode, otherEditor, "the caret isn't set to the other editor"); + ok(selCon.caretVisible, "caret isn't visible in the other editor"); + // Move focus to inputTextInEditor + inputTextInEditor.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), inputTextInEditor, + "inputTextInEditor didn't get focus #2"); + is(selection.rangeCount, 1, "selection range is lost from the document"); + range = selection.getRangeAt(0); + ok(range.collapsed, "the selection range isn't collapsed"); + var startNode = range.startContainer; + is(startNode.nodeType, 1, "the caret isn't set to the div node"); + // XXX maybe, the caret can stay on the other editor if it's better. + is(startNode, editor, + "the caret should stay on the other editor"); + ok(selCon.caretVisible, + "caret isn't visible in the inputTextInEditor"); + // Move focus to the other editor again + otherEditor.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), otherEditor, + "the other editor didn't get focus #2"); + // Set selection to the span element in the editor (unfocused) + range = document.createRange(); + range.setStart(spanInEditor.firstChild, 5); + selection.removeAllRanges(); + selection.addRange(range); + is(selection.rangeCount, 1, "selection range is lost from the document"); + is(SpecialPowers.unwrap(fm.focusedElement), otherEditor, + "the other editor shouldn't lose focus by selection range change"); + ok(selCon.caretVisible, "caret isn't visible in inputTextInEditor"); + // Move focus to the editor + editor.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), editor, + "the editor didn't get focus #2"); + is(selection.rangeCount, 1, "selection range is lost from the document"); + range = selection.getRangeAt(0); + ok(range.collapsed, "the selection range isn't collapsed"); + is(range.startOffset, 5, + "the caret is moved when the editor was focused (offset)"); + var startNode = range.startContainer; + is(startNode.nodeType, 3, "the caret isn't in text node"); + is(startNode.parentNode, spanInEditor, + "the caret is moved when the editor was focused (node)"); + ok(selCon.caretVisible, "caret isn't visible in the editor (spanInEditor)"); + + // Move focus to each focusable element in the editor. + function testFocusMove(aSetFocusElementID, aFocusable, aCaretVisible) + { + editor.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), editor, + "testFocusMove: the editor didn't get focus at initializing (" + + aSetFocusElementID + ")"); + var setFocusElement = document.getElementById(aSetFocusElementID); + setFocusElement.focus(); + if (aFocusable) { + is(SpecialPowers.unwrap(fm.focusedElement), setFocusElement, + "testFocusMove: the " + aSetFocusElementID + + " didn't get focus"); + } else { + is(SpecialPowers.unwrap(fm.focusedElement), editor, + "testFocusMove: the editor lost focus by focus() of the " + + aSetFocusElementID); + } + if (aCaretVisible) { + ok(selCon.caretVisible, + "testFocusMove: caret isn't visible when the " + + aSetFocusElementID + " has focus"); + } else { + ok(!selCon.caretVisible, + "testFocusMove: caret is visible when the " + + aSetFocusElementID + " has focus"); + } + } + testFocusMove("inputTextInEditor", true, true); + testFocusMove("inputTextReadonlyInEditor", true, true); + // XXX shouldn't the caret become invisible? + testFocusMove("inputButtonInEditor", true, true); + testFocusMove("noeditableInEditor", false, true); + testFocusMove("spanInNoneditableInEditor", false, true); + testFocusMove("inputTextInNoneditableInEditor", true, true); + testFocusMove("inputTextReadonlyInNoneditableInEditor", true, true); + testFocusMove("inputButtonInNoneditableInEditor", true, false); + testFocusMove("buttonInNoneditableInEditor", true, false); + testFocusMove("spanInEditor", false, true); + testFocusMove("inputText", true, true); + testFocusMove("inputTextReadonly", true, true); + testFocusMove("inputButton", true, false); + testFocusMove("button", true, false); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_contenteditable_text_input_handling.html b/editor/libeditor/tests/test_contenteditable_text_input_handling.html new file mode 100644 index 000000000..06b95fbb8 --- /dev/null +++ b/editor/libeditor/tests/test_contenteditable_text_input_handling.html @@ -0,0 +1,329 @@ +<html> +<head> + <title>Test for text input event handling on contenteditable editor</title> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <p id="static">static content<input id="inputInStatic"><textarea id="textareaInStatic"></textarea></p> + <p id="editor"contenteditable="true">content editable<input id="inputInEditor"><textarea id="textareaInEditor"></textarea></p> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +const kLF = !navigator.platform.indexOf("Win") ? "\r\n" : "\n"; + +function runTests() +{ + var fm = Components.classes["@mozilla.org/focus-manager;1"]. + getService(Components.interfaces.nsIFocusManager); + + var listener = { + handleEvent: function _hv(aEvent) + { + aEvent.preventDefault(); // prevent the browser default behavior + } + }; + var els = Components.classes["@mozilla.org/eventlistenerservice;1"]. + getService(Components.interfaces.nsIEventListenerService); + els.addSystemEventListener(window, "keypress", listener, false); + + var staticContent = document.getElementById("static"); + staticContent._defaultValue = getTextValue(staticContent); + staticContent._isFocusable = false; + staticContent._isEditable = false; + staticContent._isContentEditable = false; + staticContent._description = "non-editable p element"; + var inputInStatic = document.getElementById("inputInStatic"); + inputInStatic._defaultValue = getTextValue(inputInStatic); + inputInStatic._isFocusable = true; + inputInStatic._isEditable = true; + inputInStatic._isContentEditable = false; + inputInStatic._description = "input element in static content"; + var textareaInStatic = document.getElementById("textareaInStatic"); + textareaInStatic._defaultValue = getTextValue(textareaInStatic); + textareaInStatic._isFocusable = true; + textareaInStatic._isEditable = true; + textareaInStatic._isContentEditable = false; + textareaInStatic._description = "textarea element in static content"; + var editor = document.getElementById("editor"); + editor._defaultValue = getTextValue(editor); + editor._isFocusable = true; + editor._isEditable = true; + editor._isContentEditable = true; + editor._description = "contenteditable editor"; + var inputInEditor = document.getElementById("inputInEditor"); + inputInEditor._defaultValue = getTextValue(inputInEditor); + inputInEditor._isFocusable = true; + inputInEditor._isEditable = true; + inputInEditor._isContentEditable = false; + inputInEditor._description = "input element in contenteditable editor"; + var textareaInEditor = document.getElementById("textareaInEditor"); + textareaInEditor._defaultValue = getTextValue(textareaInEditor); + textareaInEditor._isFocusable = true; + textareaInEditor._isEditable = true; + textareaInEditor._isContentEditable = false; + textareaInEditor._description = "textarea element in contenteditable editor"; + + function getTextValue(aElement) + { + if (aElement == editor) { + var value = ""; + for (var node = aElement.firstChild; node; node = node.nextSibling) { + if (node.nodeType == Node.TEXT_NODE) { + value += node.data; + } else if (node.nodeType == Node.ELEMENT_NODE) { + var tagName = node.tagName.toLowerCase(); + switch (tagName) { + case "input": + case "textarea": + value += kLF; + break; + default: + ok(false, "Undefined tag is used in the editor: " + tagName); + break; + } + } + } + return value; + } + return aElement.value; + } + + function testTextInput(aFocus) + { + var when = " when " + + ((aFocus && aFocus._isFocusable) ? aFocus._description + " has focus" : + "nobody has focus"); + + function checkValue(aElement, aInsertedText) + { + if (aElement == aFocus && aElement._isEditable) { + is(getTextValue(aElement), aInsertedText + aElement._defaultValue, + aElement._description + + " wasn't edited by synthesized key events" + when); + return; + } + is(getTextValue(aElement), aElement._defaultValue, + aElement._description + + " was edited by synthesized key events" + when); + } + + if (aFocus && aFocus._isFocusable) { + aFocus.focus(); + is(fm.focusedElement, aFocus, + aFocus._description + " didn't get focus at preparing tests" + when); + } else { + var focusedElement = fm.focusedElement; + if (focusedElement) { + focusedElement.blur(); + } + ok(!fm.focusedElement, + "Failed to blur at preparing tests" + when); + } + + if (aFocus && aFocus._isFocusable) { + synthesizeKey("A", { }); + synthesizeKey("B", { }); + synthesizeKey("C", { }); + checkValue(staticContent, "ABC"); + checkValue(inputInStatic, "ABC"); + checkValue(textareaInStatic, "ABC"); + checkValue(editor, "ABC"); + checkValue(inputInEditor, "ABC"); + checkValue(textareaInEditor, "ABC"); + + if (aFocus._isEditable) { + synthesizeKey("VK_BACK_SPACE", { }); + synthesizeKey("VK_BACK_SPACE", { }); + synthesizeKey("VK_BACK_SPACE", { }); + checkValue(staticContent, ""); + checkValue(inputInStatic, ""); + checkValue(textareaInStatic, ""); + checkValue(editor, ""); + checkValue(inputInEditor, ""); + checkValue(textareaInEditor, ""); + } + } + + // When key events are fired on unfocused editor. + function testDispatchedKeyEvent(aTarget) + { + var targetDescription = " (dispatched to " + aTarget._description + ")"; + function dispatchKeyEvent(aKeyCode, aChar, aTarget) + { + var keyEvent = document.createEvent("KeyboardEvent"); + keyEvent.initKeyEvent("keypress", true, true, null, false, false, + false, false, aKeyCode, + aChar ? aChar.charCodeAt(0) : 0); + aTarget.dispatchEvent(keyEvent); + } + + function checkValueForDispatchedKeyEvent(aElement, aInsertedText) + { + if (aElement == aTarget && aElement._isEditable && + (!aElement._isContentEditable || aElement == aFocus)) { + is(getTextValue(aElement), aInsertedText + aElement._defaultValue, + aElement._description + + " wasn't edited by dispatched key events" + + when + targetDescription); + return; + } + if (aElement == aTarget) { + is(getTextValue(aElement), aElement._defaultValue, + aElement._description + + " was edited by dispatched key events" + + when + targetDescription); + return; + } + is(getTextValue(aElement), aElement._defaultValue, + aElement._description + + " was edited by key events unexpectedly" + + when + targetDescription); + } + + dispatchKeyEvent(0, "A", aTarget); + dispatchKeyEvent(0, "B", aTarget); + dispatchKeyEvent(0, "C", aTarget); + + checkValueForDispatchedKeyEvent(staticContent, "ABC"); + checkValueForDispatchedKeyEvent(inputInStatic, "ABC"); + checkValueForDispatchedKeyEvent(textareaInStatic, "ABC"); + checkValueForDispatchedKeyEvent(editor, "ABC"); + checkValueForDispatchedKeyEvent(inputInEditor, "ABC"); + checkValueForDispatchedKeyEvent(textareaInEditor, "ABC"); + + const nsIDOMKeyEvent = Components.interfaces.nsIDOMKeyEvent; + dispatchKeyEvent(nsIDOMKeyEvent.DOM_VK_BACK_SPACE, 0, aTarget); + dispatchKeyEvent(nsIDOMKeyEvent.DOM_VK_BACK_SPACE, 0, aTarget); + dispatchKeyEvent(nsIDOMKeyEvent.DOM_VK_BACK_SPACE, 0, aTarget); + + checkValueForDispatchedKeyEvent(staticContent, ""); + checkValueForDispatchedKeyEvent(inputInStatic, ""); + checkValueForDispatchedKeyEvent(textareaInStatic, ""); + checkValueForDispatchedKeyEvent(editor, ""); + checkValueForDispatchedKeyEvent(inputInEditor, ""); + checkValueForDispatchedKeyEvent(textareaInEditor, ""); + } + + testDispatchedKeyEvent(staticContent); + testDispatchedKeyEvent(inputInStatic); + testDispatchedKeyEvent(textareaInStatic); + testDispatchedKeyEvent(editor); + testDispatchedKeyEvent(inputInEditor); + testDispatchedKeyEvent(textareaInEditor); + + if (!aFocus._isEditable) { + return; + } + + // IME + // input first character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + var queryText = synthesizeQueryTextContent(0, 100); + ok(queryText, "query text event result is null" + when); + if (!queryText) { + return; + } + ok(queryText.succeeded, "query text event failed" + when); + if (!queryText.succeeded) { + return; + } + is(queryText.text, "\u3089" + aFocus._defaultValue, + "composing text is incorrect" + when); + var querySelectedText = synthesizeQuerySelectedText(); + ok(querySelectedText, "query selected text event result is null" + when); + if (!querySelectedText) { + return; + } + ok(querySelectedText.succeeded, "query selected text event failed" + when); + if (!querySelectedText.succeeded) { + return; + } + is(querySelectedText.offset, 1, + "query selected text event returns wrong offset" + when); + is(querySelectedText.text, "", + "query selected text event returns wrong selected text" + when); + // commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + queryText = synthesizeQueryTextContent(0, 100); + ok(queryText, "query text event result is null after commit" + when); + if (!queryText) { + return; + } + ok(queryText.succeeded, "query text event failed after commit" + when); + if (!queryText.succeeded) { + return; + } + is(queryText.text, "\u3089" + aFocus._defaultValue, + "composing text is incorrect after commit" + when); + querySelectedText = synthesizeQuerySelectedText(); + ok(querySelectedText, + "query selected text event result is null after commit" + when); + if (!querySelectedText) { + return; + } + ok(querySelectedText.succeeded, + "query selected text event failed after commit" + when); + if (!querySelectedText.succeeded) { + return; + } + is(querySelectedText.offset, 1, + "query selected text event returns wrong offset after commit" + when); + is(querySelectedText.text, "", + "query selected text event returns wrong selected text after commit" + + when); + + checkValue(staticContent, "\u3089"); + checkValue(inputInStatic, "\u3089"); + checkValue(textareaInStatic, "\u3089"); + checkValue(editor, "\u3089"); + checkValue(inputInEditor, "\u3089"); + checkValue(textareaInEditor, "\u3089"); + + synthesizeKey("VK_BACK_SPACE", { }); + checkValue(staticContent, ""); + checkValue(inputInStatic, ""); + checkValue(textareaInStatic, ""); + checkValue(editor, ""); + checkValue(inputInEditor, ""); + checkValue(textareaInEditor, ""); + } + + testTextInput(inputInStatic); + testTextInput(textareaInStatic); + testTextInput(editor); + testTextInput(inputInEditor); + testTextInput(textareaInEditor); + + els.removeSystemEventListener(window, "keypress", listener, false); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_css_chrome_load_access.html b/editor/libeditor/tests/test_css_chrome_load_access.html new file mode 100644 index 000000000..b6bb3fb46 --- /dev/null +++ b/editor/libeditor/tests/test_css_chrome_load_access.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1245681 +--> +<head> + <title>Test for Bug 1245681</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1245681">Mozilla Bug 1245681</a> +<p id="display"></p> +<div id="content"> + <iframe></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const Ci = SpecialPowers.Ci; +var styleSheets = null; + +function runTest() { + + var editframe = window.frames[0]; + var editdoc = editframe.document; + editdoc.designMode = 'on'; + var editor = SpecialPowers.wrap(editframe) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession) + .getEditorForWindow(editframe); + + styleSheets = editor.QueryInterface(Ci.nsIEditorStyleSheets); + + // test 1: try to access chrome:// url that is accessible to content + try + { + styleSheets.addOverrideStyleSheet("chrome://browser/content/pageinfo/pageInfo.css"); + ok(true, "should be allowed to access chrome://*.css if contentaccessible"); + } + catch (ex) { + ok(false, "should be allowed to access chrome://*.css if contentaccessible"); + } + + // test 2: try to access chrome:// url that is *not* accessible to content + // please note that addOverrideStyleSheet() is triggered by the system, + // so the load should also *always* succeed. + try + { + styleSheets.addOverrideStyleSheet("chrome://mozapps/skin/aboutNetworking.css"); + ok(true, "should be allowed to access chrome://*.css even if *not* contentaccessible"); + } + catch (ex) { + ok(false, "should be allowed to access chrome://*.css even if *not* contentaccessible"); + } + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html new file mode 100644 index 000000000..d1716a228 --- /dev/null +++ b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html @@ -0,0 +1,182 @@ +<html> +<head> + <title>Test for input event of text editor</title> + <script type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" + src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <iframe id="editor1" src="data:text/html,<html><body contenteditable id='eventTarget'></body></html>"></iframe> + <iframe id="editor2" src="data:text/html,<html contenteditable id='eventTarget'><body></body></html>"></iframe> + <iframe id="editor3" src="data:text/html,<html><body><div contenteditable id='eventTarget'></div></body></html>"></iframe> + <iframe id="editor4" src="data:text/html,<html contenteditable id='eventTarget'><body><div contenteditable id='editTarget'></div></body></html>"></iframe> + <iframe id="editor5" src="data:text/html,<html><body id='eventTarget'></body><script>document.designMode='on';</script></html>"></iframe> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +const kIsMac = navigator.platform.indexOf("Mac") == 0; + +function runTests() +{ + function doTests(aDocument, aWindow, aDescription) + { + aDescription += ": "; + aWindow.focus(); + + var body = aDocument.body; + + var eventTarget = aDocument.getElementById("eventTarget"); + // The event target must be focusable because it's the editing host. + eventTarget.focus(); + + var editTarget = aDocument.getElementById("editTarget"); + if (!editTarget) { + editTarget = eventTarget; + } + + // Root element never can be edit target. If the editTarget is the root + // element, replace with its body. + if (editTarget == aDocument.documentElement) { + editTarget = body; + } + + editTarget.innerHTML = ""; + + // If the editTarget isn't its editing host, move caret to the start of it. + if (eventTarget != editTarget) { + aDocument.getSelection().collapse(editTarget, 0); + } + + var inputEvent = null; + + var handler = function (aEvent) { + is(aEvent.target, eventTarget, + "input event is fired on unexpected element: " + aEvent.target.tagName); + ok(!aEvent.cancelable, "input event must not be cancelable"); + ok(aEvent.bubbles, "input event must be bubbles"); + if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) { + var duration = Math.abs(window.performance.now() - aEvent.timeStamp); + ok(duration < 30 * 1000, + "perhaps, timestamp wasn't set correctly :" + aEvent.timeStamp + + " (expected it to be within 30s of the current time but it " + + "differed by " + duration + "ms)"); + } else { + var eventTime = new Date(aEvent.timeStamp); + var duration = Math.abs(Date.now() - aEvent.timeStamp); + ok(duration < 30 * 1000, + "perhaps, timestamp wasn't set correctly :" + + eventTime.toLocaleString() + + " (expected it to be within 30s of the current time but it " + + "differed by " + duration + "ms)"); + } + inputEvent = aEvent; + }; + + aWindow.addEventListener("input", handler, true); + + inputEvent = null; + synthesizeKey("a", { }, aWindow); + is(editTarget.innerHTML, "a", aDescription + "wrong element was edited"); + ok(inputEvent, aDescription + "input event wasn't fired by 'a' key"); + ok(inputEvent.isTrusted, aDescription + "input event by 'a' key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("VK_BACK_SPACE", { }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by BackSpace key"); + ok(inputEvent.isTrusted, aDescription + "input event by BackSpace key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("B", { shiftKey: true }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by 'B' key"); + ok(inputEvent.isTrusted, aDescription + "input event by 'B' key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("VK_RETURN", { }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by Enter key"); + ok(inputEvent.isTrusted, aDescription + "input event by Enter key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("C", { shiftKey: true }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by 'C' key"); + ok(inputEvent.isTrusted, aDescription + "input event by 'C' key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("VK_RETURN", { }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by Enter key (again)"); + ok(inputEvent.isTrusted, aDescription + "input event by Enter key (again) wasn't trusted event"); + + inputEvent = null; + editTarget.innerHTML = "foo-bar"; + ok(!inputEvent, aDescription + "input event was fired by setting value"); + + inputEvent = null; + editTarget.innerHTML = ""; + ok(!inputEvent, aDescription + "input event was fired by setting empty value"); + + inputEvent = null; + synthesizeKey(" ", { }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by Space key"); + ok(inputEvent.isTrusted, aDescription + "input event by Space key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("VK_DELETE", { }, aWindow); + ok(!inputEvent, aDescription + "input event was fired by Delete key at the end"); + + inputEvent = null; + synthesizeKey("VK_LEFT", { }, aWindow); + ok(!inputEvent, aDescription + "input event was fired by Left key"); + + inputEvent = null; + synthesizeKey("VK_DELETE", { }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by Delete key at the start"); + ok(inputEvent.isTrusted, aDescription + "input event by Delete key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("z", { accelKey: true }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by Undo"); + ok(inputEvent.isTrusted, aDescription + "input event by Undo wasn't trusted event"); + + inputEvent = null; + synthesizeKey("z", { accelKey: true, shiftKey: true }, aWindow); + ok(inputEvent, aDescription + "input event wasn't fired by Redo"); + ok(inputEvent.isTrusted, aDescription + "input event by Redo wasn't trusted event"); + + aWindow.removeEventListener("input", handler, true); + } + + doTests(document.getElementById("editor1").contentDocument, + document.getElementById("editor1").contentWindow, + "Editor1, body has contenteditable attribute"); + doTests(document.getElementById("editor2").contentDocument, + document.getElementById("editor2").contentWindow, + "Editor2, html has contenteditable attribute"); + doTests(document.getElementById("editor3").contentDocument, + document.getElementById("editor3").contentWindow, + "Editor3, div has contenteditable attribute"); + doTests(document.getElementById("editor4").contentDocument, + document.getElementById("editor4").contentWindow, + "Editor4, html and div has contenteditable attribute"); + doTests(document.getElementById("editor5").contentDocument, + document.getElementById("editor5").contentWindow, + "Editor5, html and div has contenteditable attribute"); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_dom_input_event_on_texteditor.html b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html new file mode 100644 index 000000000..b1395e99c --- /dev/null +++ b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html @@ -0,0 +1,140 @@ +<html> +<head> + <title>Test for input event of text editor</title> + <script type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" + src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <input type="text" id="input"> + <textarea id="textarea"></textarea> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +const kIsMac = navigator.platform.indexOf("Mac") == 0; + +function runTests() +{ + function doTests(aElement, aDescription, aIsTextarea) + { + aDescription += ": "; + aElement.focus(); + aElement.value = ""; + + var inputEvent = null; + + var handler = function (aEvent) { + is(aEvent.target, aElement, + "input event is fired on unexpected element: " + aEvent.target.tagName); + ok(!aEvent.cancelable, "input event must not be cancelable"); + ok(aEvent.bubbles, "input event must be bubbles"); + if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) { + var duration = Math.abs(window.performance.now() - aEvent.timeStamp); + ok(duration < 30 * 1000, + "perhaps, timestamp wasn't set correctly :" + aEvent.timeStamp + + " (expected it to be within 30s of the current time but it " + + "differed by " + duration + "ms)"); + } else { + var eventTime = new Date(aEvent.timeStamp); + var duration = Math.abs(Date.now() - aEvent.timeStamp); + ok(duration < 30 * 1000, + "perhaps, timestamp wasn't set correctly :" + + eventTime.toLocaleString() + + " (expected it to be within 30s of the current time but it " + + "differed by " + duration + "ms)"); + } + inputEvent = aEvent; + }; + + aElement.addEventListener("input", handler, true); + + inputEvent = null; + synthesizeKey("a", { }); + is(aElement.value, "a", aDescription + "'a' key didn't change the value"); + ok(inputEvent, aDescription + "input event wasn't fired by 'a' key"); + ok(inputEvent.isTrusted, aDescription + "input event by 'a' key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("VK_BACK_SPACE", { }); + is(aElement.value, "", aDescription + "BackSpace key didn't remove the value"); + ok(inputEvent, aDescription + "input event wasn't fired by BackSpace key"); + ok(inputEvent.isTrusted, aDescription + "input event by BackSpace key wasn't trusted event"); + + if (aIsTextarea) { + inputEvent = null; + synthesizeKey("VK_RETURN", { }); + is(aElement.value, "\n", aDescription + "Enter key didn't change the value"); + ok(inputEvent, aDescription + "input event wasn't fired by Enter key"); + ok(inputEvent.isTrusted, aDescription + "input event by Enter key wasn't trusted event"); + } + + inputEvent = null; + aElement.value = "foo-bar"; + is(aElement.value, "foo-bar", aDescription + "value wasn't set"); + ok(!inputEvent, aDescription + "input event was fired by setting value"); + + inputEvent = null; + aElement.value = ""; + is(aElement.value, "", aDescription + "value wasn't set (empty)"); + ok(!inputEvent, aDescription + "input event was fired by setting empty value"); + + inputEvent = null; + synthesizeKey(" ", { }); + is(aElement.value, " ", aDescription + "Space key didn't change the value"); + ok(inputEvent, aDescription + "input event wasn't fired by Space key"); + ok(inputEvent.isTrusted, aDescription + "input event by Space key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("VK_DELETE", { }); + is(aElement.value, " ", aDescription + "Delete key removed the value"); + ok(!inputEvent, aDescription + "input event was fired by Delete key at the end"); + + inputEvent = null; + synthesizeKey("VK_LEFT", { }); + is(aElement.value, " ", aDescription + "Left key removed the value"); + ok(!inputEvent, aDescription + "input event was fired by Left key"); + + inputEvent = null; + synthesizeKey("VK_DELETE", { }); + is(aElement.value, "", aDescription + "Delete key didn't remove the value"); + ok(inputEvent, aDescription + "input event wasn't fired by Delete key at the start"); + ok(inputEvent.isTrusted, aDescription + "input event by Delete key wasn't trusted event"); + + inputEvent = null; + synthesizeKey("z", { accelKey: true }); + is(aElement.value, " ", aDescription + "Accel+Z key didn't undo the value"); + ok(inputEvent, aDescription + "input event wasn't fired by Undo"); + ok(inputEvent.isTrusted, aDescription + "input event by Undo wasn't trusted event"); + + inputEvent = null; + synthesizeKey("z", { accelKey: true, shiftKey: true }); + is(aElement.value, "", aDescription + "Accel+Y key didn't redo the value"); + ok(inputEvent, aDescription + "input event wasn't fired by Redo"); + ok(inputEvent.isTrusted, aDescription + "input event by Redo wasn't trusted event"); + + aElement.removeEventListener("input", handler, true); + } + + doTests(document.getElementById("input"), "<input type=\"text\">", false); + doTests(document.getElementById("textarea"), "<textarea>", true); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_dragdrop.html b/editor/libeditor/tests/test_dragdrop.html new file mode 100644 index 000000000..c992b7142 --- /dev/null +++ b/editor/libeditor/tests/test_dragdrop.html @@ -0,0 +1,178 @@ +<!doctype html> +<html> + +<head> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <span id="text" style="font-size: 40px;">Some Text</span> + + <input id="input" value="Drag Me"> + <textarea id="textarea">Some Text To Drag</textarea> + <p id="contenteditable" contenteditable="true">This is some <b id="bold">editable</b> text.</p> + <p id="nestedce" contenteditable="true"><span id="first"> </span>First letter <span id="noneditable" contenteditable="false">Middle</span> Last part</p> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// This listener allows us to clear the default data for the selection added for the drag. +var shouldClear = false; +window.addEventListener("dragstart", function (event) { if (shouldClear) event.dataTransfer.clearData() }, true); + +function doTest() +{ + const htmlContextData = { type: 'text/_moz_htmlcontext', + data: '<html><body></body></html>' }; + const htmlInfoData = { type: 'text/_moz_htmlinfo', data: '0,0' }; + const htmlData = { type: 'text/html', data: '<span id="text" style="font-size: 40px;">Some Text</span>' }; + + const htmlContextDataEditable = { type: 'text/_moz_htmlcontext', + data: '<html><body><p id="contenteditable" contenteditable="true"></p></body></html>' }; + + var text = document.getElementById("text"); + var input = document.getElementById("input"); + var contenteditable = document.getElementById("contenteditable"); + + var selection = window.getSelection(); + + // -------- Test dragging regular text + selection.selectAllChildren(text); + var result = synthesizeDragStart(text, [[htmlContextData, htmlInfoData, htmlData, + {type: "text/plain", data: "Some Text"}]], window, 40, 10); + is(result, null, "Test dragging regular text"); + + // -------- Test dragging text from an <input> + input.setSelectionRange(1, 4); + result = synthesizeDragStart(input, [[{type: "text/plain", data: "rag"}]], window, 25, 6); + is(result, null, "Test dragging input"); + + // -------- Test dragging text from a <textarea> + textarea.setSelectionRange(1, 7); + result = synthesizeDragStart(textarea, [[{type: "text/plain", data: "ome Te"}]], window, 25, 6); + is(result, null, "Test dragging textarea"); + textarea.blur(); + + // -------- Test dragging text from a contenteditable + selection.selectAllChildren(contenteditable.childNodes[1]); + result = synthesizeDragStart(contenteditable.childNodes[1], + [[htmlContextDataEditable, htmlInfoData, + {type: 'text/html', data: '<b id="bold">editable</b>' }, + {type: "text/plain", data: "editable"}]], window, 5, 6); + is(result, null, "Test dragging contenteditable"); + contenteditable.blur(); + + // -------- Test dragging regular text of text/html to <input> + + selection.selectAllChildren(text); + input.value = ""; + synthesizeDrop(text, input, [], "copy"); + is(input.value, "Some Text", "Drag text/html onto input"); + + // -------- Test dragging regular text of text/html to disabled <input> + + selection.selectAllChildren(text); + input.value = ""; + input.disabled = true; + synthesizeDrop(text, input, [], "copy"); + is(input.value, "", "Drag text/html onto disabled input"); + input.disabled = false; + + // -------- Test dragging regular text of text/html to readonly <input> + + selection.selectAllChildren(text); + input.readOnly = true; + synthesizeDrop(text, input, [], "copy"); + is(input.value, "", "Drag text/html onto readonly input"); + input.readOnly = false; + + // -------- Test dragging regular text of text/html to <input>. This sets + // shouldClear to true so that the default drag data is not present + // and we can use the data passed to synthesizeDrop. This allows + // testing of a drop with just text/html. + shouldClear = true; + selection.selectAllChildren(text); + input.value = ""; + synthesizeDrop(text, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}]], "copy"); + is(input.value, "", "Drag text/html onto input"); + + // -------- Test dragging regular text of text/plain and text/html to <input> + + selection.selectAllChildren(text); + input.value = ""; + synthesizeDrop(text, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}, + {type: "text/plain", data: "Some Plain Text"}]], "copy"); + is(input.value, "Some Plain Text", "Drag text/html and text/plain onto input"); + + // -------- Test dragging regular text of text/plain to <textarea> + +// XXXndeakin Can't test textareas due to some event handling issue +// selection.selectAllChildren(text); +// synthesizeDrop(text, textarea, [[{type: "text/plain", data: "Somewhat Longer Text"}]], "copy"); +// is(textarea.value, "Somewhat Longer Text", "Drag text/plain onto textarea"); + + // -------- Test dragging special text type of text/plain to contenteditable + + selection.selectAllChildren(text); + synthesizeDrop(text, input, [[{type: "text/x-moz-text-internal", data: "Some Special Text"}]], "copy"); + is(input.value, "Some Plain Text", "Drag text/x-moz-text-internal onto input"); + + // -------- Test dragging regular text of text/plain to contenteditable + + selection.selectAllChildren(text); + synthesizeDrop(text, contenteditable, [[{type: "text/plain", data: "Sample Text"}]], "copy"); + is(contenteditable.childNodes.length, 3, "Drag text/plain onto contenteditable child nodes"); + is(contenteditable.textContent, "This is some editable text.Sample Text", + "Drag text/plain onto contenteditable text"); + + // -------- Test dragging regular text of text/html to contenteditable + + selection.selectAllChildren(text); + synthesizeDrop(text, contenteditable, [[{type: "text/html", data: "Sample <i>Italic</i> Text"}]], "copy"); + is(contenteditable.childNodes.length, 6, "Drag text/html onto contenteditable child nodes"); + is(contenteditable.childNodes[4].tagName, "I", "Drag text/html onto contenteditable italic"); + is(contenteditable.childNodes[4].textContent, "Italic", "Drag text/html onto contenteditable italic text"); + + // -------- Test dragging contenteditable to <input> + + selection.selectAllChildren(document.getElementById("bold")); + synthesizeDrop(bold, input, [[{type: "text/html", data: "<b>editable</b>"}, + {type: "text/plain", data: "editable"}]], "copy"); + is(input.value, "Some Plain Texteditable", "Move text/html and text/plain from contenteditable onto input"); + + // -------- Test dragging contenteditable to contenteditable + + shouldClear = false; + + selection.selectAllChildren(contenteditable.childNodes[4]); + synthesizeDrop(contenteditable.childNodes[4], contenteditable, [], "copy"); + is(contenteditable.childNodes.length, 7, "Move text/html and text/plain from contenteditable onto itself child nodes"); + is(contenteditable.childNodes[6].tagName, "I", "Move text/html and text/plain from contenteditable onto itself italic"); + is(contenteditable.childNodes[6].textContent, "Italic", "Move text/html and text/plain from contenteditable onto itself text"); + + // We'd test 'move' here as well as 'copy', but that requires knowledge of + // the source of the drag which drag simulation doesn't provide. + + // -------- Test dragging non-editable nested inside contenteditable to contenteditable + + input.focus(); // this resets some state in the selection otherwise an inexplicable error occurs calling selectAllChildren. + input.blur(); + + var nonEditable = document.getElementById("noneditable"); + selection.selectAllChildren(nonEditable); + synthesizeDrop(nonEditable, document.getElementById("first"), [], "copy"); + is(document.getElementById("nestedce").textContent, " MiddleFirst letter Middle Last part", + "Drag non-editable text/html onto contenteditable text"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTest); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_htmleditor_keyevent_handling.html b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html new file mode 100644 index 000000000..bfec290a5 --- /dev/null +++ b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html @@ -0,0 +1,664 @@ +<html> +<head> + <title>Test for key event handler of HTML editor</title> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <div id="htmlEditor" contenteditable="true"><br></div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +var htmlEditor = document.getElementById("htmlEditor"); + +const kIsMac = navigator.platform.indexOf("Mac") == 0; +const kIsWin = navigator.platform.indexOf("Win") == 0; +const kIsLinux = navigator.platform.indexOf("Linux") == 0 || navigator.platform.indexOf("SunOS") == 0 ; + +function runTests() +{ + document.execCommand("stylewithcss", false, "true"); + + var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]. + getService(SpecialPowers.Ci.nsIFocusManager); + + var capturingPhase = { fired: false, prevented: false }; + var bubblingPhase = { fired: false, prevented: false }; + + var listener = { + handleEvent: function _hv(aEvent) + { + is(aEvent.type, "keypress", "unexpected event is handled"); + switch (aEvent.eventPhase) { + case aEvent.CAPTURING_PHASE: + capturingPhase.fired = true; + capturingPhase.prevented = aEvent.defaultPrevented; + break; + case aEvent.BUBBLING_PHASE: + bubblingPhase.fired = true; + bubblingPhase.prevented = aEvent.defaultPrevented; + aEvent.preventDefault(); // prevent the browser default behavior + break; + default: + ok(false, "event is handled in unexpected phase"); + } + } + }; + + function check(aDescription, + aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling) + { + function getDesciption(aExpected) + { + return aDescription + (aExpected ? " wasn't " : " was "); + } + is(capturingPhase.fired, aFiredOnCapture, + getDesciption(aFiredOnCapture) + "fired on capture phase"); + is(bubblingPhase.fired, aFiredOnBubbling, + getDesciption(aFiredOnBubbling) + "fired on bubbling phase"); + + // If the event is fired on bubbling phase and it was already prevented + // on capture phase, it must be prevented on bubbling phase too. + if (capturingPhase.prevented) { + todo(false, aDescription + + " was consumed already, so, we cannot test the editor behavior actually"); + aPreventedOnBubbling = true; + } + + is(bubblingPhase.prevented, aPreventedOnBubbling, + getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase"); + } + + SpecialPowers.addSystemEventListener(window, "keypress", listener, true); + SpecialPowers.addSystemEventListener(window, "keypress", listener, false); + + function doTest(aElement, aDescription, + aIsReadonly, aIsTabbable, aIsPlaintext) + { + function reset(aText) + { + capturingPhase.fired = false; + capturingPhase.prevented = false; + bubblingPhase.fired = false; + bubblingPhase.prevented = false; + aElement.innerHTML = aText; + var sel = window.getSelection(); + var range = document.createRange(); + range.setStart(aElement, aElement.childNodes.length); + sel.removeAllRanges(); + sel.addRange(range); + } + + function resetForIndent(aText) + { + capturingPhase.fired = false; + capturingPhase.prevented = false; + bubblingPhase.fired = false; + bubblingPhase.prevented = false; + aElement.innerHTML = aText; + var sel = window.getSelection(); + var range = document.createRange(); + var target = document.getElementById("target").firstChild; + range.setStart(target, target.length); + sel.removeAllRanges(); + sel.addRange(range); + } + + if (document.activeElement) { + document.activeElement.blur(); + } + + aDescription += ": " + + aElement.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus"); + + // Backspace key: + // If editor is readonly, it doesn't consume. + // If editor is editable, it consumes backspace and shift+backspace. + // Otherwise, editor doesn't consume the event. + reset(""); + synthesizeKey("VK_BACK_SPACE", { }); + check(aDescription + "Backspace", true, true, true); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { shiftKey: true }); + check(aDescription + "Shift+Backspace", true, true, true); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { ctrlKey: true }); + check(aDescription + "Ctrl+Backspace", true, true, aIsReadonly); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { altKey: true }); + check(aDescription + "Alt+Backspace", true, true, aIsReadonly || kIsMac); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { metaKey: true }); + check(aDescription + "Meta+Backspace", true, true, aIsReadonly); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { osKey: true }); + check(aDescription + "OS+Backspace", true, true, aIsReadonly); + + // Delete key: + // If editor is readonly, it doesn't consume. + // If editor is editable, delete is consumed. + // Otherwise, editor doesn't consume the event. + reset(""); + synthesizeKey("VK_DELETE", { }); + check(aDescription + "Delete", true, true, !aIsReadonly || kIsMac); + + reset(""); + synthesizeKey("VK_DELETE", { shiftKey: true }); + check(aDescription + "Shift+Delete", true, true, kIsMac); + + reset(""); + synthesizeKey("VK_DELETE", { ctrlKey: true }); + check(aDescription + "Ctrl+Delete", true, true, false); + + reset(""); + synthesizeKey("VK_DELETE", { altKey: true }); + check(aDescription + "Alt+Delete", true, true, kIsMac); + + reset(""); + synthesizeKey("VK_DELETE", { metaKey: true }); + check(aDescription + "Meta+Delete", true, true, false); + + reset(""); + synthesizeKey("VK_DELETE", { osKey: true }); + check(aDescription + "OS+Delete", true, true, false); + + // Return key: + // If editor is readonly, it doesn't consume. + // If editor is editable and not single line editor, it consumes Return + // and Shift+Return. + // Otherwise, editor doesn't consume the event. + reset("a"); + synthesizeKey("VK_RETURN", { }); + check(aDescription + "Return", + true, true, !aIsReadonly); + is(aElement.innerHTML, aIsReadonly ? "a" : "a<br><br>", + aDescription + "Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { shiftKey: true }); + check(aDescription + "Shift+Return", + true, true, !aIsReadonly); + is(aElement.innerHTML, aIsReadonly ? "a" : "a<br><br>", + aDescription + "Shift+Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { ctrlKey: true }); + check(aDescription + "Ctrl+Return", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Ctrl+Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { altKey: true }); + check(aDescription + "Alt+Return", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Alt+Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { metaKey: true }); + check(aDescription + "Meta+Return", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Meta+Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { osKey: true }); + check(aDescription + "OS+Return", true, true, false); + is(aElement.innerHTML, "a", aDescription + "OS+Return"); + + // Tab key: + // If editor is tabbable, editor doesn't consume all tab key events. + // Otherwise, editor consumes tab key event without any modifier keys. + reset("a"); + synthesizeKey("VK_TAB", { }); + check(aDescription + "Tab", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? "a" : + aIsPlaintext ? "a\t" : "a <br>", + aDescription + "Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab)"); + + reset("a"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Shift+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + reset("a"); + synthesizeKey("VK_TAB", { ctrlKey: true }); + check(aDescription + "Ctrl+Tab", false, false, false); + is(aElement.innerHTML, "a", aDescription + "Ctrl+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab)"); + + reset("a"); + synthesizeKey("VK_TAB", { altKey: true }); + check(aDescription + "Alt+Tab", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Alt+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab)"); + + reset("a"); + synthesizeKey("VK_TAB", { metaKey: true }); + check(aDescription + "Meta+Tab", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Meta+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab)"); + + reset("a"); + synthesizeKey("VK_TAB", { osKey: true }); + check(aDescription + "OS+Tab", true, true, false); + is(aElement.innerHTML, "a", aDescription + "OS+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (OS+Tab)"); + + // Indent/Outdent tests: + // UL + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("VK_TAB", { }); + check(aDescription + "Tab on UL", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable ? + "<ul><li id=\"target\">ul list item</li></ul>" : + aIsPlaintext ? "<ul><li id=\"target\">ul list item\t</li></ul>" : + "<ul><ul><li id=\"target\">ul list item</li></ul></ul>", + aDescription + "Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on UL)"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab after Tab on UL", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable || (!aIsPlaintext) ? + "<ul><li id=\"target\">ul list item</li></ul>" : + "<ul><li id=\"target\">ul list item\t</li></ul>", + aDescription + "Shift+Tab after Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab after Tab on UL)"); + + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab on UL", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable || aIsPlaintext ? + "<ul><li id=\"target\">ul list item</li></ul>" : "ul list item", + aDescription + "Shift+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab on UL)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("VK_TAB", { ctrlKey: true }); + check(aDescription + "Ctrl+Tab on UL", false, false, false); + is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>", + aDescription + "Ctrl+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab on UL)"); + + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("VK_TAB", { altKey: true }); + check(aDescription + "Alt+Tab on UL", true, true, false); + is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>", + aDescription + "Alt+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab on UL)"); + + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("VK_TAB", { metaKey: true }); + check(aDescription + "Meta+Tab on UL", true, true, false); + is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>", + aDescription + "Meta+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab on UL)"); + + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("VK_TAB", { osKey: true }); + check(aDescription + "OS+Tab on UL", true, true, false); + is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>", + aDescription + "OS+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (OS+Tab on UL)"); + + // OL + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("VK_TAB", { }); + check(aDescription + "Tab on OL", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable ? + "<ol><li id=\"target\">ol list item</li></ol>" : + aIsPlaintext ? "<ol><li id=\"target\">ol list item\t</li></ol>" : + "<ol><ol><li id=\"target\">ol list item</li></ol></ol>", + aDescription + "Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on OL)"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab after Tab on OL", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable || (!aIsPlaintext) ? + "<ol><li id=\"target\">ol list item</li></ol>" : + "<ol><li id=\"target\">ol list item\t</li></ol>", + aDescription + "Shift+Tab after Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab after Tab on OL)"); + + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab on OL", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable || aIsPlaintext ? + "<ol><li id=\"target\">ol list item</li></ol>" : "ol list item", + aDescription + "Shfit+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab on OL)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("VK_TAB", { ctrlKey: true }); + check(aDescription + "Ctrl+Tab on OL", false, false, false); + is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>", + aDescription + "Ctrl+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab on OL)"); + + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("VK_TAB", { altKey: true }); + check(aDescription + "Alt+Tab on OL", true, true, false); + is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>", + aDescription + "Alt+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab on OL)"); + + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("VK_TAB", { metaKey: true }); + check(aDescription + "Meta+Tab on OL", true, true, false); + is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>", + aDescription + "Meta+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab on OL)"); + + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("VK_TAB", { osKey: true }); + check(aDescription + "OS+Tab on OL", true, true, false); + is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>", + aDescription + "OS+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (OS+Tab on OL)"); + + // TD + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("VK_TAB", { }); + check(aDescription + "Tab on TD", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>" : + aIsPlaintext ? "<table><tbody><tr><td id=\"target\">td\t</td></tr></tbody></table>" : + "<table><tbody><tr><td id=\"target\">td</td></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>", + aDescription + "Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on TD)"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab after Tab on TD", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>" : + aIsPlaintext ? "<table><tbody><tr><td id=\"target\">td\t</td></tr></tbody></table>" : + "<table><tbody><tr><td id=\"target\">td</td></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>", + aDescription + "Shift+Tab after Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab after Tab on TD)"); + + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab on TD", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "Shift+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab on TD)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("VK_TAB", { ctrlKey: true }); + check(aDescription + "Ctrl+Tab on TD", false, false, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "Ctrl+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab on TD)"); + + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("VK_TAB", { altKey: true }); + check(aDescription + "Alt+Tab on TD", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "Alt+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab on TD)"); + + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("VK_TAB", { metaKey: true }); + check(aDescription + "Meta+Tab on TD", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "Meta+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab on TD)"); + + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("VK_TAB", { osKey: true }); + check(aDescription + "OS+Tab on TD", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "OS+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (OS+Tab on TD)"); + + // TH + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("VK_TAB", { }); + check(aDescription + "Tab on TH", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>" : + aIsPlaintext ? "<table><tbody><tr><th id=\"target\">th\t</th></tr></tbody></table>" : + "<table><tbody><tr><th id=\"target\">th</th></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>", + aDescription + "Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on TH)"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab after Tab on TH", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>" : + aIsPlaintext ? "<table><tbody><tr><th id=\"target\">th\t</th></tr></tbody></table>" : + "<table><tbody><tr><th id=\"target\">th</th></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>", + aDescription + "Shift+Tab after Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab after Tab on TH)"); + + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab on TH", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "Shift+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab on TH)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("VK_TAB", { ctrlKey: true }); + check(aDescription + "Ctrl+Tab on TH", false, false, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "Ctrl+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab on TH)"); + + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("VK_TAB", { altKey: true }); + check(aDescription + "Alt+Tab on TH", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "Alt+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab on TH)"); + + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("VK_TAB", { metaKey: true }); + check(aDescription + "Meta+Tab on TH", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "Meta+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab on TH)"); + + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("VK_TAB", { osKey: true }); + check(aDescription + "OS+Tab on TH", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "OS+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (OS+Tab on TH)"); + + // Esc key: + // In all cases, esc key events are not consumed + reset("abc"); + synthesizeKey("VK_ESCAPE", { }); + check(aDescription + "Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { shiftKey: true }); + check(aDescription + "Shift+Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { ctrlKey: true }); + check(aDescription + "Ctrl+Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { altKey: true }); + check(aDescription + "Alt+Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { metaKey: true }); + check(aDescription + "Meta+Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { osKey: true }); + check(aDescription + "OS+Esc", true, true, false); + + // typical typing tests: + reset(""); + synthesizeKey("M", { shiftKey: true }); + check(aDescription + "M", true, true, !aIsReadonly); + synthesizeKey("o", { }); + check(aDescription + "o", true, true, !aIsReadonly); + synthesizeKey("z", { }); + check(aDescription + "z", true, true, !aIsReadonly); + synthesizeKey("i", { }); + check(aDescription + "i", true, true, !aIsReadonly); + synthesizeKey("l", { }); + check(aDescription + "l", true, true, !aIsReadonly); + synthesizeKey("l", { }); + check(aDescription + "l", true, true, !aIsReadonly); + synthesizeKey("a", { }); + check(aDescription + "a", true, true, !aIsReadonly); + synthesizeKey(" ", { }); + check(aDescription + "' '", true, true, !aIsReadonly); + is(aElement.innerHTML, + aIsReadonly ? "" : aIsPlaintext ? "Mozilla " : "Mozilla <br>", + aDescription + "typed \"Mozilla \""); + } + + doTest(htmlEditor, "contenteditable=\"true\"", false, true, false); + + const nsIPlaintextEditor = SpecialPowers.Ci.nsIPlaintextEditor; + var editor = SpecialPowers.wrap(window). + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsIWebNavigation). + QueryInterface(SpecialPowers.Ci.nsIDocShell).editor; + var flags = editor.flags; + // readonly + editor.flags = flags | nsIPlaintextEditor.eEditorReadonlyMask; + doTest(htmlEditor, "readonly HTML editor", true, true, false); + + // non-tabbable + editor.flags = flags & ~(nsIPlaintextEditor.eEditorAllowInteraction); + doTest(htmlEditor, "non-tabbable HTML editor", false, false, false); + + // readonly and non-tabbable + editor.flags = + (flags | nsIPlaintextEditor.eEditorReadonlyMask) & + ~(nsIPlaintextEditor.eEditorAllowInteraction); + doTest(htmlEditor, "readonly and non-tabbable HTML editor", + true, false, false); + + // plaintext + editor.flags = flags | nsIPlaintextEditor.eEditorPlaintextMask; + doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true); + + // plaintext and non-tabbable + editor.flags = (flags | nsIPlaintextEditor.eEditorPlaintextMask) & + ~(nsIPlaintextEditor.eEditorAllowInteraction); + doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode", + false, false, true); + + + // readonly and plaintext + editor.flags = flags | nsIPlaintextEditor.eEditorPlaintextMask | + nsIPlaintextEditor.eEditorReadonlyMask; + doTest(htmlEditor, "readonly HTML editor but plaintext mode", + true, true, true); + + // readonly, plaintext and non-tabbable + editor.flags = (flags | nsIPlaintextEditor.eEditorPlaintextMask | + nsIPlaintextEditor.eEditorReadonlyMask) & + ~(nsIPlaintextEditor.eEditorAllowInteraction); + doTest(htmlEditor, "readonly and non-tabbable HTML editor but plaintext mode", + true, false, true); + + SpecialPowers.removeSystemEventListener(window, "keypress", listener, true); + SpecialPowers.removeSystemEventListener(window, "keypress", listener, false); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_keypress_untrusted_event.html b/editor/libeditor/tests/test_keypress_untrusted_event.html new file mode 100644 index 000000000..6875c5a33 --- /dev/null +++ b/editor/libeditor/tests/test_keypress_untrusted_event.html @@ -0,0 +1,99 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=622245 +--> +<head> + <title>Test for untrusted keypress events</title> + <script type="application/javascript" src="/MochiKit/packed.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622245">Mozilla Bug 622245</a> +<p id="display"></p> +<div id="content"> +<input id="i"><br> +<textarea id="t"></textarea><br> +<div id="d" contenteditable style="min-height: 1em;"></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 674770 **/ +SimpleTest.waitForExplicitFinish(); + +var input = document.getElementById("i"); +var textarea = document.getElementById("t"); +var div = document.getElementById("d"); + +addLoadEvent(function() { + input.focus(); + + SimpleTest.executeSoon(function() { + input.addEventListener("keypress", + function(aEvent) { + input.removeEventListener("keypress", arguments.callee, false); + is(aEvent.target, input, + "The keypress event target isn't the input element"); + + SimpleTest.executeSoon(function() { + is(input.value, "", + "Did keypress event cause modifying the input element?"); + textarea.focus(); + SimpleTest.executeSoon(runTextareaTest); + }); + }, false); + var keypress = document.createEvent("KeyboardEvent"); + keypress.initKeyEvent("keypress", true, true, document.defaultView, + false, false, false, false, 0, "a".charCodeAt(0)); + input.dispatchEvent(keypress); + }); +}); + +function runTextareaTest() +{ + textarea.addEventListener("keypress", + function(aEvent) { + textarea.removeEventListener("keypress", arguments.callee, false); + is(aEvent.target, textarea, + "The keypress event target isn't the textarea element"); + + SimpleTest.executeSoon(function() { + is(textarea.value, "", + "Did keypress event cause modifying the textarea element?"); + div.focus(); + SimpleTest.executeSoon(runContentediableTest); + }); + }, false); + var keypress = document.createEvent("KeyboardEvent"); + keypress.initKeyEvent("keypress", true, true, document.defaultView, + false, false, false, false, 0, "b".charCodeAt(0)); + textarea.dispatchEvent(keypress); +} + +function runContentediableTest() +{ + div.addEventListener("keypress", + function(aEvent) { + div.removeEventListener("keypress", arguments.callee, false); + is(aEvent.target, div, + "The keypress event target isn't the div element"); + + SimpleTest.executeSoon(function() { + is(div.innerHTML, "", + "Did keypress event cause modifying the div element?"); + + SimpleTest.finish(); + }); + }, false); + var keypress = document.createEvent("KeyboardEvent"); + keypress.initKeyEvent("keypress", true, true, document.defaultView, + false, false, false, false, 0, "c".charCodeAt(0)); + div.dispatchEvent(keypress); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_root_element_replacement.html b/editor/libeditor/tests/test_root_element_replacement.html new file mode 100644 index 000000000..f8b6f4336 --- /dev/null +++ b/editor/libeditor/tests/test_root_element_replacement.html @@ -0,0 +1,148 @@ +<html> +<head> + <title>Test for root element replacement</title> + <script type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" + src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +function runDesignModeTest(aDoc, aFocus, aNewSource) +{ + aDoc.designMode = "on"; + + if (aFocus) { + aDoc.documentElement.focus(); + } + + aDoc.open(); + aDoc.write(aNewSource); + aDoc.close(); + aDoc.documentElement.focus(); +} + +function runContentEditableTest(aDoc, aFocus, aNewSource) +{ + if (aFocus) { + aDoc.body.setAttribute("contenteditable", "true"); + aDoc.body.focus(); + } + + aDoc.open(); + aDoc.write(aNewSource); + aDoc.close(); + aDoc.getElementById("focus").focus(); +} + +var gTestIndex = 0; + +const kTests = [ + { description: "Replace to '<body></body>', designMode", + initializer: runDesignModeTest, + args: [ "<body></body>" ] }, + { description: "Replace to '<html><body></body></html>', designMode", + initializer: runDesignModeTest, + args: [ "<html><body></body></html>" ] }, + { description: "Replace to '<html> <body></body></html>', designMode", + initializer: runDesignModeTest, + args: [ "<html> <body></body></html>" ] }, + { description: "Replace to ' <html> <body></body></html>', designMode", + initializer: runDesignModeTest, + args: [ " <html> <body></body></html>" ] }, + + { description: "Replace to '<html contenteditable='true'><body></body></html>", + initializer: runContentEditableTest, + args: [ "<html contenteditable='true' id='focus'><body></body></html>" ] }, + { description: "Replace to '<html><body contenteditable='true'></body></html>", + initializer: runContentEditableTest, + args: [ "<html><body contenteditable='true' id='focus'></body></html>" ] }, + { description: "Replace to '<body contenteditable='true'></body>", + initializer: runContentEditableTest, + args: [ "<body contenteditable='true' id='focus'></body>" ] }, +]; + +var gIFrame; +var gSetFocusToIFrame = false; + +function onLoadIFrame() +{ + var frameDoc = gIFrame.contentWindow.document; + + var selCon = SpecialPowers.wrap(gIFrame).contentWindow. + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsIWebNavigation). + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsISelectionDisplay). + QueryInterface(SpecialPowers.Ci.nsISelectionController); + var utils = SpecialPowers.getDOMWindowUtils(window); + const nsIDOMNode = SpecialPowers.Ci.nsIDOMNode; + + // move focus to the HTML editor + const kTest = kTests[gTestIndex]; + ok(true, "Running " + kTest.description); + if (kTest.args.length == 1) { + kTest.initializer(frameDoc, gSetFocusToIFrame, kTest.args[0]); + ok(selCon.caretVisible, "caret isn't visible -- " + kTest.description); + } else { + ok(false, "kTests is broken at index=" + gTestIndex); + } + + is(utils.IMEStatus, utils.IME_STATUS_ENABLED, + "IME isn't enabled -- " + kTest.description); + synthesizeKey("A", { }, gIFrame.contentWindow); + synthesizeKey("B", { }, gIFrame.contentWindow); + synthesizeKey("C", { }, gIFrame.contentWindow); + var content = frameDoc.body.firstChild; + ok(content, "body doesn't have contents -- " + kTest.description); + if (content) { + is(content.nodeType, nsIDOMNode.TEXT_NODE, + "the content of body isn't text node -- " + kTest.description); + if (content.nodeType == nsIDOMNode.TEXT_NODE) { + is(content.data, "ABC", + "the content of body text isn't 'ABC' -- " + kTest.description); + is(frameDoc.body.innerHTML, "ABC", + "the innerHTML of body isn't 'ABC' -- " + kTest.description); + } + } + + document.getElementById("display").removeChild(gIFrame); + + // Do next test or finish the tests. + if (++gTestIndex < kTests.length) { + setTimeout(runTest, 0); + } else if (!gSetFocusToIFrame) { + gSetFocusToIFrame = true; + gTestIndex = 0; + setTimeout(runTest, 0); + } else { + SimpleTest.finish(); + } +} + +function runTest() +{ + gIFrame = document.createElement("iframe"); + document.getElementById("display").appendChild(gIFrame); + gIFrame.src = "about:blank"; + gIFrame.onload = onLoadIFrame; +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_select_all_without_body.html b/editor/libeditor/tests/test_select_all_without_body.html new file mode 100644 index 000000000..d947400c4 --- /dev/null +++ b/editor/libeditor/tests/test_select_all_without_body.html @@ -0,0 +1,27 @@ +<html> +<head> + <title>Test select all in HTML editor without body element</title> + <script type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +window.open("file_select_all_without_body.html", "_blank", + "width=600,height=600"); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_selection_move_commands.html b/editor/libeditor/tests/test_selection_move_commands.html new file mode 100644 index 000000000..e217f8fdf --- /dev/null +++ b/editor/libeditor/tests/test_selection_move_commands.html @@ -0,0 +1,219 @@ +<!doctype html> +<title>Test for nsSelectionMoveCommands</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/SpawnTask.js"></script> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=454004">Mozilla Bug 454004</a> + +<iframe id="edit" width="200" height="100" src="about:blank"></iframe> + +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("Legacy test, possibly no good reason"); + +var winUtils = SpecialPowers.getDOMWindowUtils(window); + +function* setup() { + yield SpecialPowers.pushPrefEnv({set: [["general.smoothScroll", false]]}); + winUtils.advanceTimeAndRefresh(100); +} + +function* runTests() { + var e = document.getElementById("edit"); + var doc = e.contentDocument; + var win = e.contentWindow; + var root = doc.documentElement; + var body = doc.body; + + body.style.fontSize='16px'; + body.style.lineHeight='16px'; + body.style.height='400px'; + body.style.padding='0px'; + body.style.margin='0px'; + body.style.borderWidth='0px'; + + var sel = win.getSelection(); + doc.designMode='on'; + body.innerHTML = "1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>10<br>11<br>12<br>"; + win.focus(); + // Flush out layout to make sure that the subdocument will be the size we + // expect by the time we try to scroll it. + is(body.getBoundingClientRect().height, 400, + "Body height should be what we set it to"); + yield; + + function testScrollCommand(cmd, expectTop) { + // http://dev.w3.org/csswg/cssom-view/#dom-element-getboundingclientrect + // doesn't explicitly rule out -0 here, but for now assume that only + // positive zeroes are permitted. + if (navigator.appVersion.indexOf("Android") != -1 && expectTop != 0) { + // Android doesn't get the values exactly correct for some reason + todo_is(root.getBoundingClientRect().top, -expectTop + 0, cmd); + ok(Math.abs(root.getBoundingClientRect().top + expectTop) < 0.2, + cmd + " (approximately)"); + } else { + is(root.getBoundingClientRect().top, -expectTop + 0, cmd); + } + } + + function testMoveCommand(cmd, expectNode, expectOffset) { + SpecialPowers.doCommand(window, cmd); + is(sel.isCollapsed, true, "collapsed after " + cmd); + is(sel.anchorNode, expectNode, "node after " + cmd); + is(sel.anchorOffset, expectOffset, "offset after " + cmd); + } + + function findChildNum(e, child) { + var i = 0; + var n = e.firstChild; + while (n && n != child) { + n = n.nextSibling; + ++i; + } + if (!n) + return -1; + return i; + } + + function testPageMoveCommand(cmd, expectOffset) { + SpecialPowers.doCommand(window, cmd); + is(sel.isCollapsed, true, "collapsed after " + cmd); + is(sel.anchorOffset, expectOffset, "offset after " + cmd); + return findChildNum(body, sel.anchorNode); + } + + function testSelectCommand(cmd, expectNode, expectOffset) { + var anchorNode = sel.anchorNode; + var anchorOffset = sel.anchorOffset; + SpecialPowers.doCommand(window, cmd); + is(sel.isCollapsed, false, "not collapsed after " + cmd); + is(sel.anchorNode, anchorNode, "anchor not moved after " + cmd); + is(sel.anchorOffset, anchorOffset, "anchor not moved after " + cmd); + is(sel.focusNode, expectNode, "node after " + cmd); + is(sel.focusOffset, expectOffset, "offset after " + cmd); + } + + function testPageSelectCommand(cmd, expectOffset) { + var anchorNode = sel.anchorNode; + var anchorOffset = sel.anchorOffset; + SpecialPowers.doCommand(window, cmd); + is(sel.isCollapsed, false, "not collapsed after " + cmd); + is(sel.anchorNode, anchorNode, "anchor not moved after " + cmd); + is(sel.anchorOffset, anchorOffset, "anchor not moved after " + cmd); + is(sel.focusOffset, expectOffset, "offset after " + cmd); + return findChildNum(body, sel.focusNode); + } + + function node(i) { + var n = body.firstChild; + while (i > 0) { + n = n.nextSibling; + --i; + } + return n; + } + + SpecialPowers.doCommand(window, "cmd_scrollBottom"); + yield; + testScrollCommand("cmd_scrollBottom", root.scrollHeight - 100); + SpecialPowers.doCommand(window, "cmd_scrollTop"); + yield; + testScrollCommand("cmd_scrollTop", 0); + + SpecialPowers.doCommand(window, "cmd_scrollPageDown"); + yield; + var pageHeight = -root.getBoundingClientRect().top; + ok(pageHeight > 0, "cmd_scrollPageDown works"); + ok(pageHeight <= 100, "cmd_scrollPageDown doesn't scroll too much"); + SpecialPowers.doCommand(window, "cmd_scrollBottom"); + SpecialPowers.doCommand(window, "cmd_scrollPageUp"); + yield; + testScrollCommand("cmd_scrollPageUp", root.scrollHeight - 100 - pageHeight); + + SpecialPowers.doCommand(window, "cmd_scrollTop"); + SpecialPowers.doCommand(window, "cmd_scrollLineDown"); + yield; + var lineHeight = -root.getBoundingClientRect().top; + ok(lineHeight > 0, "Can scroll by lines"); + SpecialPowers.doCommand(window, "cmd_scrollBottom"); + SpecialPowers.doCommand(window, "cmd_scrollLineUp"); + yield; + testScrollCommand("cmd_scrollLineUp", root.scrollHeight - 100 - lineHeight); + + var runSelectionTests = function(selectWordNextNode, selectWordNextOffset) { + testMoveCommand("cmd_moveBottom", body, 23); + testMoveCommand("cmd_moveTop", node(0), 0); + testSelectCommand("cmd_selectBottom", body, 23); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testSelectCommand("cmd_selectTop", node(0), 0); + + SpecialPowers.doCommand(window, "cmd_moveTop"); + testMoveCommand("cmd_lineNext", node(2), 0); + testMoveCommand("cmd_linePrevious", node(0), 0); + testSelectCommand("cmd_selectLineNext", node(2), 0); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testSelectCommand("cmd_selectLinePrevious", node(20), 2); + + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testMoveCommand("cmd_charPrevious", node(22), 1); + testMoveCommand("cmd_charNext", node(22), 2); + testSelectCommand("cmd_selectCharPrevious", node(22), 1); + SpecialPowers.doCommand(window, "cmd_moveTop"); + testSelectCommand("cmd_selectCharNext", node(0), 1); + + SpecialPowers.doCommand(window, "cmd_moveTop"); + testMoveCommand("cmd_endLine", node(0), 1); + testMoveCommand("cmd_beginLine", node(0), 0); + testSelectCommand("cmd_selectEndLine", node(0), 1); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testSelectCommand("cmd_selectBeginLine", node(22), 0); + + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testMoveCommand("cmd_wordPrevious", node(22), 0); + testMoveCommand("cmd_wordNext", body, 23); + testSelectCommand("cmd_selectWordPrevious", node(22), 0); + SpecialPowers.doCommand(window, "cmd_moveTop"); + testSelectCommand("cmd_selectWordNext", selectWordNextNode, selectWordNextOffset); + + SpecialPowers.doCommand(window, "cmd_moveTop"); + var lineNum = testPageMoveCommand("cmd_movePageDown", 0); + ok(lineNum > 0, "cmd_movePageDown works"); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + SpecialPowers.doCommand(window, "cmd_beginLine"); + is(testPageMoveCommand("cmd_movePageUp", 0), 22 - lineNum, "cmd_movePageUp"); + + SpecialPowers.doCommand(window, "cmd_moveTop"); + is(testPageSelectCommand("cmd_selectPageDown", 0), lineNum, "cmd_selectPageDown"); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + SpecialPowers.doCommand(window, "cmd_beginLine"); + is(testPageSelectCommand("cmd_selectPageUp", 0), 22 - lineNum, "cmd_selectPageUp"); + } + + yield SpecialPowers.pushPrefEnv({set: [["layout.word_select.eat_space_to_next_word", false]]}); + runSelectionTests(body, 1); + yield SpecialPowers.pushPrefEnv({set: [["layout.word_select.eat_space_to_next_word", true]]}); + runSelectionTests(node(2), 0); +} + +function cleanup() { + winUtils.restoreNormalRefresh(); + SimpleTest.finish(); +} + +function* testRunner() { + let curTest = runTests(); + while (true) { + winUtils.advanceTimeAndRefresh(100); + if (curTest.next().done) { + break; + } + winUtils.advanceTimeAndRefresh(100); + yield new Promise(resolve => setTimeout(resolve, 20)); + } +} + +spawn_task(setup) + .then(() => spawn_task(testRunner)) + .then(() => spawn_task(cleanup)) + .catch(err => ok(false, err)); +</script> diff --git a/editor/libeditor/tests/test_set_document_title_transaction.html b/editor/libeditor/tests/test_set_document_title_transaction.html new file mode 100644 index 000000000..d745d4f13 --- /dev/null +++ b/editor/libeditor/tests/test_set_document_title_transaction.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for SetDocumentTitleTransaction</title> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body onload="runTests()"> +<div id="display"> + <iframe src="data:text/html,<!DOCTYPE html><html><head><title>first title</title></head><body></body></html>"></iframe> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +function runTests() { + var iframe = document.getElementsByTagName("iframe")[0]; + function isDocumentTitleEquals(aDescription, aExpectedTitle) { + is(iframe.contentDocument.title, aExpectedTitle, aDescription + ": document.title should be " + aExpectedTitle); + is(iframe.contentDocument.getElementsByTagName("title")[0].textContent, aExpectedTitle, aDescription + ": The text in the title element should be " + aExpectedTitle); + } + + isDocumentTitleEquals("Checking isDocumentTitleEquals()", "first title"); + + const kTests = [ + { description: "designMode=\"on\"", + init: function () { + iframe.contentDocument.designMode = "on"; + }, + cleanUp: function () { + iframe.contentDocument.designMode = "off"; + } + }, + { description: "html element has contenteditable attribute", + init: function () { + iframe.contentDocument.documentElement.setAttribute("contenteditable", "true"); + }, + cleanUp: function () { + iframe.contentDocument.documentElement.removeAttribute("contenteditable"); + } + }, + ]; + + for (var i = 0; i < kTests.length; i++) { + const kTest = kTests[i]; + kTest.init(); + + var editor = SpecialPowers.wrap(iframe.contentWindow). + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsIWebNavigation). + QueryInterface(SpecialPowers.Ci.nsIDocShell).editor; + ok(editor, kTest.description + ": The docshell should have editor"); + var htmlEditor = editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor); + ok(htmlEditor, kTest.description + ": The editor should have nsIHTMLEditor interface"); + + // Replace existing title. + htmlEditor.setDocumentTitle("Modified title"); + isDocumentTitleEquals(kTest.description, "Modified title"); + + // When the document doesn't have <title> element, title element should be created automatically. + iframe.contentDocument.head.removeChild(iframe.contentDocument.getElementsByTagName("title")[0]); + is(iframe.contentDocument.getElementsByTagName("title").length, 0, kTest.description + ": There should be no title element"); + htmlEditor.setDocumentTitle("new title"); + is(iframe.contentDocument.getElementsByTagName("title").length, 1, kTest.description + ": There should be a title element"); + isDocumentTitleEquals(kTest.description, "new title"); + + kTest.cleanUp(); + } + + SimpleTest.finish(); +} +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_spellcheck_pref.html b/editor/libeditor/tests/test_spellcheck_pref.html new file mode 100644 index 000000000..9faff45f3 --- /dev/null +++ b/editor/libeditor/tests/test_spellcheck_pref.html @@ -0,0 +1,23 @@ +<html> +<head> + <title>Test if spellcheck is turned on</title> + <script type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + + is(SpecialPowers.getIntPref("layout.spellcheckDefault"), 1, "Check if the layout.spellcheckDefault pref is turned on"); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_texteditor_keyevent_handling.html b/editor/libeditor/tests/test_texteditor_keyevent_handling.html new file mode 100644 index 000000000..5c4a8d1c2 --- /dev/null +++ b/editor/libeditor/tests/test_texteditor_keyevent_handling.html @@ -0,0 +1,386 @@ +<html> +<head> + <title>Test for key event handler of text editor</title> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <input type="text" id="inputField"> + <input type="password" id="passwordField"> + <textarea id="textarea"></textarea> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +var inputField = document.getElementById("inputField"); +var passwordField = document.getElementById("passwordField"); +var textarea = document.getElementById("textarea"); + +const kIsMac = navigator.platform.indexOf("Mac") == 0; +const kIsWin = navigator.platform.indexOf("Win") == 0; +const kIsLinux = navigator.platform.indexOf("Linux") == 0; + +function runTests() +{ + var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]. + getService(SpecialPowers.Ci.nsIFocusManager); + + var capturingPhase = { fired: false, prevented: false }; + var bubblingPhase = { fired: false, prevented: false }; + + var listener = { + handleEvent: function _hv(aEvent) + { + is(aEvent.type, "keypress", "unexpected event is handled"); + switch (aEvent.eventPhase) { + case aEvent.CAPTURING_PHASE: + capturingPhase.fired = true; + capturingPhase.prevented = aEvent.defaultPrevented; + break; + case aEvent.BUBBLING_PHASE: + bubblingPhase.fired = true; + bubblingPhase.prevented = aEvent.defaultPrevented; + aEvent.preventDefault(); // prevent the browser default behavior + break; + default: + ok(false, "event is handled in unexpected phase"); + } + } + }; + + function check(aDescription, + aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling) + { + function getDesciption(aExpected) + { + return aDescription + (aExpected ? " wasn't " : " was "); + } + + is(capturingPhase.fired, aFiredOnCapture, + getDesciption(aFiredOnCapture) + "fired on capture phase"); + is(bubblingPhase.fired, aFiredOnBubbling, + getDesciption(aFiredOnBubbling) + "fired on bubbling phase"); + + // If the event is fired on bubbling phase and it was already prevented + // on capture phase, it must be prevented on bubbling phase too. + if (capturingPhase.prevented) { + todo(false, aDescription + + " was consumed already, so, we cannot test the editor behavior actually"); + aPreventedOnBubbling = true; + } + + is(bubblingPhase.prevented, aPreventedOnBubbling, + getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase"); + } + + var parentElement = document.getElementById("display"); + SpecialPowers.addSystemEventListener(parentElement, "keypress", listener, + true); + SpecialPowers.addSystemEventListener(parentElement, "keypress", listener, + false); + + function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly, + aIsTabbable) + { + function reset(aText) + { + capturingPhase.fired = false; + capturingPhase.prevented = false; + bubblingPhase.fired = false; + bubblingPhase.prevented = false; + aElement.value = aText; + } + + if (document.activeElement) { + document.activeElement.blur(); + } + + aDescription += ": " + + aElement.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus"); + + // Backspace key: + // If editor is readonly, it doesn't consume. + // If editor is editable, it consumes backspace and shift+backspace. + // Otherwise, editor doesn't consume the event but the native key + // bindings on nsTextControlFrame may consume it. + reset(""); + synthesizeKey("VK_BACK_SPACE", { }); + check(aDescription + "Backspace", true, true, true); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { shiftKey: true }); + check(aDescription + "Shift+Backspace", true, true, true); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { ctrlKey: true }); + // Win: cmd_deleteWordBackward + check(aDescription + "Ctrl+Backspace", + true, true, aIsReadonly || kIsWin); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { altKey: true }); + // Win: cmd_undo + // Mac: cmd_deleteWordBackward + check(aDescription + "Alt+Backspace", + true, true, aIsReadonly || kIsWin || kIsMac); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { metaKey: true }); + check(aDescription + "Meta+Backspace", true, true, aIsReadonly); + + reset(""); + synthesizeKey("VK_BACK_SPACE", { osKey: true }); + check(aDescription + "OS+Backspace", true, true, aIsReadonly); + + // Delete key: + // If editor is readonly, it doesn't consume. + // If editor is editable, delete is consumed. + // Otherwise, editor doesn't consume the event but the native key + // bindings on nsTextControlFrame may consume it. + reset(""); + synthesizeKey("VK_DELETE", { }); + // Linux: native handler + // Mac: cmd_deleteCharForward + check(aDescription + "Delete", + true, true, !aIsReadonly || kIsLinux || kIsMac); + + reset(""); + // Win: cmd_cutOrDelete + // Linux: cmd_cut + // Mac: cmd_deleteCharForward + synthesizeKey("VK_DELETE", { shiftKey: true }); + check(aDescription + "Shift+Delete", + true, true, true); + + reset(""); + synthesizeKey("VK_DELETE", { ctrlKey: true }); + // Win: cmd_deleteWordForward + // Linux: cmd_copy + check(aDescription + "Ctrl+Delete", + true, true, kIsWin || kIsLinux); + + reset(""); + synthesizeKey("VK_DELETE", { altKey: true }); + // Mac: cmd_deleteWordForward + check(aDescription + "Alt+Delete", + true, true, kIsMac); + + reset(""); + synthesizeKey("VK_DELETE", { metaKey: true }); + // Linux: native handler consumed. + check(aDescription + "Meta+Delete", + true, true, kIsLinux); + + reset(""); + synthesizeKey("VK_DELETE", { osKey: true }); + check(aDescription + "OS+Delete", + true, true, false); + + // XXX input.value returns "\n" when it's empty, so, we should use dummy + // value ("a") for the following tests. + + // Return key: + // If editor is readonly, it doesn't consume. + // If editor is editable and not single line editor, it consumes Return + // and Shift+Return. + // Otherwise, editor doesn't consume the event. + reset("a"); + synthesizeKey("VK_RETURN", { }); + check(aDescription + "Return", + true, true, !aIsSingleLine && !aIsReadonly); + is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a", + aDescription + "Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { shiftKey: true }); + check(aDescription + "Shift+Return", + true, true, !aIsSingleLine && !aIsReadonly); + is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a", + aDescription + "Shift+Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { ctrlKey: true }); + check(aDescription + "Ctrl+Return", true, true, false); + is(aElement.value, "a", aDescription + "Ctrl+Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { altKey: true }); + check(aDescription + "Alt+Return", true, true, false); + is(aElement.value, "a", aDescription + "Alt+Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { metaKey: true }); + check(aDescription + "Meta+Return", true, true, false); + is(aElement.value, "a", aDescription + "Meta+Return"); + + reset("a"); + synthesizeKey("VK_RETURN", { osKey: true }); + check(aDescription + "OS+Return", true, true, false); + is(aElement.value, "a", aDescription + "OS+Return"); + + // Tab key: + // If editor is tabbable, editor doesn't consume all tab key events. + // Otherwise, editor consumes tab key event without any modifier keys. + reset("a"); + synthesizeKey("VK_TAB", { }); + check(aDescription + "Tab", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.value, !aIsTabbable && !aIsReadonly ? "a\t" : "a", + aDescription + "Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab)"); + + // If the editor is not tabbable, make sure that it accepts tab characters + // even if it's empty. + if (!aIsTabbable && !aIsReadonly) { + reset(""); + synthesizeKey("VK_TAB", {}); + check(aDescription + "Tab on empty textarea", + true, true, !aIsReadonly); + is(aElement.value, "\t", aDescription + "Tab on empty textarea"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on empty textarea"); + } + + reset("a"); + synthesizeKey("VK_TAB", { shiftKey: true }); + check(aDescription + "Shift+Tab", true, true, false); + is(aElement.value, "a", aDescription + "Shift+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + reset("a"); + synthesizeKey("VK_TAB", { ctrlKey: true }); + check(aDescription + "Ctrl+Tab", false, false, false); + is(aElement.value, "a", aDescription + "Ctrl+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab)"); + + reset("a"); + synthesizeKey("VK_TAB", { altKey: true }); + check(aDescription + "Alt+Tab", true, true, false); + is(aElement.value, "a", aDescription + "Alt+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab)"); + + reset("a"); + synthesizeKey("VK_TAB", { metaKey: true }); + check(aDescription + "Meta+Tab", true, true, false); + is(aElement.value, "a", aDescription + "Meta+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab)"); + + reset("a"); + synthesizeKey("VK_TAB", { osKey: true }); + check(aDescription + "OS+Tab", true, true, false); + is(aElement.value, "a", aDescription + "OS+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (OS+Tab)"); + + // Esc key: + // In all cases, esc key events are not consumed + reset("abc"); + synthesizeKey("VK_ESCAPE", { }); + check(aDescription + "Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { shiftKey: true }); + check(aDescription + "Shift+Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { ctrlKey: true }); + check(aDescription + "Ctrl+Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { altKey: true }); + check(aDescription + "Alt+Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { metaKey: true }); + check(aDescription + "Meta+Esc", true, true, false); + + reset("abc"); + synthesizeKey("VK_ESCAPE", { osKey: true }); + check(aDescription + "OS+Esc", true, true, false); + + // typical typing tests: + reset(""); + synthesizeKey("M", { shiftKey: true }); + check(aDescription + "M", true, true, !aIsReadonly); + synthesizeKey("o", { }); + check(aDescription + "o", true, true, !aIsReadonly); + synthesizeKey("z", { }); + check(aDescription + "z", true, true, !aIsReadonly); + synthesizeKey("i", { }); + check(aDescription + "i", true, true, !aIsReadonly); + synthesizeKey("l", { }); + check(aDescription + "l", true, true, !aIsReadonly); + synthesizeKey("l", { }); + check(aDescription + "l", true, true, !aIsReadonly); + synthesizeKey("a", { }); + check(aDescription + "a", true, true, !aIsReadonly); + synthesizeKey(" ", { }); + check(aDescription + "' '", true, true, !aIsReadonly); + is(aElement.value, !aIsReadonly ? "Mozilla " : "", + aDescription + "typed \"Mozilla \""); + } + + doTest(inputField, "<input type=\"text\">", true, false, true); + + inputField.setAttribute("readonly", "readonly"); + doTest(inputField, "<input type=\"text\" readonly>", true, true, true); + + doTest(passwordField, "<input type=\"password\">", true, false, true); + + passwordField.setAttribute("readonly", "readonly"); + doTest(passwordField, "<input type=\"password\" readonly>", true, true, true); + + doTest(textarea, "<textarea>", false, false, true); + + textarea.setAttribute("readonly", "readonly"); + doTest(textarea, "<textarea readonly>", false, true, true); + + // make non-tabbable plaintext editor + textarea.removeAttribute("readonly"); + const nsIPlaintextEditor = SpecialPowers.Ci.nsIPlaintextEditor; + const nsIDOMNSEditableElement = SpecialPowers.Ci.nsIDOMNSEditableElement; + var editor = SpecialPowers.wrap(textarea).editor; + var flags = editor.flags; + editor.flags = flags & ~(nsIPlaintextEditor.eEditorWidgetMask | + nsIPlaintextEditor.eEditorAllowInteraction); + doTest(textarea, "non-tabbable <textarea>", false, false, false); + + textarea.setAttribute("readonly", "readonly"); + doTest(textarea, "non-tabbable <textarea readonly>", false, true, false); + + editor.flags = flags; + + SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener, + true); + SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener, + false); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> |