2 * This file is part of the select element renderer in WebCore.
4 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
5 * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. All rights reserved.
6 * 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Library General Public License for more details.
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
26 #include "RenderMenuList.h"
28 #include "AXObjectCache.h"
29 #include "AccessibilityMenuList.h"
30 #include "CSSFontSelector.h"
32 #include "FontCache.h"
34 #include "FrameView.h"
35 #include "HTMLNames.h"
36 #include "HTMLOptionElement.h"
37 #include "HTMLOptGroupElement.h"
38 #include "HTMLSelectElement.h"
39 #include "NodeRenderStyle.h"
41 #include "PopupMenu.h"
42 #include "RenderScrollbar.h"
43 #include "RenderText.h"
44 #include "RenderTheme.h"
45 #include "RenderView.h"
47 #include "StyleResolver.h"
52 #include "LocalizedStrings.h"
57 using namespace HTMLNames;
60 static size_t selectedOptionCount(const RenderMenuList& renderMenuList)
62 const Vector<HTMLElement*>& listItems = renderMenuList.selectElement().listItems();
63 size_t numberOfItems = listItems.size();
66 for (size_t i = 0; i < numberOfItems; ++i) {
67 if (listItems[i]->hasTagName(optionTag) && toHTMLOptionElement(listItems[i])->selected())
74 RenderMenuList::RenderMenuList(HTMLSelectElement& element, PassRef<RenderStyle> style)
75 : RenderFlexibleBox(element, std::move(style))
76 , m_buttonText(nullptr)
77 , m_innerBlock(nullptr)
78 , m_needsOptionsWidthUpdate(true)
80 , m_lastActiveIndex(-1)
82 , m_popupIsVisible(false)
87 RenderMenuList::~RenderMenuList()
91 m_popup->disconnectClient();
96 void RenderMenuList::createInnerBlock()
99 ASSERT(firstChild() == m_innerBlock);
100 ASSERT(!m_innerBlock->nextSibling());
104 // Create an anonymous block.
105 ASSERT(!firstChild());
106 m_innerBlock = createAnonymousBlock();
108 RenderFlexibleBox::addChild(m_innerBlock);
111 void RenderMenuList::adjustInnerStyle()
113 RenderStyle& innerStyle = m_innerBlock->style();
114 innerStyle.setFlexGrow(1);
115 innerStyle.setFlexShrink(1);
116 // min-width: 0; is needed for correct shrinking.
117 // FIXME: Remove this line when https://bugs.webkit.org/show_bug.cgi?id=111790 is fixed.
118 innerStyle.setMinWidth(Length(0, Fixed));
119 // Use margin:auto instead of align-items:center to get safe centering, i.e.
120 // when the content overflows, treat it the same as align-items: flex-start.
121 // But we only do that for the cases where html.css would otherwise use center.
122 if (style().alignItems() == AlignCenter) {
123 innerStyle.setMarginTop(Length());
124 innerStyle.setMarginBottom(Length());
125 innerStyle.setAlignSelf(AlignFlexStart);
128 innerStyle.setPaddingLeft(Length(theme().popupInternalPaddingLeft(&style()), Fixed));
129 innerStyle.setPaddingRight(Length(theme().popupInternalPaddingRight(&style()), Fixed));
130 innerStyle.setPaddingTop(Length(theme().popupInternalPaddingTop(&style()), Fixed));
131 innerStyle.setPaddingBottom(Length(theme().popupInternalPaddingBottom(&style()), Fixed));
133 if (document().page()->chrome().selectItemWritingDirectionIsNatural()) {
134 // Items in the popup will not respect the CSS text-align and direction properties,
135 // so we must adjust our own style to match.
136 innerStyle.setTextAlign(LEFT);
137 TextDirection direction = (m_buttonText && m_buttonText->text()->defaultWritingDirection() == U_RIGHT_TO_LEFT) ? RTL : LTR;
138 innerStyle.setDirection(direction);
140 } else if (document().page()->chrome().selectItemAlignmentFollowsMenuWritingDirection()) {
141 innerStyle.setTextAlign(style().direction() == LTR ? LEFT : RIGHT);
142 TextDirection direction;
143 EUnicodeBidi unicodeBidi;
144 if (multiple() && selectedOptionCount(*this) != 1) {
145 direction = (m_buttonText && m_buttonText->text()->defaultWritingDirection() == U_RIGHT_TO_LEFT) ? RTL : LTR;
146 unicodeBidi = UBNormal;
147 } else if (m_optionStyle) {
148 direction = m_optionStyle->direction();
149 unicodeBidi = m_optionStyle->unicodeBidi();
151 direction = style().direction();
152 unicodeBidi = style().unicodeBidi();
155 innerStyle.setDirection(direction);
156 innerStyle.setUnicodeBidi(unicodeBidi);
159 } else if (m_optionStyle && document().page()->chrome().selectItemAlignmentFollowsMenuWritingDirection()) {
160 if ((m_optionStyle->direction() != innerStyle.direction() || m_optionStyle->unicodeBidi() != innerStyle.unicodeBidi()))
161 m_innerBlock->setNeedsLayoutAndPrefWidthsRecalc();
162 innerStyle.setTextAlign(style().isLeftToRightDirection() ? LEFT : RIGHT);
163 innerStyle.setDirection(m_optionStyle->direction());
164 innerStyle.setUnicodeBidi(m_optionStyle->unicodeBidi());
166 #endif // !PLATFORM(IOS)
169 HTMLSelectElement& RenderMenuList::selectElement() const
171 return toHTMLSelectElement(nodeForNonAnonymous());
174 void RenderMenuList::addChild(RenderObject* newChild, RenderObject* beforeChild)
177 m_innerBlock->addChild(newChild, beforeChild);
178 ASSERT(m_innerBlock == firstChild());
180 if (AXObjectCache* cache = document().existingAXObjectCache())
181 cache->childrenChanged(this, newChild);
184 void RenderMenuList::removeChild(RenderObject& oldChild)
186 if (&oldChild == m_innerBlock || !m_innerBlock) {
187 RenderFlexibleBox::removeChild(oldChild);
190 m_innerBlock->removeChild(oldChild);
193 void RenderMenuList::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
195 RenderBlock::styleDidChange(diff, oldStyle);
197 if (m_innerBlock) // RenderBlock handled updating the anonymous block's style.
200 bool fontChanged = !oldStyle || oldStyle->font() != style().font();
202 updateOptionsWidth();
203 m_needsOptionsWidthUpdate = false;
207 void RenderMenuList::updateOptionsWidth()
209 float maxOptionWidth = 0;
210 const Vector<HTMLElement*>& listItems = selectElement().listItems();
211 int size = listItems.size();
212 FontCachePurgePreventer fontCachePurgePreventer;
214 for (int i = 0; i < size; ++i) {
215 HTMLElement* element = listItems[i];
216 if (!isHTMLOptionElement(element))
219 String text = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel();
220 applyTextTransform(style(), text, ' ');
221 if (theme().popupOptionSupportsTextIndent()) {
222 // Add in the option's text indent. We can't calculate percentage values for now.
223 float optionWidth = 0;
224 if (RenderStyle* optionStyle = element->renderStyle())
225 optionWidth += minimumValueForLength(optionStyle->textIndent(), 0);
227 optionWidth += style().font().width(text);
228 maxOptionWidth = std::max(maxOptionWidth, optionWidth);
229 } else if (!text.isEmpty())
230 maxOptionWidth = std::max(maxOptionWidth, style().font().width(text));
233 int width = static_cast<int>(ceilf(maxOptionWidth));
234 if (m_optionsWidth == width)
237 m_optionsWidth = width;
239 setNeedsLayoutAndPrefWidthsRecalc();
242 void RenderMenuList::updateFromElement()
244 if (m_needsOptionsWidthUpdate) {
245 updateOptionsWidth();
246 m_needsOptionsWidthUpdate = false;
250 if (m_popupIsVisible)
251 m_popup->updateFromElement();
254 setTextFromOption(selectElement().selectedIndex());
257 void RenderMenuList::setTextFromOption(int optionIndex)
259 const Vector<HTMLElement*>& listItems = selectElement().listItems();
260 int size = listItems.size();
262 int i = selectElement().optionToListIndex(optionIndex);
263 String text = emptyString();
264 if (i >= 0 && i < size) {
265 Element* element = listItems[i];
266 if (isHTMLOptionElement(element)) {
267 text = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel();
268 m_optionStyle = element->renderStyle();
274 size_t count = selectedOptionCount(*this);
276 text = htmlSelectMultipleItems(count);
280 setText(text.stripWhiteSpace());
281 didUpdateActiveOption(optionIndex);
284 void RenderMenuList::setText(const String& s)
286 String textToUse = s.isEmpty() ? String(ASCIILiteral("\n")) : s;
289 m_buttonText->setText(textToUse.impl(), true);
291 m_buttonText = new RenderText(document(), textToUse);
292 addChild(m_buttonText);
297 String RenderMenuList::text() const
299 return m_buttonText ? m_buttonText->text() : String();
302 LayoutRect RenderMenuList::controlClipRect(const LayoutPoint& additionalOffset) const
304 // Clip to the intersection of the content box and the content box for the inner box
305 // This will leave room for the arrows which sit in the inner box padding,
306 // and if the inner box ever spills out of the outer box, that will get clipped too.
307 LayoutRect outerBox(additionalOffset.x() + borderLeft() + paddingLeft(),
308 additionalOffset.y() + borderTop() + paddingTop(),
312 LayoutRect innerBox(additionalOffset.x() + m_innerBlock->x() + m_innerBlock->paddingLeft(),
313 additionalOffset.y() + m_innerBlock->y() + m_innerBlock->paddingTop(),
314 m_innerBlock->contentWidth(),
315 m_innerBlock->contentHeight());
317 return intersection(outerBox, innerBox);
320 void RenderMenuList::computeIntrinsicLogicalWidths(LayoutUnit& minLogicalWidth, LayoutUnit& maxLogicalWidth) const
322 maxLogicalWidth = std::max(m_optionsWidth, theme().minimumMenuListSize(&style())) + m_innerBlock->paddingLeft() + m_innerBlock->paddingRight();
323 if (!style().width().isPercent())
324 minLogicalWidth = maxLogicalWidth;
327 void RenderMenuList::computePreferredLogicalWidths()
329 m_minPreferredLogicalWidth = 0;
330 m_maxPreferredLogicalWidth = 0;
332 if (style().width().isFixed() && style().width().value() > 0)
333 m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = adjustContentBoxLogicalWidthForBoxSizing(style().width().value());
335 computeIntrinsicLogicalWidths(m_minPreferredLogicalWidth, m_maxPreferredLogicalWidth);
337 if (style().minWidth().isFixed() && style().minWidth().value() > 0) {
338 m_maxPreferredLogicalWidth = std::max(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().minWidth().value()));
339 m_minPreferredLogicalWidth = std::max(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().minWidth().value()));
342 if (style().maxWidth().isFixed()) {
343 m_maxPreferredLogicalWidth = std::min(m_maxPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().maxWidth().value()));
344 m_minPreferredLogicalWidth = std::min(m_minPreferredLogicalWidth, adjustContentBoxLogicalWidthForBoxSizing(style().maxWidth().value()));
347 LayoutUnit toAdd = horizontalBorderAndPaddingExtent();
348 m_minPreferredLogicalWidth += toAdd;
349 m_maxPreferredLogicalWidth += toAdd;
351 setPreferredLogicalWidthsDirty(false);
355 NO_RETURN_DUE_TO_ASSERT
356 void RenderMenuList::showPopup()
358 ASSERT_NOT_REACHED();
361 void RenderMenuList::showPopup()
363 if (m_popupIsVisible)
366 if (document().page()->chrome().hasOpenedPopup())
369 // Create m_innerBlock here so it ends up as the first child.
370 // This is important because otherwise we might try to create m_innerBlock
371 // inside the showPopup call and it would fail.
374 m_popup = document().page()->chrome().createPopupMenu(this);
375 m_popupIsVisible = true;
377 // Compute the top left taking transforms into account, but use
378 // the actual width of the element to size the popup.
379 FloatPoint absTopLeft = localToAbsolute(FloatPoint(), UseTransforms);
380 IntRect absBounds = absoluteBoundingBoxRectIgnoringTransforms();
381 absBounds.setLocation(roundedIntPoint(absTopLeft));
382 m_popup->show(absBounds, &view().frameView(), selectElement().optionToListIndex(selectElement().selectedIndex()));
386 void RenderMenuList::hidePopup()
394 void RenderMenuList::valueChanged(unsigned listIndex, bool fireOnChange)
396 // Check to ensure a page navigation has not occurred while
398 if (&document() != document().frame()->document())
401 selectElement().optionSelectedByUser(selectElement().listToOptionIndex(listIndex), fireOnChange);
404 void RenderMenuList::listBoxSelectItem(int listIndex, bool allowMultiplySelections, bool shift, bool fireOnChangeNow)
406 selectElement().listBoxSelectItem(listIndex, allowMultiplySelections, shift, fireOnChangeNow);
409 bool RenderMenuList::multiple() const
411 return selectElement().multiple();
414 void RenderMenuList::didSetSelectedIndex(int listIndex)
416 didUpdateActiveOption(selectElement().listToOptionIndex(listIndex));
419 void RenderMenuList::didUpdateActiveOption(int optionIndex)
421 if (!AXObjectCache::accessibilityEnabled() || !document().existingAXObjectCache())
424 if (m_lastActiveIndex == optionIndex)
426 m_lastActiveIndex = optionIndex;
428 int listIndex = selectElement().optionToListIndex(optionIndex);
429 if (listIndex < 0 || listIndex >= static_cast<int>(selectElement().listItems().size()))
432 HTMLElement* listItem = selectElement().listItems()[listIndex];
434 if (listItem->renderer()) {
435 if (AccessibilityMenuList* menuList = toAccessibilityMenuList(document().axObjectCache()->get(this)))
436 menuList->didUpdateActiveOption(optionIndex);
440 String RenderMenuList::itemText(unsigned listIndex) const
442 const Vector<HTMLElement*>& listItems = selectElement().listItems();
443 if (listIndex >= listItems.size())
447 Element* element = listItems[listIndex];
448 if (isHTMLOptGroupElement(element))
449 itemString = toHTMLOptGroupElement(element)->groupLabelText();
450 else if (isHTMLOptionElement(element))
451 itemString = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel();
453 applyTextTransform(style(), itemString, ' ');
457 String RenderMenuList::itemLabel(unsigned) const
462 String RenderMenuList::itemIcon(unsigned) const
467 String RenderMenuList::itemAccessibilityText(unsigned listIndex) const
469 // Allow the accessible name be changed if necessary.
470 const Vector<HTMLElement*>& listItems = selectElement().listItems();
471 if (listIndex >= listItems.size())
473 return listItems[listIndex]->fastGetAttribute(aria_labelAttr);
476 String RenderMenuList::itemToolTip(unsigned listIndex) const
478 const Vector<HTMLElement*>& listItems = selectElement().listItems();
479 if (listIndex >= listItems.size())
481 return listItems[listIndex]->title();
484 bool RenderMenuList::itemIsEnabled(unsigned listIndex) const
486 const Vector<HTMLElement*>& listItems = selectElement().listItems();
487 if (listIndex >= listItems.size())
489 HTMLElement* element = listItems[listIndex];
490 if (!isHTMLOptionElement(element))
493 bool groupEnabled = true;
494 if (Element* parentElement = element->parentElement()) {
495 if (isHTMLOptGroupElement(parentElement))
496 groupEnabled = !parentElement->isDisabledFormControl();
501 return !element->isDisabledFormControl();
504 PopupMenuStyle RenderMenuList::itemStyle(unsigned listIndex) const
506 const Vector<HTMLElement*>& listItems = selectElement().listItems();
507 if (listIndex >= listItems.size()) {
508 // If we are making an out of bounds access, then we want to use the style
509 // of a different option element (index 0). However, if there isn't an option element
510 // before at index 0, we fall back to the menu's style.
514 // Try to retrieve the style of an option element we know exists (index 0).
517 HTMLElement* element = listItems[listIndex];
519 Color itemBackgroundColor;
520 bool itemHasCustomBackgroundColor;
521 getItemBackgroundColor(listIndex, itemBackgroundColor, itemHasCustomBackgroundColor);
523 RenderStyle* style = element->renderStyle() ? element->renderStyle() : element->computedStyle();
524 return style ? PopupMenuStyle(style->visitedDependentColor(CSSPropertyColor), itemBackgroundColor, style->font(), style->visibility() == VISIBLE,
525 style->display() == NONE, true, style->textIndent(), style->direction(), isOverride(style->unicodeBidi()),
526 itemHasCustomBackgroundColor ? PopupMenuStyle::CustomBackgroundColor : PopupMenuStyle::DefaultBackgroundColor) : menuStyle();
529 void RenderMenuList::getItemBackgroundColor(unsigned listIndex, Color& itemBackgroundColor, bool& itemHasCustomBackgroundColor) const
531 const Vector<HTMLElement*>& listItems = selectElement().listItems();
532 if (listIndex >= listItems.size()) {
533 itemBackgroundColor = style().visitedDependentColor(CSSPropertyBackgroundColor);
534 itemHasCustomBackgroundColor = false;
537 HTMLElement* element = listItems[listIndex];
539 Color backgroundColor;
540 if (element->renderStyle())
541 backgroundColor = element->renderStyle()->visitedDependentColor(CSSPropertyBackgroundColor);
542 itemHasCustomBackgroundColor = backgroundColor.isValid() && backgroundColor.alpha();
543 // If the item has an opaque background color, return that.
544 if (!backgroundColor.hasAlpha()) {
545 itemBackgroundColor = backgroundColor;
549 // Otherwise, the item's background is overlayed on top of the menu background.
550 backgroundColor = style().visitedDependentColor(CSSPropertyBackgroundColor).blend(backgroundColor);
551 if (!backgroundColor.hasAlpha()) {
552 itemBackgroundColor = backgroundColor;
556 // If the menu background is not opaque, then add an opaque white background behind.
557 itemBackgroundColor = Color(Color::white).blend(backgroundColor);
560 PopupMenuStyle RenderMenuList::menuStyle() const
562 const RenderStyle& styleToUse = m_innerBlock ? m_innerBlock->style() : style();
563 IntRect absBounds = absoluteBoundingBoxRectIgnoringTransforms();
564 return PopupMenuStyle(styleToUse.visitedDependentColor(CSSPropertyColor), styleToUse.visitedDependentColor(CSSPropertyBackgroundColor),
565 styleToUse.font(), styleToUse.visibility() == VISIBLE, styleToUse.display() == NONE, style().hasAppearance(), styleToUse.textIndent(),
566 style().direction(), isOverride(style().unicodeBidi()), PopupMenuStyle::DefaultBackgroundColor,
567 PopupMenuStyle::SelectPopup, theme().popupMenuSize(&styleToUse, absBounds));
570 HostWindow* RenderMenuList::hostWindow() const
572 return view().frameView().hostWindow();
575 PassRefPtr<Scrollbar> RenderMenuList::createScrollbar(ScrollableArea* scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize controlSize)
577 RefPtr<Scrollbar> widget;
578 bool hasCustomScrollbarStyle = style().hasPseudoStyle(SCROLLBAR);
579 if (hasCustomScrollbarStyle)
580 widget = RenderScrollbar::createCustomScrollbar(scrollableArea, orientation, &selectElement());
582 widget = Scrollbar::createNativeScrollbar(scrollableArea, orientation, controlSize);
583 return widget.release();
586 int RenderMenuList::clientInsetLeft() const
591 int RenderMenuList::clientInsetRight() const
596 LayoutUnit RenderMenuList::clientPaddingLeft() const
598 return paddingLeft() + m_innerBlock->paddingLeft();
601 const int endOfLinePadding = 2;
602 LayoutUnit RenderMenuList::clientPaddingRight() const
604 if (style().appearance() == MenulistPart || style().appearance() == MenulistButtonPart) {
605 // For these appearance values, the theme applies padding to leave room for the
606 // drop-down button. But leaving room for the button inside the popup menu itself
607 // looks strange, so we return a small default padding to avoid having a large empty
608 // space appear on the side of the popup menu.
609 return endOfLinePadding;
612 // If the appearance isn't MenulistPart, then the select is styled (non-native), so
613 // we want to return the user specified padding.
614 return paddingRight() + m_innerBlock->paddingRight();
617 int RenderMenuList::listSize() const
619 return selectElement().listItems().size();
622 int RenderMenuList::selectedIndex() const
624 return selectElement().optionToListIndex(selectElement().selectedIndex());
627 void RenderMenuList::popupDidHide()
630 m_popupIsVisible = false;
634 bool RenderMenuList::itemIsSeparator(unsigned listIndex) const
636 const Vector<HTMLElement*>& listItems = selectElement().listItems();
637 return listIndex < listItems.size() && listItems[listIndex]->hasTagName(hrTag);
640 bool RenderMenuList::itemIsLabel(unsigned listIndex) const
642 const Vector<HTMLElement*>& listItems = selectElement().listItems();
643 return listIndex < listItems.size() && isHTMLOptGroupElement(listItems[listIndex]);
646 bool RenderMenuList::itemIsSelected(unsigned listIndex) const
648 const Vector<HTMLElement*>& listItems = selectElement().listItems();
649 if (listIndex >= listItems.size())
651 HTMLElement* element = listItems[listIndex];
652 return isHTMLOptionElement(element) && toHTMLOptionElement(element)->selected();
655 void RenderMenuList::setTextFromItem(unsigned listIndex)
657 setTextFromOption(selectElement().listToOptionIndex(listIndex));
660 FontSelector* RenderMenuList::fontSelector() const
662 return document().ensureStyleResolver().fontSelector();