Better type ahead for DateTimeSymbolicFieldElement
authorkeishi@webkit.org <keishi@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 29 Nov 2012 09:15:57 +0000 (09:15 +0000)
committerkeishi@webkit.org <keishi@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 29 Nov 2012 09:15:57 +0000 (09:15 +0000)
https://bugs.webkit.org/show_bug.cgi?id=103031

Reviewed by Kent Tamura.

Source/WebCore:

This cuts out the type ahead code that will be used by HTMLSelectElement
and DateTimeSymbolicFieldElement into a TypeAhead class. This will
improve DateTimeSymbolicFieldElement type ahead which was first
character match only, by adding cycling, prefix match and index number
match.

Added tests to month-multiple-fields-keyboard-events.html.

* GNUmakefile.list.am: Added TypeAhead.{h,cpp}
* Target.pri: Ditto.
* WebCore.gypi: Ditto.
* WebCore.vcproj/WebCore.vcproj: Ditto.
* WebCore.xcodeproj/project.pbxproj: Ditto.
* CMakeLists.txt: Ditto.
* html/HTMLSelectElement.cpp:
(WebCore):
(WebCore::HTMLSelectElement::HTMLSelectElement):
(WebCore::HTMLSelectElement::indexOfSelectedOption): Returns index of current selection.
(WebCore::HTMLSelectElement::optionCount): Returns total number of options.
(WebCore::HTMLSelectElement::optionAtIndex): Returns option at index.
(WebCore::HTMLSelectElement::typeAheadFind): Use TypeAhead.
* html/HTMLSelectElement.h:
(HTMLSelectElement):
* html/TypeAhead.cpp: Added.
(WebCore):
(WebCore::TypeAhead::TypeAhead):
(WebCore::stripLeadingWhiteSpace): Moved from HTMLSelectElement.cpp.
(WebCore::TypeAhead::handleEvent): Returns index for match.
* html/TypeAhead.h: Added.
(WebCore):
(TypeAheadDataSource): Provide the data about the options that TypeAhead should match against.
(TypeAhead):
* html/shadow/DateTimeSymbolicFieldElement.cpp:
(WebCore::DateTimeSymbolicFieldElement::DateTimeSymbolicFieldElement):
(WebCore::DateTimeSymbolicFieldElement::handleKeyboardEvent):
(WebCore::DateTimeSymbolicFieldElement::indexOfSelectedOption):
(WebCore):
(WebCore::DateTimeSymbolicFieldElement::optionCount):
(WebCore::DateTimeSymbolicFieldElement::optionAtIndex):
* html/shadow/DateTimeSymbolicFieldElement.h:
(DateTimeSymbolicFieldElement):

LayoutTests:

* fast/forms/month-multiple-fields/month-multiple-fields-keyboard-events-expected.txt:
* fast/forms/month-multiple-fields/month-multiple-fields-keyboard-events.html: Added tests for typeahead.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@136111 268f45cc-cd09-0410-ab3c-d52691b4dbfc

16 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/forms/month-multiple-fields/month-multiple-fields-keyboard-events-expected.txt
LayoutTests/fast/forms/month-multiple-fields/month-multiple-fields-keyboard-events.html
Source/WebCore/CMakeLists.txt
Source/WebCore/ChangeLog
Source/WebCore/GNUmakefile.list.am
Source/WebCore/Target.pri
Source/WebCore/WebCore.gypi
Source/WebCore/WebCore.vcproj/WebCore.vcproj
Source/WebCore/WebCore.xcodeproj/project.pbxproj
Source/WebCore/html/HTMLSelectElement.cpp
Source/WebCore/html/HTMLSelectElement.h
Source/WebCore/html/TypeAhead.cpp [new file with mode: 0644]
Source/WebCore/html/TypeAhead.h [new file with mode: 0644]
Source/WebCore/html/shadow/DateTimeSymbolicFieldElement.cpp
Source/WebCore/html/shadow/DateTimeSymbolicFieldElement.h

index 9f65a70..ae0ebfd 100644 (file)
@@ -1,3 +1,13 @@
+2012-11-29  Keishi Hattori  <keishi@webkit.org>
+
+        Better type ahead for DateTimeSymbolicFieldElement
+        https://bugs.webkit.org/show_bug.cgi?id=103031
+
+        Reviewed by Kent Tamura.
+
+        * fast/forms/month-multiple-fields/month-multiple-fields-keyboard-events-expected.txt:
+        * fast/forms/month-multiple-fields/month-multiple-fields-keyboard-events.html: Added tests for typeahead.
+
 2012-11-29  Kent Tamura  <tkent@chromium.org>
 
         Convert *-appearance-pseudo-classes.html to dumpAsText tests
index 52c657f..14c9648 100644 (file)
@@ -15,7 +15,7 @@ Backspace - Make value empty
 == Digit keys ==
 FAIL input.value should be 0012-09. Was 0112-09.
 == Left/Right keys ==
-FAIL input.value should be 0005-06. Was 0005-09.
+PASS input.value is "0005-06"
 PASS document.activeElement.id is "input"
 == Up/Down keys ==
 PASS input.value is "2012-10"
@@ -49,6 +49,23 @@ PASS input.value is ""
 == Typeahead ==
 PASS input.value is "0001-12"
 PASS input.value is "0002-12"
+== Typeahead cycle first character ==
+PASS input.value is "2012-01"
+PASS input.value is "2012-06"
+PASS input.value is "2012-07"
+PASS input.value is "2012-01"
+PASS input.value is "2012-01"
+== Typeahead prefix match ==
+PASS input.value is "2012-03"
+PASS input.value is "2012-03"
+PASS input.value is "2012-05"
+PASS input.value is "2012-05"
+== Typeahead index match ==
+PASS input.value is "2012-01"
+PASS input.value is "2012-12"
+PASS input.value is "2012-12"
+== Typeahead should search from current selection ==
+PASS input.value is "2012-06"
 == RTL Left/Right keys ==
 PASS input.value is "2012-10"
 PASS input.value is "0002-10"
index 3213448..56036b7 100644 (file)
@@ -171,6 +171,40 @@ keyDown('rightArrow');
 keyDown('2');
 shouldBeEqualToString('input.value', '0002-12');
 
+beginTest('Typeahead cycle first character', '2012-09');
+keyDown('j'); //          -> [Jan] 2012
+shouldBeEqualToString('input.value', '2012-01');
+keyDown('j'); //          -> [Jun] 2012
+shouldBeEqualToString('input.value', '2012-06');
+keyDown('j'); //          -> [Jul] 2012
+shouldBeEqualToString('input.value', '2012-07');
+keyDown('j'); //          -> [Jan] 2012
+shouldBeEqualToString('input.value', '2012-01');
+keyDown('x'); //          -> [Jan] 2012
+shouldBeEqualToString('input.value', '2012-01');
+
+beginTest('Typeahead prefix match', '2012-09');
+keyDown('m'); //          -> [Mar] 2012
+shouldBeEqualToString('input.value', '2012-03');
+keyDown('a'); //          -> [Mar] 2012
+shouldBeEqualToString('input.value', '2012-03');
+keyDown('y'); //          -> [May] 2012
+shouldBeEqualToString('input.value', '2012-05');
+keyDown('x'); //          -> [May] 2012
+shouldBeEqualToString('input.value', '2012-05');
+
+beginTest('Typeahead index match', '2012-09');
+keyDown('1'); //          -> [Jan] 2012
+shouldBeEqualToString('input.value', '2012-01');
+keyDown('2'); //          -> [Dec] 2012
+shouldBeEqualToString('input.value', '2012-12');
+keyDown('x'); //          -> [Dec] 2012
+shouldBeEqualToString('input.value', '2012-12');
+
+beginTest('Typeahead should search from current selection', '2012-01');
+keyDown('j'); //          -> [Jun] 2012
+shouldBeEqualToString('input.value', '2012-06');
+
 // The tests in the following block fail on platforms without the
 // lang-attribute-aware-form-control-UI feature.
 input.setAttribute("lang", "he-il");
index b40b4ad..961c5d5 100644 (file)
@@ -1467,6 +1467,7 @@ SET(WebCore_SOURCES
     html/TextInputType.cpp
     html/TimeInputType.cpp
     html/TimeRanges.cpp
+    html/TypeAhead.cpp
     html/URLInputType.cpp
     html/ValidationMessage.cpp
     html/ValidityState.cpp
index 7c660ac..2cd2dbe 100644 (file)
@@ -1,3 +1,52 @@
+2012-11-29  Keishi Hattori  <keishi@webkit.org>
+
+        Better type ahead for DateTimeSymbolicFieldElement
+        https://bugs.webkit.org/show_bug.cgi?id=103031
+
+        Reviewed by Kent Tamura.
+
+        This cuts out the type ahead code that will be used by HTMLSelectElement
+        and DateTimeSymbolicFieldElement into a TypeAhead class. This will
+        improve DateTimeSymbolicFieldElement type ahead which was first
+        character match only, by adding cycling, prefix match and index number
+        match.
+
+        Added tests to month-multiple-fields-keyboard-events.html.
+
+        * GNUmakefile.list.am: Added TypeAhead.{h,cpp}
+        * Target.pri: Ditto.
+        * WebCore.gypi: Ditto.
+        * WebCore.vcproj/WebCore.vcproj: Ditto.
+        * WebCore.xcodeproj/project.pbxproj: Ditto.
+        * CMakeLists.txt: Ditto.
+        * html/HTMLSelectElement.cpp:
+        (WebCore):
+        (WebCore::HTMLSelectElement::HTMLSelectElement):
+        (WebCore::HTMLSelectElement::indexOfSelectedOption): Returns index of current selection.
+        (WebCore::HTMLSelectElement::optionCount): Returns total number of options.
+        (WebCore::HTMLSelectElement::optionAtIndex): Returns option at index.
+        (WebCore::HTMLSelectElement::typeAheadFind): Use TypeAhead.
+        * html/HTMLSelectElement.h:
+        (HTMLSelectElement):
+        * html/TypeAhead.cpp: Added.
+        (WebCore):
+        (WebCore::TypeAhead::TypeAhead):
+        (WebCore::stripLeadingWhiteSpace): Moved from HTMLSelectElement.cpp.
+        (WebCore::TypeAhead::handleEvent): Returns index for match.
+        * html/TypeAhead.h: Added.
+        (WebCore):
+        (TypeAheadDataSource): Provide the data about the options that TypeAhead should match against.
+        (TypeAhead):
+        * html/shadow/DateTimeSymbolicFieldElement.cpp:
+        (WebCore::DateTimeSymbolicFieldElement::DateTimeSymbolicFieldElement):
+        (WebCore::DateTimeSymbolicFieldElement::handleKeyboardEvent):
+        (WebCore::DateTimeSymbolicFieldElement::indexOfSelectedOption):
+        (WebCore):
+        (WebCore::DateTimeSymbolicFieldElement::optionCount):
+        (WebCore::DateTimeSymbolicFieldElement::optionAtIndex):
+        * html/shadow/DateTimeSymbolicFieldElement.h:
+        (DateTimeSymbolicFieldElement):
+
 2012-11-29  Andrei Bucur  <abucur@adobe.com>
 
         [CSS Regions] Fix content node renderers ordering inside the named flow thread
index 8fb6378..25dd388 100644 (file)
@@ -3625,6 +3625,8 @@ webcore_sources += \
        Source/WebCore/html/track/WebVTTToken.h \
        Source/WebCore/html/track/WebVTTTokenizer.h \
        Source/WebCore/html/track/WebVTTTokenizer.cpp \
+       Source/WebCore/html/TypeAhead.cpp \
+       Source/WebCore/html/TypeAhead.h \
        Source/WebCore/html/URLInputType.cpp \
        Source/WebCore/html/URLInputType.h \
        Source/WebCore/html/ValidationMessage.cpp \
index c57d58b..9ca22d9 100644 (file)
@@ -690,6 +690,7 @@ SOURCES += \
     html/TextFieldInputType.cpp \
     html/TextInputType.cpp \
     html/TimeInputType.cpp \
+    html/TypeAhead.cpp \
     html/URLInputType.cpp \
     html/ValidationMessage.cpp \
     html/ValidityState.cpp \
@@ -1844,6 +1845,7 @@ HEADERS += \
     html/StepRange.h \
     html/TextDocument.h \
     html/TimeRanges.h \
+    html/TypeAhead.h \
     html/ValidityState.h \
     html/parser/CSSPreloadScanner.h \
     html/parser/HTMLConstructionSite.h \
index d95a17d..daac4e9 100644 (file)
             'html/ValidityState.h',
             'html/WeekInputType.cpp',
             'html/WeekInputType.h',
+            'html/TypeAhead.cpp',
+            'html/TypeAhead.h',
             'html/canvas/CanvasContextAttributes.cpp',
             'html/canvas/CanvasContextAttributes.h',
             'html/canvas/CanvasGradient.cpp',
index 11d3a22..686e626 100755 (executable)
                                RelativePath="..\html\TimeRanges.h"
                                >
                        </File>
+               <File
+                       RelativePath="..\html\TypeAhead.cpp"
+                       >
+               </File>
+               <File
+                       RelativePath="..\html\TypeAhead.h"
+                       >
+               </File>
                        <File
                                RelativePath="..\html\URLInputType.cpp"
                                >
index b2d5756..669e26e 100644 (file)
                C33EE5C514FB49610002095A /* BaseClickableWithKeyInputType.h in Headers */ = {isa = PBXBuildFile; fileRef = C33EE5C314FB49610002095A /* BaseClickableWithKeyInputType.h */; };
                C348612315FDE21E007A1CC9 /* InputTypeNames.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C348612115FDE21E007A1CC9 /* InputTypeNames.cpp */; };
                C348612415FDE21E007A1CC9 /* InputTypeNames.h in Headers */ = {isa = PBXBuildFile; fileRef = C348612215FDE21E007A1CC9 /* InputTypeNames.h */; };
+               C375D7FD16639519006184AB /* TypeAhead.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C375D7FB16639519006184AB /* TypeAhead.cpp */; };
+               C375D7FE16639519006184AB /* TypeAhead.h in Headers */ = {isa = PBXBuildFile; fileRef = C375D7FC16639519006184AB /* TypeAhead.h */; };
                C37CDEBD149EF2030042090D /* ColorChooserClient.h in Headers */ = {isa = PBXBuildFile; fileRef = C37CDEBC149EF2030042090D /* ColorChooserClient.h */; settings = {ATTRIBUTES = (Private, ); }; };
                C3CF17A415B0063F00276D39 /* IdTargetObserver.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C3CF17A015B0063F00276D39 /* IdTargetObserver.cpp */; };
                C3CF17A515B0063F00276D39 /* IdTargetObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = C3CF17A115B0063F00276D39 /* IdTargetObserver.h */; settings = {ATTRIBUTES = (Private, ); }; };
                C33EE5C314FB49610002095A /* BaseClickableWithKeyInputType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BaseClickableWithKeyInputType.h; sourceTree = "<group>"; };
                C348612115FDE21E007A1CC9 /* InputTypeNames.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = InputTypeNames.cpp; sourceTree = "<group>"; };
                C348612215FDE21E007A1CC9 /* InputTypeNames.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InputTypeNames.h; sourceTree = "<group>"; };
+               C375D7FB16639519006184AB /* TypeAhead.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TypeAhead.cpp; sourceTree = "<group>"; };
+               C375D7FC16639519006184AB /* TypeAhead.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TypeAhead.h; sourceTree = "<group>"; };
                C37CDEBC149EF2030042090D /* ColorChooserClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ColorChooserClient.h; sourceTree = "<group>"; };
                C3CF17A015B0063F00276D39 /* IdTargetObserver.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IdTargetObserver.cpp; sourceTree = "<group>"; };
                C3CF17A115B0063F00276D39 /* IdTargetObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IdTargetObserver.h; sourceTree = "<group>"; };
                                E446139D0CD6331000FADA75 /* TimeRanges.cpp */,
                                E446139E0CD6331000FADA75 /* TimeRanges.h */,
                                E446139F0CD6331000FADA75 /* TimeRanges.idl */,
+                               C375D7FB16639519006184AB /* TypeAhead.cpp */,
+                               C375D7FC16639519006184AB /* TypeAhead.h */,
                                F55B3DA91251F12D003EF269 /* URLInputType.cpp */,
                                F55B3DAA1251F12D003EF269 /* URLInputType.h */,
                                F5A154251279534D00D0B0C0 /* ValidationMessage.cpp */,
                                FB2C15C3165D649D0039C9F8 /* CachedSVGDocumentReference.h in Headers */,
                                31741AAD16636609008A5B7E /* SimulatedClickOptions.h in Headers */,
                                15B8B7C91652C5220036EF55 /* JSWebKitCSSMixFunctionValue.h in Headers */,
+                               C375D7FE16639519006184AB /* TypeAhead.h in Headers */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                                447958051643B4B2001E0A7F /* ParsedContentType.cpp in Sources */,
                                15B8B7C81652C5220036EF55 /* JSWebKitCSSMixFunctionValue.cpp in Sources */,
                                86BA766E166427A8005BE5D1 /* FrameLoadRequest.cpp in Sources */,
+                               C375D7FD16639519006184AB /* TypeAhead.cpp in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
index 23ac57a..f41ee0d 100644 (file)
@@ -64,16 +64,13 @@ using namespace HTMLNames;
 // Upper limit agreed upon with representatives of Opera and Mozilla.
 static const unsigned maxSelectItems = 10000;
 
-static const DOMTimeStamp typeAheadTimeout = 1000;
-
 HTMLSelectElement::HTMLSelectElement(const QualifiedName& tagName, Document* document, HTMLFormElement* form)
     : HTMLFormControlElementWithState(tagName, document, form)
-    , m_lastCharTime(0)
+    , m_typeAhead(this)
     , m_size(0)
     , m_lastOnChangeIndex(-1)
     , m_activeSelectionAnchorIndex(-1)
     , m_activeSelectionEndIndex(-1)
-    , m_repeatingChar(0)
     , m_isProcessingUserDrivenChange(false)
     , m_multiple(false)
     , m_activeSelectionState(false)
@@ -1474,82 +1471,34 @@ int HTMLSelectElement::lastSelectedListIndex() const
     return -1;
 }
 
-static String stripLeadingWhiteSpace(const String& string)
+int HTMLSelectElement::indexOfSelectedOption() const
 {
-    int length = string.length();
-
-    int i;
-    for (i = 0; i < length; ++i) {
-        if (string[i] != noBreakSpace && (string[i] <= 0x7F ? !isASCIISpace(string[i]) : (direction(string[i]) != WhiteSpaceNeutral)))
-            break;
-    }
-
-    return string.substring(i, length - i);
+    return optionToListIndex(selectedIndex());
 }
 
-void HTMLSelectElement::typeAheadFind(KeyboardEvent* event)
+int HTMLSelectElement::optionCount() const
 {
-    if (event->timeStamp() < m_lastCharTime)
-        return;
-
-    DOMTimeStamp delta = event->timeStamp() - m_lastCharTime;
-    m_lastCharTime = event->timeStamp();
-
-    UChar c = event->charCode();
-
-    String prefix;
-    int searchStartOffset = 1;
-    if (delta > typeAheadTimeout) {
-        prefix = String(&c, 1);
-        m_typedString = prefix;
-        m_repeatingChar = c;
-    } else {
-        m_typedString.append(c);
-
-        if (c == m_repeatingChar) {
-            // The user is likely trying to cycle through all the items starting
-            // with this character, so just search on the character.
-            prefix = String(&c, 1);
-        } else {
-            m_repeatingChar = 0;
-            prefix = m_typedString;
-            searchStartOffset = 0;
-        }
-    }
+    return listItems().size();
+}
 
+String HTMLSelectElement::optionAtIndex(int index) const
+{
     const Vector<HTMLElement*>& items = listItems();
-    int itemCount = items.size();
-    if (itemCount < 1)
-        return;
+    
+    HTMLElement* element = items[index];
+    if (!element->hasTagName(optionTag) || toHTMLOptionElement(element)->disabled())
+        return String();
+    return toHTMLOptionElement(element)->textIndentedToRespectGroupLabel();
+}
 
-    int selected = selectedIndex();
-    int index = optionToListIndex(selected >= 0 ? selected : 0) + searchStartOffset;
+void HTMLSelectElement::typeAheadFind(KeyboardEvent* event)
+{
+    int index = m_typeAhead.handleEvent(event, TypeAhead::MatchPrefix | TypeAhead::CycleFirstChar);
     if (index < 0)
         return;
-    index %= itemCount;
-
-    // Compute a case-folded copy of the prefix string before beginning the search for
-    // a matching element. This code uses foldCase to work around the fact that
-    // String::startWith does not fold non-ASCII characters. This code can be changed
-    // to use startWith once that is fixed.
-    String prefixWithCaseFolded(prefix.foldCase());
-    for (int i = 0; i < itemCount; ++i, index = (index + 1) % itemCount) {
-        HTMLElement* element = items[index];
-        if (!element->hasTagName(optionTag) || toHTMLOptionElement(element)->disabled())
-            continue;
-
-        // Fold the option string and check if its prefix is equal to the folded prefix.
-        String text = toHTMLOptionElement(element)->textIndentedToRespectGroupLabel();
-        if (stripLeadingWhiteSpace(text).foldCase().startsWith(prefixWithCaseFolded)) {
-            selectOption(listToOptionIndex(index), DeselectOtherOptions | DispatchChangeEvent | UserDriven);
-            if (!usesMenuList())
-                listBoxOnChange();
-
-            setOptionsChangedOnRenderer();
-            setNeedsStyleRecalc();
-            return;
-        }
-    }
+    selectOption(listToOptionIndex(index), DeselectOtherOptions | DispatchChangeEvent | UserDriven);
+    if (!usesMenuList())
+        listBoxOnChange();
 }
 
 Node::InsertionNotificationRequest HTMLSelectElement::insertedInto(ContainerNode* insertionPoint)
index 6d88da1..a2f2650 100644 (file)
 #include "Event.h"
 #include "HTMLFormControlElementWithState.h"
 #include "HTMLOptionsCollection.h"
+#include "TypeAhead.h"
 #include <wtf/Vector.h>
 
 namespace WebCore {
 
 class HTMLOptionElement;
 
-class HTMLSelectElement : public HTMLFormControlElementWithState {
+class HTMLSelectElement : public HTMLFormControlElementWithState, public TypeAheadDataSource {
 public:
     static PassRefPtr<HTMLSelectElement> create(const QualifiedName&, Document*, HTMLFormElement*);
 
@@ -103,7 +104,7 @@ public:
     
     // For use in the implementation of HTMLOptionElement.
     void optionSelectionStateChanged(HTMLOptionElement*, bool optionIsSelected);
-    
+
 protected:
     HTMLSelectElement(const QualifiedName&, Document*, HTMLFormElement*);
 
@@ -181,17 +182,20 @@ private:
     virtual void childrenChanged(bool changedByParser = false, Node* beforeChange = 0, Node* afterChange = 0, int childCountDelta = 0);
     virtual bool areAuthorShadowsAllowed() const OVERRIDE { return false; }
 
+    // TypeAheadDataSource functions.
+    virtual int indexOfSelectedOption() const OVERRIDE;
+    virtual int optionCount() const OVERRIDE;
+    virtual String optionAtIndex(int index) const OVERRIDE;
+
     // m_listItems contains HTMLOptionElement, HTMLOptGroupElement, and HTMLHRElement objects.
     mutable Vector<HTMLElement*> m_listItems;
     Vector<bool> m_lastOnChangeSelection;
     Vector<bool> m_cachedStateForActiveSelection;
-    DOMTimeStamp m_lastCharTime;
-    String m_typedString;
+    TypeAhead m_typeAhead;
     int m_size;
     int m_lastOnChangeIndex;
     int m_activeSelectionAnchorIndex;
     int m_activeSelectionEndIndex;
-    UChar m_repeatingChar;
     bool m_isProcessingUserDrivenChange;
     bool m_multiple;
     bool m_activeSelectionState;
diff --git a/Source/WebCore/html/TypeAhead.cpp b/Source/WebCore/html/TypeAhead.cpp
new file mode 100644 (file)
index 0000000..d79d7c0
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
+ * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
+ *           (C) 1999 Antti Koivisto (koivisto@kde.org)
+ *           (C) 2001 Dirk Mueller (mueller@kde.org)
+ * Copyright (C) 2004, 2005, 2006, 2007, 2009, 2010, 2011 Apple Inc. All rights reserved.
+ *           (C) 2006 Alexey Proskuryakov (ap@nypop.com)
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ * Copyright (C) 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "config.h"
+#include "TypeAhead.h"
+
+#include "KeyboardEvent.h"
+#include <wtf/unicode/CharacterNames.h>
+#include <wtf/unicode/Unicode.h>
+
+using namespace WTF::Unicode;
+
+namespace WebCore {
+
+TypeAhead::TypeAhead(TypeAheadDataSource* dataSource)
+    : m_dataSource(dataSource)
+    , m_lastTypeTime(0)
+    , m_repeatingChar(0)
+{
+}
+
+static const DOMTimeStamp typeAheadTimeout = 1000;
+
+static String stripLeadingWhiteSpace(const String& string)
+{
+    unsigned length = string.length();
+
+    unsigned i;
+    for (i = 0; i < length; ++i) {
+        if (string[i] != noBreakSpace && (string[i] <= 0x7F ? !isASCIISpace(string[i]) : (direction(string[i]) != WhiteSpaceNeutral)))
+            break;
+    }
+
+    return string.substring(i, length - i);
+}
+
+int TypeAhead::handleEvent(KeyboardEvent* event, MatchModeFlags matchMode)
+{
+    if (event->timeStamp() < m_lastTypeTime)
+        return -1;
+
+    int optionCount = m_dataSource->optionCount();
+    DOMTimeStamp delta = event->timeStamp() - m_lastTypeTime;
+    m_lastTypeTime = event->timeStamp();
+
+    UChar c = event->charCode();
+
+    if (delta > typeAheadTimeout)
+        m_buffer.clear();
+    m_buffer.append(c);
+
+    if (optionCount < 1)
+        return -1;
+
+    int searchStartOffset = 1;
+    String prefix;
+    if (matchMode & CycleFirstChar && c == m_repeatingChar) {
+        // The user is likely trying to cycle through all the items starting
+        // with this character, so just search on the character.
+        prefix = String(&c, 1);
+        m_repeatingChar = c;
+    } else if (matchMode & MatchPrefix) {
+        prefix = m_buffer.toString();
+        if (m_buffer.length() > 1) {
+            m_repeatingChar = 0;
+            searchStartOffset = 0;
+        } else
+            m_repeatingChar = c;
+    }
+
+    if (!prefix.isEmpty()) {
+        int selected = m_dataSource->indexOfSelectedOption();
+        int index = (selected < 0 ? 0 : selected) + searchStartOffset;
+        index %= optionCount;
+
+        // Compute a case-folded copy of the prefix string before beginning the search for
+        // a matching element. This code uses foldCase to work around the fact that
+        // String::startWith does not fold non-ASCII characters. This code can be changed
+        // to use startWith once that is fixed.
+        String prefixWithCaseFolded(prefix.foldCase());
+        for (int i = 0; i < optionCount; ++i, index = (index + 1) % optionCount) {
+            // Fold the option string and check if its prefix is equal to the folded prefix.
+            String text = m_dataSource->optionAtIndex(index);
+            if (stripLeadingWhiteSpace(text).foldCase().startsWith(prefixWithCaseFolded))
+                return index;
+        }
+    }
+
+    if (matchMode & MatchIndex) {
+        bool ok = false;
+        int index = m_buffer.toString().toInt(&ok);
+        if (index > 0 && index <= optionCount)
+            return index - 1;
+    }
+    return -1;
+}
+
+} // namespace WebCore
diff --git a/Source/WebCore/html/TypeAhead.h b/Source/WebCore/html/TypeAhead.h
new file mode 100644 (file)
index 0000000..e15d63d
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1.  Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ * 2.  Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef TypeAhead_h
+#define TypeAhead_h
+
+#include "DOMTimeStamp.h"
+#include <wtf/text/StringBuilder.h>
+#include <wtf/text/WTFString.h>
+
+namespace WebCore {
+
+class KeyboardEvent;
+
+class TypeAheadDataSource {
+public:
+    virtual ~TypeAheadDataSource() { }
+
+    virtual int indexOfSelectedOption() const = 0;
+    virtual int optionCount() const = 0;
+    virtual String optionAtIndex(int index) const = 0;
+};
+
+class TypeAhead {
+public:
+    TypeAhead(TypeAheadDataSource*);
+
+    enum ModeFlag {
+        MatchPrefix = 1 << 0,
+        CycleFirstChar = 1 << 1,
+        MatchIndex = 1 << 2,
+    };
+    typedef unsigned MatchModeFlags;
+
+    // Returns the index for the matching option.
+    int handleEvent(KeyboardEvent*, MatchModeFlags);
+
+private:
+    TypeAheadDataSource* m_dataSource;
+    DOMTimeStamp m_lastTypeTime;
+    UChar m_repeatingChar;
+    MatchModeFlags m_matchMode;
+    StringBuilder m_buffer;
+};
+
+} // namespace WebCore
+
+#endif // TypeAhead_h
index 8ef4dac..3e83509 100644 (file)
@@ -55,6 +55,7 @@ DateTimeSymbolicFieldElement::DateTimeSymbolicFieldElement(Document* document, F
     , m_symbols(symbols)
     , m_visibleEmptyValue(makeVisibleEmptyValue(symbols))
     , m_selectedIndex(-1)
+    , m_typeAhead(this)
 {
     ASSERT(!symbols.isEmpty());
     setHasCustomCallbacks();
@@ -82,12 +83,11 @@ void DateTimeSymbolicFieldElement::handleKeyboardEvent(KeyboardEvent* keyboardEv
         return;
 
     keyboardEvent->setDefaultHandled();
-    for (unsigned index = 0; index < m_symbols.size(); ++index) {
-        if (!m_symbols[index].isEmpty() && WTF::Unicode::toLower(m_symbols[index][0]) == charCode) {
-            setValueAsInteger(index, DispatchEvent);
-            return;
-        }
-    }
+
+    int index = m_typeAhead.handleEvent(keyboardEvent, TypeAhead::MatchPrefix | TypeAhead::CycleFirstChar | TypeAhead::MatchIndex);
+    if (index < 0)
+        return;
+    setValueAsInteger(index, DispatchEvent);
 }
 
 bool DateTimeSymbolicFieldElement::hasValue() const
@@ -150,6 +150,21 @@ String DateTimeSymbolicFieldElement::visibleValue() const
     return hasValue() ? m_symbols[m_selectedIndex] : visibleEmptyValue();
 }
 
+int DateTimeSymbolicFieldElement::indexOfSelectedOption() const
+{
+    return m_selectedIndex;
+}
+
+int DateTimeSymbolicFieldElement::optionCount() const
+{
+    return m_symbols.size();
+}
+
+String DateTimeSymbolicFieldElement::optionAtIndex(int index) const
+{
+    return m_symbols[index];
+}
+
 } // namespace WebCore
 
 #endif
index 5c6b05a..aa6c8da 100644 (file)
 
 #if ENABLE(INPUT_MULTIPLE_FIELDS_UI)
 #include "DateTimeFieldElement.h"
+#include "TypeAhead.h"
 
 namespace WebCore {
 
 // DateTimeSymbolicFieldElement represents non-numeric field of data time
 // format, such as: AM/PM, and month.
-class DateTimeSymbolicFieldElement : public DateTimeFieldElement {
+class DateTimeSymbolicFieldElement : public DateTimeFieldElement, public TypeAheadDataSource {
     WTF_MAKE_NONCOPYABLE(DateTimeSymbolicFieldElement);
 
 protected:
@@ -59,12 +60,18 @@ private:
     virtual String value() const OVERRIDE FINAL;
     virtual String visibleValue() const OVERRIDE FINAL;
 
+    // TypeAheadDataSource functions.
+    virtual int indexOfSelectedOption() const OVERRIDE;
+    virtual int optionCount() const OVERRIDE;
+    virtual String optionAtIndex(int index) const OVERRIDE;
+
     const Vector<String> m_symbols;
 
     // We use AtomicString to share visible empty value among multiple
     // DateTimeEditElements in the page.
     const AtomicString m_visibleEmptyValue;
     int m_selectedIndex;
+    TypeAhead m_typeAhead;
 };
 
 } // namespace WebCore