1/*
2 * Copyright (C) 2010 Google Inc. All rights reserved.
3 * Copyright (C) 2012 Research In Motion Limited. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 *     * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *     * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
14 * distribution.
15 *     * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32#include "config.h"
33#include "SuggestionBoxHandler.h"
34
35#include "CSSPropertyNames.h"
36#include "CSSValueKeywords.h"
37#include "DOMTokenList.h"
38#include "ElementShadow.h"
39#include "ExceptionCodePlaceholder.h"
40#include "HTMLCollection.h"
41#include "HTMLDataListElement.h"
42#include "HTMLDivElement.h"
43#include "HTMLInputElement.h"
44#include "HTMLNames.h"
45#include "HTMLOptionElement.h"
46#include "HTMLSpanElement.h"
47#include "RenderBlock.h"
48#include "RenderObject.h"
49#include "ShadowRoot.h"
50#include "StyleResolver.h"
51#include "SuggestionBoxElement.h"
52#include "Text.h"
53
54#include <BlackBerryPlatformLog.h>
55#include <wtf/PassOwnPtr.h>
56
57namespace WebCore {
58
59using namespace HTMLNames;
60
61SuggestionBoxHandler::SuggestionBoxHandler(HTMLInputElement* element)
62    : m_inputElement(element)
63{
64}
65
66SuggestionBoxHandler::~SuggestionBoxHandler()
67{
68    hideDropdownBox();
69}
70
71PassOwnPtr<SuggestionBoxHandler> SuggestionBoxHandler::create(HTMLInputElement* element)
72{
73    return adoptPtr(new SuggestionBoxHandler(element));
74}
75
76void SuggestionBoxHandler::setInputElementAndUpdateDisplay(HTMLInputElement* element, bool allowEmptyPrefix)
77{
78    hideDropdownBox();
79    m_inputElement = element;
80    showDropdownBox(allowEmptyPrefix);
81}
82
83void SuggestionBoxHandler::showDropdownBox(bool allowEmptyPrefix)
84{
85    // Clearing suggestions and parse again
86    m_suggestions.clear();
87    parseSuggestions(allowEmptyPrefix);
88
89    // Don't bother building the tree if suggestion size is 0.
90    // We need to call insertSuggestions to clear previous suggestions
91    // in the DOM if suggestion size is 0.
92    if (!m_dropdownBox) {
93        if (!m_suggestions.size())
94            return;
95        buildDropdownBoxTree();
96    }
97
98    insertSuggestionsToDropdownBox();
99}
100
101void SuggestionBoxHandler::hideDropdownBox()
102{
103    if (m_dropdownBox) {
104        if (ShadowRoot* shadowRoot = m_inputElement->userAgentShadowRoot()) {
105            ExceptionCode ec;
106            shadowRoot->removeChild(m_dropdownBox.get(), ec);
107        }
108        m_dropdownBox = 0;
109    }
110    m_suggestions.clear();
111}
112
113void SuggestionBoxHandler::changeInputElementInnerTextValue(String& text)
114{
115    m_inputElement->setInnerTextValue(text);
116}
117
118void SuggestionBoxHandler::parseSuggestions(bool allowEmptyPrefix)
119{
120    // Return if this handler doesn't have an input element to attach to
121    if (!m_inputElement)
122        return;
123
124    // List attribute exists, check if datalist exists.
125    HTMLDataListElement* dList = m_inputElement->dataList();
126    if (!dList)
127        return;
128
129    RefPtr<HTMLCollection> optionsList = dList->options();
130    String prefix = m_inputElement->innerTextValue();
131
132    // Exit if:
133    // 1) If length of nodes in datalist is empty, return.
134    // 2) If we do not allow empty prefixes and prefix is null
135    if (!optionsList->length() || (!allowEmptyPrefix && prefix.isEmpty())) {
136        hideDropdownBox();
137        return;
138    }
139
140    // Prefix match the inner text value of the input against the candidate suggestions.
141    // Make sure the length of the suggestion is smaller than max length
142    for (unsigned i = 0; i < optionsList->length(); i++) {
143        HTMLOptionElement* target = toHTMLOptionElement((optionsList->item(i)));
144        if (!m_inputElement->isValidValue(target->value()))
145            continue;
146        String candidateString = m_inputElement->sanitizeValue(target->value());
147        if (!candidateString.findIgnoringCase(prefix) && candidateString.length() <= (unsigned) m_inputElement->maxLength())
148            m_suggestions.append(candidateString);
149    }
150}
151
152void SuggestionBoxHandler::insertSuggestionsToDropdownBox()
153{
154    // In case someone is calling this function without successfully building the dropdownBox tree
155    ASSERT(m_dropdownBox);
156
157    Document* document = m_dropdownBox->document();
158
159    // Clear the dropdown box's previous values
160    m_dropdownBox->removeChildren();
161
162    int prefixIndex = m_inputElement->innerTextValue().length();
163
164    unsigned total = m_suggestions.size();
165    for (unsigned i = 0; i < total; ++i) {
166        RefPtr<HTMLElement> suggestionItem = SuggestionBoxElement::create(this, m_suggestions[i], document);
167        if (i < total - 1)
168            suggestionItem->setPseudo("-webkit-suggestion-dropdown-box-item");
169        else
170            suggestionItem->setPseudo("-webkit-suggestion-dropdown-box-item-last");
171
172        // Append the part where it prefix matched
173        RefPtr<HTMLSpanElement> prefixElement = HTMLSpanElement::create(HTMLNames::spanTag, document);
174        prefixElement->appendChild(Text::create(document, m_suggestions[i].substring(0, prefixIndex)));
175        prefixElement->setPseudo("-webkit-suggestion-prefix-text");
176        suggestionItem->appendChild(prefixElement.release(), ASSERT_NO_EXCEPTION);
177
178        // Append the rest of the text in the suggestion item
179        suggestionItem->appendChild(Text::create(document, m_suggestions[i].substring(prefixIndex)), ASSERT_NO_EXCEPTION);
180        m_dropdownBox->appendChild(suggestionItem.release(), ASSERT_NO_EXCEPTION);
181    }
182}
183
184static void adjustDropdownBoxPosition(const LayoutRect& hostRect, HTMLElement* dropdownBox)
185{
186    // FIXME: reverse positioning and scroll
187    ASSERT(dropdownBox);
188    if (hostRect.isEmpty())
189        return;
190    double hostX = hostRect.x();
191    double hostY = hostRect.y();
192    if (RenderObject* renderer = dropdownBox->renderer()) {
193        if (RenderBox* container = renderer->containingBlock()) {
194            FloatPoint containerLocation = container->localToAbsolute();
195            hostX -= containerLocation.x() + container->borderLeft();
196            hostY -= containerLocation.y() + container->borderTop();
197        }
198    }
199
200    dropdownBox->setInlineStyleProperty(CSSPropertyTop, hostY + hostRect.height(), CSSPrimitiveValue::CSS_PX);
201    // The 'left' value of ::-webkit-validation-bubble-arrow.
202    const int bubbleArrowTopOffset = 32;
203    double bubbleX = hostX;
204    if (hostRect.width() / 2 < bubbleArrowTopOffset)
205        bubbleX = max(hostX + hostRect.width() / 2 - bubbleArrowTopOffset, 0.0);
206    dropdownBox->setInlineStyleProperty(CSSPropertyLeft, bubbleX, CSSPrimitiveValue::CSS_PX);
207}
208
209void SuggestionBoxHandler::buildDropdownBoxTree()
210{
211    ShadowRoot* shadowRoot = m_inputElement->ensureUserAgentShadowRoot();
212
213    // Return if shadowRoot is null
214    if (!shadowRoot)
215        return;
216
217    Document* document = m_inputElement->document();
218    m_dropdownBox = HTMLDivElement::create(document);
219    m_dropdownBox->setPseudo("-webkit-suggestion-dropdown-box");
220    // Need to force position:absolute because RenderMenuList doesn't assume it
221    // contains non-absolute or non-fixed renderers as children.
222    m_dropdownBox->setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
223
224    // Minus 2 pixels because of the 1px borders on the suggestion box.
225    m_dropdownBox->setInlineStyleProperty(CSSPropertyWidth, String::number(m_inputElement->boundingBox().width().rawValue() - 2) + "px", false);
226    ExceptionCode ec = 0;
227    shadowRoot->appendChild(m_dropdownBox.get(), ec);
228    ASSERT(!ec);
229    document->updateLayout();
230    adjustDropdownBoxPosition(m_inputElement->boundingBox(), m_dropdownBox.get());
231}
232
233} // namespace WebCore
234