/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/dom/HTMLOptionElement.h" #include "HTMLOptGroupElement.h" #include "mozilla/dom/HTMLOptionElementBinding.h" #include "mozilla/dom/HTMLSelectElement.h" #include "nsGkAtoms.h" #include "nsStyleConsts.h" #include "nsIFormControl.h" #include "nsISelectControlFrame.h" // Notify/query select frame for selected state #include "nsIFormControlFrame.h" #include "mozilla/dom/Document.h" #include "nsNodeInfoManager.h" #include "nsCOMPtr.h" #include "nsContentCreatorFunctions.h" #include "mozAutoDocUpdate.h" #include "nsTextNode.h" /** * Implementation of <option> */ NS_IMPL_NS_NEW_HTML_ELEMENT(Option) namespace mozilla::dom { HTMLOptionElement::HTMLOptionElement( already_AddRefed&& aNodeInfo) : nsGenericHTMLElement(std::move(aNodeInfo)), mSelectedChanged(false), mIsSelected(false), mIsInSetDefaultSelected(false) { // We start off enabled AddStatesSilently(ElementState::ENABLED); } HTMLOptionElement::~HTMLOptionElement() = default; NS_IMPL_ELEMENT_CLONE(HTMLOptionElement) mozilla::dom::HTMLFormElement* HTMLOptionElement::GetForm() { HTMLSelectElement* selectControl = GetSelect(); return selectControl ? selectControl->GetForm() : nullptr; } void HTMLOptionElement::SetSelectedInternal(bool aValue, bool aNotify) { mSelectedChanged = true; mIsSelected = aValue; // When mIsInSetDefaultSelected is true, the state change will be handled by // SetAttr/UnsetAttr. if (!mIsInSetDefaultSelected) { UpdateState(aNotify); } } void HTMLOptionElement::OptGroupDisabledChanged(bool aNotify) { UpdateDisabledState(aNotify); } void HTMLOptionElement::UpdateDisabledState(bool aNotify) { bool isDisabled = HasAttr(kNameSpaceID_None, nsGkAtoms::disabled); if (!isDisabled) { nsIContent* parent = GetParent(); if (auto optGroupElement = HTMLOptGroupElement::FromNodeOrNull(parent)) { isDisabled = optGroupElement->IsDisabled(); } } ElementState disabledStates; if (isDisabled) { disabledStates |= ElementState::DISABLED; } else { disabledStates |= ElementState::ENABLED; } ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES; ElementState changedStates = disabledStates ^ oldDisabledStates; if (!changedStates.IsEmpty()) { ToggleStates(changedStates, aNotify); } } void HTMLOptionElement::SetSelected(bool aValue) { // Note: The select content obj maintains all the PresState // so defer to it to get the answer HTMLSelectElement* selectInt = GetSelect(); if (selectInt) { int32_t index = Index(); HTMLSelectElement::OptionFlags mask{ HTMLSelectElement::OptionFlag::SetDisabled, HTMLSelectElement::OptionFlag::Notify}; if (aValue) { mask += HTMLSelectElement::OptionFlag::IsSelected; } // This should end up calling SetSelectedInternal selectInt->SetOptionsSelectedByIndex(index, index, mask); } else { SetSelectedInternal(aValue, true); } } int32_t HTMLOptionElement::Index() { static int32_t defaultIndex = 0; // Only select elements can contain a list of options. HTMLSelectElement* selectElement = GetSelect(); if (!selectElement) { return defaultIndex; } HTMLOptionsCollection* options = selectElement->GetOptions(); if (!options) { return defaultIndex; } int32_t index = defaultIndex; MOZ_ALWAYS_SUCCEEDS(options->GetOptionIndex(this, 0, true, &index)); return index; } nsChangeHint HTMLOptionElement::GetAttributeChangeHint(const nsAtom* aAttribute, int32_t aModType) const { nsChangeHint retval = nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType); if (aAttribute == nsGkAtoms::label) { retval |= nsChangeHint_ReconstructFrame; } else if (aAttribute == nsGkAtoms::text) { retval |= NS_STYLE_HINT_REFLOW; } return retval; } void HTMLOptionElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, const nsAttrValue* aValue, bool aNotify) { nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue, aNotify); if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::selected || mSelectedChanged) { return; } // We just changed out selected state (since we look at the "selected" // attribute when mSelectedChanged is false). Let's tell our select about // it. HTMLSelectElement* selectInt = GetSelect(); if (!selectInt) { // If option is a child of select, SetOptionsSelectedByIndex will set // mIsSelected if needed. mIsSelected = aValue; return; } NS_ASSERTION(!mSelectedChanged, "Shouldn't be here"); bool inSetDefaultSelected = mIsInSetDefaultSelected; mIsInSetDefaultSelected = true; int32_t index = Index(); HTMLSelectElement::OptionFlags mask = HTMLSelectElement::OptionFlag::SetDisabled; if (aValue) { mask += HTMLSelectElement::OptionFlag::IsSelected; } if (aNotify) { mask += HTMLSelectElement::OptionFlag::Notify; } // This can end up calling SetSelectedInternal if our selected state needs to // change, which we will allow to take effect so that parts of // SetOptionsSelectedByIndex that might depend on it working don't get // confused. selectInt->SetOptionsSelectedByIndex(index, index, mask); // Now reset our members; when we finish the attr set we'll end up with the // rigt selected state. mIsInSetDefaultSelected = inSetDefaultSelected; // mIsSelected might have been changed by SetOptionsSelectedByIndex. Possibly // more than once; make sure our mSelectedChanged state is set back correctly. mSelectedChanged = false; } void HTMLOptionElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, const nsAttrValue* aValue, const nsAttrValue* aOldValue, nsIPrincipal* aSubjectPrincipal, bool aNotify) { if (aNameSpaceID == kNameSpaceID_None) { if (aName == nsGkAtoms::disabled) { UpdateDisabledState(aNotify); } if (aName == nsGkAtoms::value && Selected()) { // Since this option is selected, changing value // may have changed missing validity state of the // Select element HTMLSelectElement* select = GetSelect(); if (select) { select->UpdateValueMissingValidityState(); } } } return nsGenericHTMLElement::AfterSetAttr( aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); } void HTMLOptionElement::GetText(nsAString& aText) { nsAutoString text; nsIContent* child = nsINode::GetFirstChild(); while (child) { if (Text* textChild = child->GetAsText()) { textChild->AppendTextTo(text); } if (child->IsHTMLElement(nsGkAtoms::script) || child->IsSVGElement(nsGkAtoms::script)) { child = child->GetNextNonChildNode(this); } else { child = child->GetNextNode(this); } } // XXX No CompressWhitespace for nsAString. Sad. text.CompressWhitespace(true, true); aText = text; } void HTMLOptionElement::SetText(const nsAString& aText, ErrorResult& aRv) { aRv = nsContentUtils::SetNodeTextContent(this, aText, true); } nsresult HTMLOptionElement::BindToTree(BindContext& aContext, nsINode& aParent) { nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); NS_ENSURE_SUCCESS(rv, rv); // Our new parent might change :disabled/:enabled state. UpdateDisabledState(false); return NS_OK; } void HTMLOptionElement::UnbindFromTree(bool aNullParent) { nsGenericHTMLElement::UnbindFromTree(aNullParent); // Our previous parent could have been involved in :disabled/:enabled state. UpdateDisabledState(false); } ElementState HTMLOptionElement::IntrinsicState() const { ElementState state = nsGenericHTMLElement::IntrinsicState(); if (Selected()) { state |= ElementState::CHECKED; } if (DefaultSelected()) { state |= ElementState::DEFAULT; } return state; } // Get the select content element that contains this option HTMLSelectElement* HTMLOptionElement::GetSelect() { nsIContent* parent = GetParent(); if (!parent) { return nullptr; } HTMLSelectElement* select = HTMLSelectElement::FromNode(parent); if (select) { return select; } if (!parent->IsHTMLElement(nsGkAtoms::optgroup)) { return nullptr; } return HTMLSelectElement::FromNodeOrNull(parent->GetParent()); } already_AddRefed HTMLOptionElement::Option( const GlobalObject& aGlobal, const nsAString& aText, const Optional& aValue, bool aDefaultSelected, bool aSelected, ErrorResult& aError) { nsCOMPtr win = do_QueryInterface(aGlobal.GetAsSupports()); Document* doc; if (!win || !(doc = win->GetExtantDoc())) { aError.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr nodeInfo = doc->NodeInfoManager()->GetNodeInfo( nsGkAtoms::option, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE); auto* nim = nodeInfo->NodeInfoManager(); RefPtr option = new (nim) HTMLOptionElement(nodeInfo.forget()); if (!aText.IsEmpty()) { // Create a new text node and append it to the option RefPtr textContent = new (option->NodeInfo()->NodeInfoManager()) nsTextNode(option->NodeInfo()->NodeInfoManager()); textContent->SetText(aText, false); option->AppendChildTo(textContent, false, aError); if (aError.Failed()) { return nullptr; } } if (aValue.WasPassed()) { // Set the value attribute for this element. We're calling SetAttr // directly because we want to pass aNotify == false. aError = option->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue.Value(), false); if (aError.Failed()) { return nullptr; } } if (aDefaultSelected) { // We're calling SetAttr directly because we want to pass // aNotify == false. aError = option->SetAttr(kNameSpaceID_None, nsGkAtoms::selected, u""_ns, false); if (aError.Failed()) { return nullptr; } } option->SetSelected(aSelected); option->SetSelectedChanged(false); return option.forget(); } nsresult HTMLOptionElement::CopyInnerTo(Element* aDest) { nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); NS_ENSURE_SUCCESS(rv, rv); if (aDest->OwnerDoc()->IsStaticDocument()) { static_cast(aDest)->SetSelected(Selected()); } return NS_OK; } JSObject* HTMLOptionElement::WrapNode(JSContext* aCx, JS::Handle aGivenProto) { return HTMLOptionElement_Binding::Wrap(aCx, this, aGivenProto); } } // namespace mozilla::dom