diff --git a/Gruntfile.js b/Gruntfile.js index 7407bea7..65377317 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,17 +1,17 @@ var semver = require('semver'), f = require('util').format, jsFiles = [ - 'src/js/version.js', - 'src/js/utils.js', - 'src/js/event_target.js', - 'src/js/persistent_storage.js', - 'src/js/request_cache.js', - 'src/js/transport.js', - 'src/js/dataset.js', - 'src/js/input_view.js', - 'src/js/dropdown_view.js', - 'src/js/typeahead_view.js', - 'src/js/typeahead.js' + 'src/version.js', + 'src/utils.js', + 'src/event_target.js', + 'src/persistent_storage.js', + 'src/request_cache.js', + 'src/transport.js', + 'src/dataset.js', + 'src/input_view.js', + 'src/dropdown_view.js', + 'src/typeahead_view.js', + 'src/typeahead.js' ]; module.exports = function(grunt) { @@ -28,41 +28,13 @@ module.exports = function(grunt) { ' */\n\n' ].join('\n'), - less: { - css: { - src: 'src/css/typeahead.css', - dest: '<%= buildDir %>/typeahead.css' - }, - cssmin: { - options: { yuicompress: true }, - src: 'src/css/typeahead.css', - dest: '<%= buildDir %>/typeahead.min.css' - } - }, - concat: { - css: { - options: { - banner: '<%= banner %>', - stripBanners: true - }, - src: '<%= less.css.dest %>', - dest: '<%= less.css.dest %>' - }, - cssmin: { - options: { - banner: '<%= banner %>', - stripBanners: true - }, - src: '<%= less.cssmin.dest %>', - dest: '<%= less.cssmin.dest %>' - }, js: { - src: ['src/js/intro.js', jsFiles, 'src/js/outro.js'], + src: ['src/intro.js', jsFiles, 'src/outro.js'], dest: '<%= buildDir %>/typeahead.js' }, jsmin: { - src: ['src/js/intro.js', jsFiles, 'src/js/outro.js'], + src: ['src/intro.js', jsFiles, 'src/outro.js'], dest: '<%= buildDir %>/typeahead.min.js' } }, @@ -111,10 +83,6 @@ module.exports = function(grunt) { js: { files: jsFiles, tasks: 'build:js' - }, - css: { - files: '<%= less.css.src %>', - tasks: 'build:css' } }, @@ -245,9 +213,7 @@ module.exports = function(grunt) { // ------- grunt.registerTask('default', 'build'); - grunt.registerTask('build', ['build:js', 'build:css']); - grunt.registerTask('build:js', ['concat:js', 'concat:jsmin', 'sed:version', 'uglify']); - grunt.registerTask('build:css', ['less', 'concat:css', 'concat:cssmin']); + grunt.registerTask('build', ['concat:js', 'concat:jsmin', 'sed:version', 'uglify']); grunt.registerTask('server', 'connect:server'); grunt.registerTask('lint', 'jshint'); grunt.registerTask('test', 'jasmine:js'); @@ -258,7 +224,6 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-sed'); grunt.loadNpmTasks('grunt-exec'); - grunt.loadNpmTasks('grunt-contrib-less'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-uglify'); diff --git a/component.json b/component.json index b4f42cac..3a4d8cc3 100644 --- a/component.json +++ b/component.json @@ -2,10 +2,9 @@ "name": "typeahead.js", "version": "0.8.1", "main": [ - "dist/typeahead.css", "dist/typeahead.js" ], "dependencies": { "jquery": "~1.9" } -} \ No newline at end of file +} diff --git a/dist/typeahead.css b/dist/typeahead.css deleted file mode 100644 index 1fa961b0..00000000 --- a/dist/typeahead.css +++ /dev/null @@ -1,72 +0,0 @@ -/*! - * typeahead.js 0.8.1 - * https://github.com/twitter/typeahead - * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT - */ - -.twitter-typeahead { - position: relative !important; - display: inline-block; - *display: inline; - *zoom: 1; -} -.tt-query { - position: relative !important; - /* for unknown reasons, this fixes alignment issues in ie7 */ - - *margin-top: -1px !important; - vertical-align: top !important; - background-color: transparent !important; - /* ie6-8 doesn't fire hover and click events for transparent elements - for a workaround, use a 1x1 transparent gif */ - - background-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7) !important; -} -.tt-hint { - position: absolute !important; - top: 0 !important; - left: 0 !important; - border-color: transparent !important; - -webkit-box-shadow: none !important; - -moz-box-shadow: none !important; - box-shadow: none !important; -} -.tt-dropdown-menu, -.tt-suggestions, -.tt-suggestion { - padding: 0; - margin: 0; - list-style: none; -} -.tt-dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 100; - /* TODO: need default z-index, should be configurable */ - - display: none; -} -.tt-dropdown-menu.tt-is-open { - display: block; -} -.tt-dropdown-menu.tt-is-empty { - display: none; -} -.tt-suggestion { - display: block; - white-space: nowrap; - cursor: pointer; -} -.tt-suggestion * { - white-space: normal; -} -/* rtl support */ -/* ----------- */ -.twitter-typeahead.tt-rtl { - direction: rtl; -} -.twitter-typeahead.tt-rtl .tt-dropdown-menu { - left: auto; - right: 0; -} diff --git a/dist/typeahead.min.css b/dist/typeahead.min.css deleted file mode 100644 index 87b44b65..00000000 --- a/dist/typeahead.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * typeahead.js 0.8.1 - * https://github.com/twitter/typeahead - * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT - */ - -.twitter-typeahead{position:relative!important;display:inline-block;*display:inline;*zoom:1}.tt-query{position:relative!important;*margin-top:-1px!important;vertical-align:top!important;background-color:transparent!important;background-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)!important}.tt-hint{position:absolute!important;top:0!important;left:0!important;border-color:transparent!important;-webkit-box-shadow:none!important;-moz-box-shadow:none!important;box-shadow:none!important}.tt-dropdown-menu,.tt-suggestions,.tt-suggestion{padding:0;margin:0;list-style:none}.tt-dropdown-menu{position:absolute;top:100%;left:0;z-index:100;display:none}.tt-dropdown-menu.tt-is-open{display:block}.tt-dropdown-menu.tt-is-empty{display:none}.tt-suggestion{display:block;white-space:nowrap;cursor:pointer}.tt-suggestion *{white-space:normal}.twitter-typeahead.tt-rtl{direction:rtl}.twitter-typeahead.tt-rtl .tt-dropdown-menu{left:auto;right:0} \ No newline at end of file diff --git a/package.json b/package.json index 3c07b129..52e692bd 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "grunt": "~0.4", "grunt-sed": "~0.1", "grunt-exec": "~0.4", - "grunt-contrib-less": "~0.5", "grunt-contrib-watch": "~0.2", "grunt-contrib-jshint": "~0.1", "grunt-contrib-uglify": "~0.1", @@ -44,4 +43,4 @@ }, "private": true, "version": "0.8.1" -} \ No newline at end of file +} diff --git a/src/css/typeahead.css b/src/css/typeahead.css deleted file mode 100644 index c6a0ee9c..00000000 --- a/src/css/typeahead.css +++ /dev/null @@ -1,79 +0,0 @@ -/* - * typeahead.js - * https://github.com/twitter/typeahead - * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT - */ - -.twitter-typeahead { - position: relative !important; - display: inline-block; - *display: inline; - *zoom: 1; -} - -.tt-query { - position: relative !important; - /* for unknown reasons, this fixes alignment issues in ie7 */ - *margin-top: -1px !important; - vertical-align: top !important; - background-color: transparent !important; - /* ie6-8 doesn't fire hover and click events for transparent elements - for a workaround, use a 1x1 transparent gif */ - background-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7) !important; -} - -.tt-hint { - position: absolute !important; - top: 0 !important; - left: 0 !important; - border-color: transparent !important; - -webkit-box-shadow: none !important; - -moz-box-shadow: none !important; - box-shadow: none !important; -} - -.tt-dropdown-menu, -.tt-suggestions, -.tt-suggestion { - padding: 0; - margin: 0; - list-style: none; -} - -.tt-dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 100; /* TODO: need default z-index, should be configurable */ - display: none; -} - -.tt-dropdown-menu.tt-is-open { - display: block; -} - -.tt-dropdown-menu.tt-is-empty { - display: none; -} - -.tt-suggestion { - display: block; - white-space: nowrap; - cursor: pointer; -} - -.tt-suggestion * { - white-space: normal; -} - -/* rtl support */ -/* ----------- */ - -.twitter-typeahead.tt-rtl { - direction: rtl; -} - -.twitter-typeahead.tt-rtl .tt-dropdown-menu { - left: auto; - right: 0; -} diff --git a/src/js/dataset.js b/src/dataset.js similarity index 100% rename from src/js/dataset.js rename to src/dataset.js diff --git a/src/js/dropdown_view.js b/src/dropdown_view.js similarity index 63% rename from src/js/dropdown_view.js rename to src/dropdown_view.js index 5feffa13..bd866b26 100644 --- a/src/js/dropdown_view.js +++ b/src/dropdown_view.js @@ -5,6 +5,14 @@ */ var DropdownView = (function() { + var html = { + suggestionsList: '' + }, + css = { + suggestionsList: { display: 'block' }, + suggestion: { whiteSpace: 'nowrap', cursor: 'pointer' }, + suggestionChild: { whiteSpace: 'normal' } + }; // constructor // ----------- @@ -12,7 +20,9 @@ var DropdownView = (function() { function DropdownView(o) { utils.bindAll(this); - this.isMouseOverDropdown; + this.isOpen = false; + this.isEmpty = true; + this.isMouseOverDropdown = false; this.$menu = $(o.menu) .on('mouseenter', this._handleMouseenter) @@ -34,19 +44,32 @@ var DropdownView = (function() { }, _handleMouseover: function($e) { + var $suggestion = $($e.currentTarget); + this._getSuggestions().removeClass('tt-is-under-cursor'); - $($e.currentTarget).addClass('tt-is-under-cursor'); + $suggestion.addClass('tt-is-under-cursor'); }, _handleSelection: function($e) { - this.trigger('select', formatDataForSuggestion($($e.currentTarget))); + var $suggestion = $($e.currentTarget); + this.trigger('suggestionSelected', formatDataForSuggestion($suggestion)); + }, + + _show: function() { + // can't use jQuery#show because $menu is a span element we want + // display: block; not dislay: inline; + this.$menu.css('display', 'block'); + }, + + _hide: function() { + this.$menu.hide(); }, _moveCursor: function(increment) { var $suggestions, $cur, nextIndex, $underCursor; - // don't bother moving the cursor if the menu is hidden - if (!this.$menu.hasClass('tt-is-open')) { + // don't bother moving the cursor if the menu is closed or empty + if (!this.isVisible()) { return; } @@ -60,7 +83,7 @@ var DropdownView = (function() { nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1; if (nextIndex === -1) { - this.trigger('cursorOff'); + this.trigger('cursorRemoved'); return; } @@ -71,7 +94,7 @@ var DropdownView = (function() { } $underCursor = $suggestions.eq(nextIndex).addClass('tt-is-under-cursor'); - this.trigger('cursorOn', { value: $underCursor.data('value') }); + this.trigger('cursorMoved', { value: $underCursor.data('value') }); }, _getSuggestions: function() { @@ -81,37 +104,47 @@ var DropdownView = (function() { // public methods // -------------- - hideUnlessMouseIsOverDropdown: function() { + isVisible: function() { + return this.isOpen && !this.isEmpty; + }, + + closeUnlessMouseIsOverDropdown: function() { // this helps detect the scenario a blur event has triggered - // this function. we don't want to hide the menu in that case + // this function. we don't want to close the menu in that case // because it'll prevent the probable associated click event // from being fired if (!this.isMouseOverDropdown) { - this.hide(); + this.close(); } }, - hide: function() { - if (this.$menu.hasClass('tt-is-open')) { + close: function() { + if (this.isOpen) { + this.isOpen = false; + this._hide(); + this.$menu - .removeClass('tt-is-open') .find('.tt-suggestions > .tt-suggestion') .removeClass('tt-is-under-cursor'); - this.trigger('hide'); + this.trigger('closed'); } }, - show: function() { - if (!this.$menu.hasClass('tt-is-open')) { - this.$menu.addClass('tt-is-open'); + open: function() { + if (!this.isOpen) { + this.isOpen = true; + !this.isEmpty && this._show(); - this.trigger('show'); + this.trigger('opened'); } }, - isOpen: function() { - return this.$menu.hasClass('tt-is-open'); + setLanguageDirection: function(dir) { + var ltrCss = { left: '0', right: 'auto' }, + rtlCss = { left: 'auto', right:' 0' }; + + dir === 'ltr' ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss); }, moveCursorUp: function() { @@ -140,15 +173,19 @@ var DropdownView = (function() { renderSuggestions: function(query, dataset, suggestions) { var datasetClassName = 'tt-dataset-' + dataset.name, + $suggestionsList, $dataset = this.$menu.find('.' + datasetClassName), elBuilder, fragment, - el; + $el; // first time rendering suggestions for this dataset if ($dataset.length === 0) { - $dataset = $('
    1. ') + $suggestionsList = $(html.suggestionsList).css(css.suggestionsList); + + $dataset = $('
      ') .addClass(datasetClassName) + .append($suggestionsList) .appendTo(this.$menu); } @@ -158,15 +195,21 @@ var DropdownView = (function() { this.clearSuggestions(dataset.name); if (suggestions.length > 0) { - this.$menu.removeClass('tt-is-empty'); + this.isEmpty = false; + this.isOpen && this._show(); utils.each(suggestions, function(i, suggestion) { elBuilder.innerHTML = dataset.template.render(suggestion); - el = elBuilder.firstChild; - el.setAttribute('data-value', suggestion.value); + $el = $(elBuilder.firstChild) + .css(css.suggestion) + .data('value', suggestion.value); + + $el.children().each(function() { + $(this).css(css.suggestionChild); + }); - fragment.appendChild(el); + fragment.appendChild($el[0]); }); } @@ -174,7 +217,7 @@ var DropdownView = (function() { .data({ query: query, dataset: dataset.name }) .append(fragment); - this.trigger('suggestionsRender'); + this.trigger('suggestionsRendered'); }, clearSuggestions: function(datasetName) { @@ -184,8 +227,10 @@ var DropdownView = (function() { $suggestions.empty(); - // add empty class if the dropdown menu is empty - this._getSuggestions().length === 0 && this.$menu.addClass('tt-is-empty'); + if (this._getSuggestions().length === 0) { + this.isEmpty = true; + this._hide(); + } } }); diff --git a/src/js/event_target.js b/src/event_target.js similarity index 100% rename from src/js/event_target.js rename to src/event_target.js diff --git a/src/js/input_view.js b/src/input_view.js similarity index 94% rename from src/js/input_view.js rename to src/input_view.js index 46144c1d..2ce9e64f 100644 --- a/src/js/input_view.js +++ b/src/input_view.js @@ -60,18 +60,18 @@ var InputView = (function() { // --------------- _handleFocus: function() { - this.trigger('focus'); + this.trigger('focused'); }, _handleBlur: function() { - this.trigger('blur'); + this.trigger('blured'); }, _handleSpecialKeyEvent: function($e) { // which is normalized and consistent (but not for IE) var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode]; - keyName && this.trigger(keyName, $e); + keyName && this.trigger(keyName + 'Keyed', $e); }, _compareQueryToInputValue: function() { @@ -81,11 +81,11 @@ var InputView = (function() { this.query.length !== inputValue.length : false; if (isSameQueryExceptWhitespace) { - this.trigger('whitespaceChange', { value: this.query }); + this.trigger('whitespaceChanged', { value: this.query }); } else if (!isSameQuery) { - this.trigger('queryChange', { value: this.query = inputValue }); + this.trigger('queryChanged', { value: this.query = inputValue }); } }, diff --git a/src/js/intro.js b/src/intro.js similarity index 100% rename from src/js/intro.js rename to src/intro.js diff --git a/src/js/outro.js b/src/outro.js similarity index 100% rename from src/js/outro.js rename to src/outro.js diff --git a/src/js/persistent_storage.js b/src/persistent_storage.js similarity index 100% rename from src/js/persistent_storage.js rename to src/persistent_storage.js diff --git a/src/js/request_cache.js b/src/request_cache.js similarity index 100% rename from src/js/request_cache.js rename to src/request_cache.js diff --git a/src/js/transport.js b/src/transport.js similarity index 100% rename from src/js/transport.js rename to src/transport.js diff --git a/src/js/typeahead.js b/src/typeahead.js similarity index 97% rename from src/js/typeahead.js rename to src/typeahead.js index 95b180e9..a1d53432 100644 --- a/src/js/typeahead.js +++ b/src/typeahead.js @@ -86,7 +86,7 @@ } function compileTemplate(template, engine) { - var wrapper = '
    2. %body
    3. ', + var wrapper = '
      %body
      ', compiledTemplate; if (template) { diff --git a/src/js/typeahead_view.js b/src/typeahead_view.js similarity index 52% rename from src/js/typeahead_view.js rename to src/typeahead_view.js index 0be0a6f0..dc3a71f1 100644 --- a/src/js/typeahead_view.js +++ b/src/typeahead_view.js @@ -5,12 +5,48 @@ */ var TypeaheadView = (function() { - var html = { - wrapper: '', - hint: '', - dropdown: '
        ' - }; + wrapper: '', + hint: '', + dropdown: '' + }, + css = { + wrapper: { + position: 'relative', + display: 'inline-block' + }, + hint: { + position: 'absolute', + top: '0', + left: '0', + borderColor: 'transparent', + boxShadow: 'none' + }, + query: { + position: 'relative', + verticalAlign: 'top', + backgroundColor: 'transparent', + // ie6-8 doesn't fire hover and click events for elements with + // transparent backgrounds, for a workaround, use 1x1 transparent gif + backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)' + }, + dropdown: { + position: 'absolute', + top: '100%', + left: '0', + // TODO: should this be configurable? + zIndex: '100', + display: 'none' + } + }; + + // ie7 specific styling + if (utils.isMsie() && utils.isMsie() <= 7) { + utils.mixin(css.wrapper, { display: 'inline', zoom: '1' }); + // if someone can tell me why this is necessary to align + // the hint with the query in ie7, i'll send you $5 - @JakeHarding + utils.mixin(css.query, { marginTop: '-1px' }); + } // constructor // ----------- @@ -20,40 +56,41 @@ var TypeaheadView = (function() { utils.bindAll(this); - this.$node = wrapInput(o.input); + this.$node = buildDomStructure(o.input); this.datasets = o.datasets; + this.dir = null; $menu = this.$node.find('.tt-dropdown-menu'); $input = this.$node.find('.tt-query'); $hint = this.$node.find('.tt-hint'); this.dropdownView = new DropdownView({ menu: $menu }) - .on('select', this._handleSelection) - .on('cursorOn', this._clearHint) - .on('cursorOn', this._setInputValueToSuggestionUnderCursor) - .on('cursorOff', this._setInputValueToQuery) - .on('cursorOff', this._updateHint) - .on('suggestionsRender', this._updateHint) - .on('show', this._updateHint) - .on('hide', this._clearHint); + .on('suggestionSelected', this._handleSelection) + .on('cursorMoved', this._clearHint) + .on('cursorMoved', this._setInputValueToSuggestionUnderCursor) + .on('cursorRemoved', this._setInputValueToQuery) + .on('cursorRemoved', this._updateHint) + .on('suggestionsRendered', this._updateHint) + .on('opened', this._updateHint) + .on('closed', this._clearHint); this.inputView = new InputView({ input: $input, hint: $hint }) - .on('focus', this._showDropdown) - .on('blur', this._hideDropdown) - .on('blur', this._setInputValueToQuery) - .on('enter', this._handleSelection) - .on('queryChange', this._clearHint) - .on('queryChange', this._clearSuggestions) - .on('queryChange', this._getSuggestions) - .on('whitespaceChange', this._updateHint) - .on('queryChange whitespaceChange', this._showDropdown) - .on('queryChange whitespaceChange', this._setLanguageDirection) - .on('esc', this._hideDropdown) - .on('esc', this._setInputValueToQuery) - .on('tab up down', this._managePreventDefault) - .on('up down', this._moveDropdownCursor) - .on('up down', this._showDropdown) - .on('tab left right', this._autocomplete); + .on('focused', this._openDropdown) + .on('blured', this._closeDropdown) + .on('blured', this._setInputValueToQuery) + .on('enterKeyed', this._handleSelection) + .on('queryChanged', this._clearHint) + .on('queryChanged', this._clearSuggestions) + .on('queryChanged', this._getSuggestions) + .on('whitespaceChanged', this._updateHint) + .on('queryChanged whitespaceChanged', this._openDropdown) + .on('queryChanged whitespaceChanged', this._setLanguageDirection) + .on('escKeyed', this._closeDropdown) + .on('escKeyed', this._setInputValueToQuery) + .on('tabKeyed upKeyed downKeyed', this._managePreventDefault) + .on('upKeyed downKeyed', this._moveDropdownCursor) + .on('upKeyed downKeyed', this._openDropdown) + .on('tabKeyed leftKeyed rightKeyed', this._autocomplete); } utils.mixin(TypeaheadView.prototype, EventTarget, { @@ -67,14 +104,14 @@ var TypeaheadView = (function() { preventDefault = false; switch (e.type) { - case 'tab': + case 'tabKeyed': hint = this.inputView.getHintValue(); inputValue = this.inputView.getInputValue(); preventDefault = hint && hint !== inputValue; break; - case 'up': - case 'down': + case 'upKeyed': + case 'downKeyed': preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey; break; } @@ -83,22 +120,26 @@ var TypeaheadView = (function() { }, _setLanguageDirection: function() { - var dirClassName = 'tt-' + this.inputView.getLanguageDirection(); + var dir = this.inputView.getLanguageDirection(); - if (!this.$node.hasClass(dirClassName)) { - this.$node.removeClass('tt-ltr tt-rtl').addClass(dirClassName); + if (dir !== this.dir) { + this.dir = dir; + this.$node.css('direction', dir); + this.dropdownView.setLanguageDirection(dir); } }, _updateHint: function() { var dataForFirstSuggestion = this.dropdownView.getFirstSuggestion(), hint = dataForFirstSuggestion ? dataForFirstSuggestion.value : null, + dropdownIsVisible = this.dropdownView.isVisible(), + inputHasOverflow = this.inputView.isOverflow(), inputValue, query, beginsWithQuery, match; - if (hint && this.dropdownView.isOpen() && !this.inputView.isOverflow()) { + if (hint && dropdownIsVisible && !inputHasOverflow) { inputValue = this.inputView.getInputValue(); query = inputValue .replace(/\s{2,}/g, ' ') // condense whitespace @@ -129,26 +170,26 @@ var TypeaheadView = (function() { this.inputView.setInputValue(suggestion.value, true); }, - _showDropdown: function() { - this.dropdownView.show(); + _openDropdown: function() { + this.dropdownView.open(); }, - _hideDropdown: function(e) { - this.dropdownView[e.type === 'blur' ? - 'hideUnlessMouseIsOverDropdown' : 'hide'](); + _closeDropdown: function(e) { + this.dropdownView[e.type === 'blured' ? + 'closeUnlessMouseIsOverDropdown' : 'close'](); }, _moveDropdownCursor: function(e) { var $e = e.data; if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) { - this.dropdownView[e.type === 'up' ? + this.dropdownView[e.type === 'upKeyed' ? 'moveCursorUp' : 'moveCursorDown'](); } }, _handleSelection: function(e) { - var byClick = e.type === 'select', + var byClick = e.type === 'suggestionSelected', suggestionData = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor(); @@ -163,7 +204,7 @@ var TypeaheadView = (function() { // focus is not a synchronous event in ie, so we deal with it byClick && utils.isMsie() ? - setTimeout(this.dropdownView.hide, 0) : this.dropdownView.hide(); + setTimeout(this.dropdownView.close, 0) : this.dropdownView.close(); } }, @@ -188,10 +229,10 @@ var TypeaheadView = (function() { _autocomplete: function(e) { var isCursorAtEnd, ignoreEvent, query, hint; - if (e.type === 'right' || e.type === 'left') { + if (e.type === 'rightKeyed' || e.type === 'leftKeyed') { isCursorAtEnd = this.inputView.isCursorAtEnd(); ignoreEvent = this.inputView.getLanguageDirection() === 'ltr' ? - e.type === 'left' : e.type === 'right'; + e.type === 'leftKeyed' : e.type === 'rightKeyed'; if (!isCursorAtEnd || ignoreEvent) { return; } } @@ -207,26 +248,38 @@ var TypeaheadView = (function() { return TypeaheadView; - function wrapInput(input) { - var $input = $(input), - $hint = $(html.hint).css({ - 'background-color': $input.css('background-color') - }); - - if ($input.length === 0) { - return null; - } + function buildDomStructure(input) { + var $wrapper = $(html.wrapper), + $dropdown = $(html.dropdown), + $input = $(input), + $hint = $(html.hint); + + $wrapper = $wrapper.css(css.wrapper); + $dropdown = $dropdown.css(css.dropdown); + + $hint + .attr({ + type: 'text', + autocomplete: 'off', + spellcheck: false, + disabled: true + }) + .css(css.hint) + .css('background', $input.css('background')); + + $input + .addClass('tt-query') + .attr({ autocomplete: 'off', spellcheck: false }) + .css(css.query); // ie7 does not like it when dir is set to auto, // it does not like it one bit try { !$input.attr('dir') && $input.attr('dir', 'auto'); } catch (e) {} return $input - .attr({ autocomplete: 'off', spellcheck: false }) - .addClass('tt-query') - .wrap(html.wrapper) + .wrap($wrapper) .parent() .prepend($hint) - .append(html.dropdown); + .append($dropdown); } })(); diff --git a/src/js/utils.js b/src/utils.js similarity index 96% rename from src/js/utils.js rename to src/utils.js index 15d937f3..96bee3ca 100644 --- a/src/js/utils.js +++ b/src/utils.js @@ -6,7 +6,9 @@ var utils = { isMsie: function() { - return (/msie [\w.]+/i).test(navigator.userAgent); + var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent); + + return match ? parseInt(match[2], 10) : false; }, isString: function(obj) { return typeof obj === 'string'; }, diff --git a/src/js/version.js b/src/version.js similarity index 100% rename from src/js/version.js rename to src/version.js diff --git a/test/dropdown_view_spec.js b/test/dropdown_view_spec.js index e1ade1ce..39c9d2c3 100644 --- a/test/dropdown_view_spec.js +++ b/test/dropdown_view_spec.js @@ -1,15 +1,5 @@ describe('DropdownView', function() { - var fixture = [ - '
          ', - '
        1. ', - '
            ', - '
          1. one
          2. ', - '
          3. two
          4. ', - '
          5. three
          6. ', - '
          ', - '
        2. ', - '
        ' - ].join('\n'); + var fixture = ''; beforeEach(function() { var $fixtures; @@ -18,13 +8,14 @@ describe('DropdownView', function() { $fixtures = $('#jasmine-fixtures'); this.$menu = $fixtures.find('.tt-dropdown-menu'); - this.$testSet = $fixtures.find('.tt-dataset-test > .tt-suggestions'); this.dropdownView = new DropdownView({ menu: this.$menu }); }); - describe('when mouseenter', function() { + describe('on mouseenter', function() { beforeEach(function() { + renderTestDataset(this.dropdownView, true); + this.$menu.mouseleave().mouseenter(); }); @@ -33,7 +24,7 @@ describe('DropdownView', function() { }); }); - describe('when mouseleave', function() { + describe('on mouseleave', function() { beforeEach(function() { this.$menu.mouseenter().mouseleave(); }); @@ -43,48 +34,51 @@ describe('DropdownView', function() { }); }); - describe('when mouseover suggestion', function() { - var spy, $suggestion1, $suggestion2, $suggestion3; - + describe('on suggestion mouseover', function() { beforeEach(function() { - this.dropdownView.on('cursorOn', spy = jasmine.createSpy()); + this.dropdownView.on('cursorMoved', this.spy = jasmine.createSpy()); + + this.$testDataset = renderTestDataset(this.dropdownView, true); - $suggestion1 = this.$testSet.find('.tt-suggestion:nth-child(1)'); - $suggestion2 = this.$testSet.find('.tt-suggestion:nth-child(2)'); - $suggestion3 = this.$testSet.find('.tt-suggestion:nth-child(3)'); + this.$suggestion1 = this.$testDataset.find('.tt-suggestion:nth-child(1)'); + this.$suggestion2 = this.$testDataset.find('.tt-suggestion:nth-child(2)'); + this.$suggestion3 = this.$testDataset.find('.tt-suggestion:nth-child(3)'); // start with suggestion1 highlighted - $suggestion1.addClass('tt-is-under-cursor'); + this.$suggestion1.addClass('tt-is-under-cursor'); - $suggestion2.mouseover(); + this.$suggestion2.mouseover(); }); it('should add tt-is-under-cursor class to suggestion', function() { - expect($suggestion2).toHaveClass('tt-is-under-cursor'); + expect(this.$suggestion2).toHaveClass('tt-is-under-cursor'); }); it('should remove tt-is-under-cursor class from other suggestions', function() { - expect($suggestion1).not.toHaveClass('tt-is-under-cursor'); - expect($suggestion3).not.toHaveClass('tt-is-under-cursor'); + expect(this.$suggestion1).not.toHaveClass('tt-is-under-cursor'); + expect(this.$suggestion3).not.toHaveClass('tt-is-under-cursor'); }); }); - describe('when click suggestion', function() { - var spy, $suggestion; + describe('on suggestion click', function() { beforeEach(function() { - this.dropdownView.on('select', spy = jasmine.createSpy()); + this.dropdownView + .on('suggestionSelected', this.spy = jasmine.createSpy()); - $suggestion = this.$testSet + this.$testDataset = renderTestDataset(this.dropdownView, true); + + this.$suggestion = this.$testDataset .data('query', 'test query') .data('dataset', 'test dataset') - .find('.tt-suggestion:nth-child(1)').click(); + .find('.tt-suggestion:nth-child(1)') + .click(); }); - it('should trigger select', function() { - expect(spy).toHaveBeenCalledWith({ - type: 'select', + it('should trigger suggestionSelected', function() { + expect(this.spy).toHaveBeenCalledWith({ + type: 'suggestionSelected', data: { query: 'test query', dataset: 'test dataset', @@ -94,151 +88,146 @@ describe('DropdownView', function() { }); }); - describe('#hideUnlessMouseIsOverDropdown', function() { + describe('#closeUnlessMouseIsOverDropdown', function() { beforeEach(function() { - spyOn(this.dropdownView, 'hide'); + spyOn(this.dropdownView, 'close'); }); describe('if isMouseOverDropdown is true', function() { beforeEach(function() { this.dropdownView.isMouseOverDropdown = true; - this.dropdownView.hideUnlessMouseIsOverDropdown(); + this.dropdownView.closeUnlessMouseIsOverDropdown(); }); - it('should not call hide', function() { - expect(this.dropdownView.hide).not.toHaveBeenCalled(); + it('should not call close', function() { + expect(this.dropdownView.close).not.toHaveBeenCalled(); }); }); describe('if isMouseOverDropdown is false', function() { beforeEach(function() { this.dropdownView.isMouseOverDropdown = false; - this.dropdownView.hideUnlessMouseIsOverDropdown(); + this.dropdownView.closeUnlessMouseIsOverDropdown(); }); - it('should call hide', function() { - expect(this.dropdownView.hide).toHaveBeenCalled(); + it('should call close', function() { + expect(this.dropdownView.close).toHaveBeenCalled(); }); }); }); - describe('#hide', function() { - var spy; - - beforeEach(function() { - this.dropdownView.on('hide', spy = jasmine.createSpy()); - }); - - describe('if menu has tt-is-open class', function() { + describe('#close', function() { + describe('if open', function() { beforeEach(function() { - this.$menu.addClass('tt-is-open') + renderTestDataset(this.dropdownView, true); + this.dropdownView.on('closed', this.spy = jasmine.createSpy()); + + this.$menu .find('.tt-suggestions > .tt-suggestion') .addClass('.tt-is-under-cursor'); - this.dropdownView.hide(); + this.dropdownView.close(); }); - it('should remove tt-is-open class', function() { - expect(this.$menu).not.toHaveClass('tt-is-open'); + it('should close menu', function() { + expect(this.$menu).toBeHidden(); }); it('should remove tt-is-under-cursor class from suggestions', function() { - var $suggestions = this.$menu.find('.tt-suggestions > .tt-suggestion'); + var $suggestions = this.$menu.find('.tt-suggestion'); + expect($suggestions).not.toHaveClass('tt-is-under-cursor'); }); - it('should trigger hide', function() { - expect(spy).toHaveBeenCalled(); + it('should trigger closed', function() { + expect(this.spy).toHaveBeenCalled(); }); }); - describe('if menu does not have tt-is-open class', function() { + describe('if not open', function() { beforeEach(function() { - this.$menu.removeClass('tt-is-open'); + renderTestDataset(this.dropdownView, false); + this.dropdownView.on('closed', this.spy = jasmine.createSpy()); - this.dropdownView.hide(); + this.dropdownView.close(); }); - it('should not add the tt-is-open class', function() { - expect(this.$menu).not.toHaveClass('tt-is-open'); + it('should keep menu hidden', function() { + expect(this.$menu).toBeHidden(); }); - it('should not trigger hide', function() { - expect(spy).not.toHaveBeenCalled(); + it('should not trigger closed', function() { + expect(this.spy).not.toHaveBeenCalled(); }); }); }); - describe('#show', function() { - var spy; - - beforeEach(function() { - this.dropdownView.on('show', spy = jasmine.createSpy()); - }); - - describe('if menu has tt-is-open class', function() { + describe('#open', function() { + describe('if open', function() { beforeEach(function() { - this.$menu.addClass('tt-is-open'); + renderTestDataset(this.dropdownView, true); + this.dropdownView.on('opened', this.spy = jasmine.createSpy()); - this.dropdownView.show(); + this.dropdownView.open(); }); - it('should not remove tt-is-open class', function() { - expect(this.$menu).toHaveClass('tt-is-open'); + it('should keep menu visible', function() { + expect(this.$menu).toBeVisible(); }); - it('should not trigger show', function() { - expect(spy).not.toHaveBeenCalled(); + it('should not trigger opened', function() { + expect(this.spy).not.toHaveBeenCalled(); }); }); - describe('if menu does not have tt-is-open class', function() { + describe('if not open', function() { beforeEach(function() { - this.$menu.removeClass('tt-is-open'); + renderTestDataset(this.dropdownView, false); + this.dropdownView.on('opened', this.spy = jasmine.createSpy()); - this.dropdownView.show(); + this.dropdownView.open(); }); - it('should add the tt-is-open class', function() { - expect(this.$menu).toHaveClass('tt-is-open'); + it('should make menu visible', function() { + expect(this.$menu).toBeVisible(); }); - it('should trigger show', function() { - expect(spy).toHaveBeenCalled(); + it('should trigger opened', function() { + expect(this.spy).toHaveBeenCalled(); }); }); }); describe('#moveCursorUp', function() { - var cursorOnSpy, cursorOffSpy; - beforeEach(function() { - this.dropdownView.on('cursorOn', cursorOnSpy = jasmine.createSpy()); - this.dropdownView.on('cursorOff', cursorOffSpy = jasmine.createSpy()); + this.dropdownView + .on('cursorMoved', this.cursorMovedSpy = jasmine.createSpy()) + .on('cursorRemoved', this.cursorRemovedSpy = jasmine.createSpy()); }); - describe('if menu is closed', function() { + describe('if not visible', function() { beforeEach(function() { - this.dropdownView.hide(); + renderTestDataset(this.dropdownView, false); + this.dropdownView.moveCursorUp(); }); it('should not move the cursor to any suggestion', function() { - expect(this.$testSet.find('.under-cursor')).not.toExist(); + expect(this.$menu.find('.tt-is-under-cursor')).not.toExist(); }); - it('should not trigger cursorOn', function() { - expect(cursorOnSpy).not.toHaveBeenCalled(); + it('should not trigger cursorMoved', function() { + expect(this.cursorMovedSpy).not.toHaveBeenCalled(); }); - it('should not trigger cursorOff', function() { - expect(cursorOffSpy).not.toHaveBeenCalled(); + it('should not trigger cursorRemoved', function() { + expect(this.cursorRemovedSpy).not.toHaveBeenCalled(); }); }); - describe('if menu is open', function() { + describe('if visible', function() { beforeEach(function() { - this.dropdownView.show(); + renderTestDataset(this.dropdownView, true); }); describe('if no suggestion is under the cursor', function() { @@ -247,21 +236,22 @@ describe('DropdownView', function() { }); it('should move cursor to last suggestion', function() { - var $lastSuggestion = this.$testSet.find('.tt-suggestion').last(), - $suggestionUnderCursor = this.$testSet - .find('.tt-is-under-cursor'); + var $lastSuggestion = this.$menu + .find('.tt-suggestion') + .last(), + $suggestionUnderCursor = this.$menu.find('.tt-is-under-cursor'); expect($lastSuggestion).toBe($suggestionUnderCursor); }); - it('should trigger cursorOn', function() { - expect(cursorOnSpy).toHaveBeenCalled(); + it('should trigger cursorMoved', function() { + expect(this.cursorMovedSpy).toHaveBeenCalled(); }); }); describe('if the last suggestion is under the cursor', function() { beforeEach(function() { - this.$testSet + this.$menu .find('.tt-suggestion') .last() .addClass('tt-is-under-cursor'); @@ -270,9 +260,8 @@ describe('DropdownView', function() { }); it('should move cursor to previous suggestion', function() { - var $suggestionUnderCursor = this.$testSet - .find('.tt-is-under-cursor'), - $prevSuggestion = this.$testSet + var $suggestionUnderCursor = this.$menu.find('.tt-is-under-cursor'), + $prevSuggestion = this.$menu .find('.tt-suggestion') .last() .prev(); @@ -280,14 +269,14 @@ describe('DropdownView', function() { expect($prevSuggestion).toBe($suggestionUnderCursor); }); - it('should trigger cursorOn', function() { - expect(cursorOnSpy).toHaveBeenCalled(); + it('should trigger cursorMoved', function() { + expect(this.cursorMovedSpy).toHaveBeenCalled(); }); }); describe('if the first suggestion is under the cursor', function() { beforeEach(function() { - this.$testSet + this.$menu .find('.tt-suggestion') .first() .addClass('tt-is-under-cursor'); @@ -296,50 +285,48 @@ describe('DropdownView', function() { }); it('should remove the cursor', function() { - var $suggestionUnderCursor = this.$testSet - .find('.tt-is-under-cursor'); + var $suggestionUnderCursor = this.$menu.find('.tt-is-under-cursor'); expect($suggestionUnderCursor).not.toExist(); }); - it('should trigger cursorOff', function() { - expect(cursorOffSpy).toHaveBeenCalled(); + it('should trigger cursorRemoved', function() { + expect(this.cursorRemovedSpy).toHaveBeenCalled(); }); }); }); }); describe('#moveCursorDown', function() { - var cursorOnSpy, cursorOffSpy; - beforeEach(function() { - this.dropdownView.on('cursorOn', cursorOnSpy = jasmine.createSpy()); - this.dropdownView.on('cursorOff', cursorOffSpy = jasmine.createSpy()); + this.dropdownView + .on('cursorMoved', this.cursorMovedSpy = jasmine.createSpy()) + .on('cursorRemoved', this.cursorRemovedSpy = jasmine.createSpy()); }); - describe('if menu is closed', function() { + describe('if not visible', function() { beforeEach(function() { - this.dropdownView.hide(); + renderTestDataset(this.dropdownView, false); this.dropdownView.moveCursorDown(); }); it('should not move the cursor to any suggestion', function() { - expect(this.$testSet.find('.under-cursor')).not.toExist(); + expect(this.$menu.find('.tt-is-under-cursor')).not.toExist(); }); - it('should not trigger cursorOn', function() { - expect(cursorOnSpy).not.toHaveBeenCalled(); + it('should not trigger cursorMoved', function() { + expect(this.cursorMovedSpy).not.toHaveBeenCalled(); }); - it('should not trigger cursorOff', function() { - expect(cursorOffSpy).not.toHaveBeenCalled(); + it('should not trigger cursorRemoved', function() { + expect(this.cursorRemovedSpy).not.toHaveBeenCalled(); }); }); - describe('if menu is open', function() { + describe('if visible', function() { beforeEach(function() { - this.dropdownView.show(); + renderTestDataset(this.dropdownView, true); }); describe('if no suggestion is under the cursor', function() { @@ -348,21 +335,22 @@ describe('DropdownView', function() { }); it('should move cursor to first suggestion', function() { - var $firstSuggestion = this.$testSet.find('.tt-suggestion').first(), - $suggestionUnderCursor = this.$testSet + var $firstSuggestion = this.$menu + .find('.tt-suggestion').first(), + $suggestionUnderCursor = this.$menu .find('.tt-is-under-cursor'); expect($firstSuggestion).toBe($suggestionUnderCursor); }); - it('should trigger cursorOn', function() { - expect(cursorOnSpy).toHaveBeenCalled(); + it('should trigger cursorMoved', function() { + expect(this.cursorMovedSpy).toHaveBeenCalled(); }); }); describe('if the first suggestion is under the cursor', function() { beforeEach(function() { - this.$testSet + this.$menu .find('.tt-suggestion') .first() .addClass('tt-is-under-cursor'); @@ -371,9 +359,8 @@ describe('DropdownView', function() { }); it('should move cursor to next suggestion', function() { - var $suggestionUnderCursor = this.$testSet - .find('.tt-is-under-cursor'), - $nextSuggestion = this.$testSet + var $suggestionUnderCursor = this.$menu.find('.tt-is-under-cursor'), + $nextSuggestion = this.$menu .find('.tt-suggestion') .first() .next(); @@ -381,14 +368,14 @@ describe('DropdownView', function() { expect($nextSuggestion).toBe($suggestionUnderCursor); }); - it('should trigger cursorOn', function() { - expect(cursorOnSpy).toHaveBeenCalled(); + it('should trigger cursorMoved', function() { + expect(this.cursorMovedSpy).toHaveBeenCalled(); }); }); describe('if the last suggestion is under the cursor', function() { beforeEach(function() { - this.$testSet + this.$menu .find('.tt-suggestion') .last() .addClass('tt-is-under-cursor'); @@ -397,20 +384,23 @@ describe('DropdownView', function() { }); it('should remove the cursor', function() { - var $suggestionUnderCursor = this.$testSet - .find('.tt-is-under-cursor'); + var $suggestionUnderCursor = this.$menu.find('.tt-is-under-cursor'); expect($suggestionUnderCursor).not.toExist(); }); - it('should trigger cursorOff', function() { - expect(cursorOffSpy).toHaveBeenCalled(); + it('should trigger cursorRemoved', function() { + expect(this.cursorRemovedSpy).toHaveBeenCalled(); }); }); }); }); describe('#getSuggestionUnderCursor', function() { + beforeEach(function() { + this.$testDataset = renderTestDataset(this.dropdownView, true); + }); + describe('if no suggestion is under the cursor', function() { it('should return null', function() { expect(this.dropdownView.getSuggestionUnderCursor()).toBeNull(); @@ -418,45 +408,50 @@ describe('DropdownView', function() { }); describe('if suggestion is under the cursor', function() { - var $suggestion; - - beforeEach(function() { - $suggestion = this.$testSet - .find('.tt-suggestion') - .first() - .addClass('tt-is-under-cursor'); - }); - it('should return obj with data about suggestion under the cursor', function() { - var suggestionData = this.dropdownView.getSuggestionUnderCursor(); + var $suggestion = this.$menu + .find('.tt-suggestion') + .first() + .addClass('tt-is-under-cursor'), + suggestionData = this.dropdownView.getSuggestionUnderCursor(); expect(suggestionData.value).toBe($suggestion.data('value')); - expect(suggestionData.query).toBe(this.$testSet.data('query')); - expect(suggestionData.dataset).toBe(this.$testSet.data('dataset')); + expect(suggestionData.query).toBe(this.$testDataset.data('query')); + expect(suggestionData.dataset).toBe(this.$testDataset.data('dataset')); }); }); }); describe('#getFirstSuggestion', function() { + beforeEach(function() { + this.$testDataset = renderTestDataset(this.dropdownView, true); + }); + it('should return obj with data about suggestion under the cursor', function() { var $firstSuggestion = this.dropdownView._getSuggestions().first(), suggestionData = this.dropdownView.getFirstSuggestion(); expect(suggestionData.value).toBe($firstSuggestion.data('value')); - expect(suggestionData.query).toBe(this.$testSet.data('query')); - expect(suggestionData.dataset).toBe(this.$testSet.data('dataset')); + expect(suggestionData.query).toBe(this.$testDataset.data('query')); + expect(suggestionData.dataset).toBe(this.$testDataset.data('dataset')); }); }); describe('#renderSuggestions', function() { var template = { - render: function(c) { return '

        ' + c.value + '

        '; } + render: function(c) { + return '
      1. ' + c.value + '

      2. '; + } }, mockNewDataset = { name: 'new', template: template }, mockOldDataset = { name: 'test', template: template }; + beforeEach(function() { + this.$testDataset = renderTestDataset(this.dropdownView, true); + }); + describe('if new dataset', function() { beforeEach(function() { this.dropdownView.renderSuggestions('query', mockNewDataset, []); @@ -469,7 +464,7 @@ describe('DropdownView', function() { describe('if there are no suggestions', function() { beforeEach(function() { - this.dropdownView.on('suggestionsRender', spy = jasmine.createSpy()); + this.dropdownView.on('suggestionsRendered', spy = jasmine.createSpy()); spyOn(this.dropdownView, 'clearSuggestions'); @@ -480,16 +475,15 @@ describe('DropdownView', function() { expect(this.dropdownView.clearSuggestions).toHaveBeenCalledWith('test'); }); - it('should trigger suggestionsRender', function() { + it('should trigger suggestionsRendered', function() { expect(spy).toHaveBeenCalled(); }); }); describe('if there are suggestions', function() { - var spy; - beforeEach(function() { - this.dropdownView.on('suggestionsRender', spy = jasmine.createSpy()); + this.dropdownView + .on('suggestionsRendered', this.spy = jasmine.createSpy()); spyOn(this.dropdownView, 'clearSuggestions').andCallThrough(); @@ -504,31 +498,28 @@ describe('DropdownView', function() { expect(this.dropdownView.clearSuggestions).toHaveBeenCalledWith('test'); }); - it('should remove tt-is-empty class from menu', function() { - expect(this.dropdownView.$menu).not.toHaveClass('tt-is-empty'); - }); - it('should update data values on list', function() { - expect(this.$testSet).toHaveData('query', 'query'); - expect(this.$testSet).toHaveData('dataset', 'test'); + expect(this.$testDataset).toHaveData('query', 'query'); + expect(this.$testDataset).toHaveData('dataset', 'test'); }); it('should overwrite previous suggestions', function() { - var $suggestions = this.$testSet.children(); + var $suggestions = this.$testDataset.children(); expect($suggestions.length).toBe(1); expect($suggestions.first()).toHaveText('i am a value'); expect($suggestions.first()).toHaveData('value', 'i am a value'); }); - it('should trigger suggestionsRender', function() { - expect(spy).toHaveBeenCalled(); + it('should trigger suggestionsRendered', function() { + expect(this.spy).toHaveBeenCalled(); }); }); }); describe('#clearSuggestions', function() { beforeEach(function() { + renderTestDataset(this.dropdownView, true); this.dropdownView.clearSuggestions(); }); @@ -536,8 +527,33 @@ describe('DropdownView', function() { expect(this.$menu.find('.tt-suggestion')).not.toExist(); }); - it('should add tt-is-empty class to menu', function() { - expect(this.$menu).toHaveClass('tt-is-empty'); + it('should close menu', function() { + expect(this.$menu).toBeHidden(); }); }); + + // helper functions + // ---------------- + + function renderTestDataset(view, open) { + var mockQuery = 'test q', + mockDataset = { + name: 'test' , + template: { + render: function(c) { + return '
      3. ' + c.value + '

      4. '; + } + } + }, + mockSuggestions = [ + { value: 'one' }, + { value: 'two' }, + { value: 'three' } + ]; + + view.renderSuggestions(mockQuery, mockDataset, mockSuggestions); + open && view.open(); + + return $('#jasmine-fixtures .tt-dataset-test > .tt-suggestions'); + } }); diff --git a/test/input_view_spec.js b/test/input_view_spec.js index c3c095b5..3678e76f 100644 --- a/test/input_view_spec.js +++ b/test/input_view_spec.js @@ -28,23 +28,25 @@ describe('InputView', function() { describe('when input gains focus', function() { beforeEach(function() { - spyOnEvent(this.$input, 'focus'); + this.inputView.on('focused', this.spy = jasmine.createSpy()); + this.$input.blur().focus(); }); - it('should trigger focus', function() { - expect('focus').toHaveBeenTriggeredOn(this.$input); + it('should trigger focused', function() { + expect(this.spy).toHaveBeenCalled(); }); }); describe('when query loses focus', function() { beforeEach(function() { - spyOnEvent(this.$input, 'blur'); + this.inputView.on('blured', this.spy = jasmine.createSpy()); + this.$input.focus().blur(); }); - it('should trigger blur', function() { - expect('blur').toHaveBeenTriggeredOn(this.$input); + it('should trigger blured', function() { + expect(this.spy).toHaveBeenCalled(); }); }); @@ -57,7 +59,7 @@ describe('InputView', function() { this.spies = {}; keys.forEach(function(key) { - that.inputView.on(key, that.spies[key] = jasmine.createSpy()); + that.inputView.on(key + 'Keyed', that.spies[key] = jasmine.createSpy()); }); }); @@ -68,7 +70,7 @@ describe('InputView', function() { simulateKeyEvent(this.$input, 'keydown', KEY_MAP[key]); }); - it('should trigger ' + key, function() { + it('should trigger ' + key + 'Keyed', function() { expect(this.spies[key]).toHaveBeenCalled(); }); }); @@ -77,8 +79,8 @@ describe('InputView', function() { describe('when input', function() { beforeEach(function() { - this.inputView.on('queryChange', this.qcSpy = jasmine.createSpy()); - this.inputView.on('whitespaceChange', this.wcSpy = jasmine.createSpy()); + this.inputView.on('queryChanged', this.qcSpy = jasmine.createSpy()); + this.inputView.on('whitespaceChanged', this.wcSpy = jasmine.createSpy()); }); describe('if query changed', function() { @@ -89,11 +91,11 @@ describe('InputView', function() { simulateKeyEvent(this.$input, 'input', KEY_MAP.NORMAL); }); - it('should trigger queryChange', function() { + it('should trigger queryChanged', function() { expect(this.qcSpy).toHaveBeenCalled(); }); - it('should not trigger whitespaceChange', function() { + it('should not trigger whitespaceChanged', function() { expect(this.wcSpy).not.toHaveBeenCalled(); }); @@ -110,11 +112,11 @@ describe('InputView', function() { simulateKeyEvent(this.$input, 'input', KEY_MAP.NORMAL); }); - it('should trigger whitespaceChange', function() { + it('should trigger whitespaceChanged', function() { expect(this.wcSpy).toHaveBeenCalled(); }); - it('should not trigger queryChange', function() { + it('should not trigger queryChanged', function() { expect(this.qcSpy).not.toHaveBeenCalled(); }); @@ -131,11 +133,11 @@ describe('InputView', function() { simulateKeyEvent(this.$input, 'input', KEY_MAP.NORMAL); }); - it('should not trigger queryChange', function() { + it('should not trigger queryChanged', function() { expect(this.qcSpy).not.toHaveBeenCalled(); }); - it('should not trigger whitespaceChange', function() { + it('should not trigger whitespaceChanged', function() { expect(this.wcSpy).not.toHaveBeenCalled(); }); }); diff --git a/test/typeahead_view_spec.js b/test/typeahead_view_spec.js index 84d3da6c..74651366 100644 --- a/test/typeahead_view_spec.js +++ b/test/typeahead_view_spec.js @@ -36,9 +36,10 @@ describe('TypeaheadView', function() { // handlers triggered by dropdownView events // ----------------------------------------- - describe('when dropdownView triggers select', function() { + describe('when dropdownView triggers suggestionSelected', function() { beforeEach(function() { - this.dropdownView.trigger('select', { value: 'i am selected' }); + this.dropdownView + .trigger('suggestionSelected', { value: 'i am selected' }); }); it('should update input value', function() { @@ -50,14 +51,14 @@ describe('TypeaheadView', function() { expect(this.inputView.focus).toHaveBeenCalled(); }); - it('should hide dropdown', function() { - expect(this.dropdownView.hide).toHaveBeenCalled(); + it('should close dropdown', function() { + expect(this.dropdownView.close).toHaveBeenCalled(); }); }); - describe('when dropdownView triggers cursorOn', function() { + describe('when dropdownView triggers cursorMoved', function() { beforeEach(function() { - this.dropdownView.trigger('cursorOn', { value: 'i am hint' }); + this.dropdownView.trigger('cursorMoved', { value: 'i am hint' }); }); it('should clear hint', function() { @@ -70,24 +71,24 @@ describe('TypeaheadView', function() { }); }); - describe('when dropdownView triggers cursorOff', function() { + describe('when dropdownView triggers cursorRemoved', function() { it('should reset input value to user query', function() { this.inputView.getQuery.andReturn('san '); - this.dropdownView.trigger('cursorOff'); + this.dropdownView.trigger('cursorRemoved'); expect(this.inputView.setInputValue).toHaveBeenCalledWith('san '); }); - _updateHintSpecHelper('dropdownView', 'cursorOff'); + _updateHintSpecHelper('dropdownView', 'cursorRemoved'); }); - describe('when dropdownView triggers suggestionsRender', function() { - _updateHintSpecHelper('dropdownView', 'suggestionsRender'); + describe('when dropdownView triggers suggestionsRendered', function() { + _updateHintSpecHelper('dropdownView', 'suggestionsRendered'); }); - describe('when dropdownView triggers hide', function() { + describe('when dropdownView triggers closed', function() { beforeEach(function() { - this.dropdownView.trigger('hide'); + this.dropdownView.trigger('closed'); }); it('should clear hint', function() { @@ -98,15 +99,15 @@ describe('TypeaheadView', function() { // handlers triggered by inputView events // -------------------------------------- - describe('when inputView triggers blur', function() { + describe('when inputView triggers blured', function() { beforeEach(function() { this.inputView.getQuery.andReturn('reset'); - this.inputView.trigger('blur'); + this.inputView.trigger('blured'); }); - it('should hide dropdown unless mouse is over it', function() { - expect(this.dropdownView.hideUnlessMouseIsOverDropdown) + it('should close dropdown unless mouse is over it', function() { + expect(this.dropdownView.closeUnlessMouseIsOverDropdown) .toHaveBeenCalled(); }); @@ -115,14 +116,14 @@ describe('TypeaheadView', function() { }); }); - describe('when inputView triggers enter', function() { + describe('when inputView triggers enterKeyed', function() { beforeEach(function() { this.spy = jasmine.createSpy(); this.dropdownView.getSuggestionUnderCursor .andReturn({ value: 'i am selected' }); - this.inputView.trigger('enter', { preventDefault: this.spy }); + this.inputView.trigger('enterKeyed', { preventDefault: this.spy }); }); it('should update input value', function() { @@ -134,94 +135,92 @@ describe('TypeaheadView', function() { expect(this.spy).toHaveBeenCalled(); }); - it('should hide dropdown', function() { - expect(this.dropdownView.hide).toHaveBeenCalled(); + it('should close dropdown', function() { + expect(this.dropdownView.close).toHaveBeenCalled(); }); }); - describe('when inputView triggers whitespaceChange', function() { - _updateHintSpecHelper('inputView', 'whitespaceChange'); + describe('when inputView triggers whitespaceChanged', function() { + _updateHintSpecHelper('inputView', 'whitespaceChanged'); - it('should show the dropdown menu', function() { - this.inputView.trigger('whitespaceChange'); - expect(this.dropdownView.show).toHaveBeenCalled(); + it('should open the dropdown menu', function() { + this.inputView.trigger('whitespaceChanged'); + expect(this.dropdownView.open).toHaveBeenCalled(); }); describe('if language direction has changed', function() { beforeEach(function() { - this.typeaheadView.$node - .removeClass('tt-ltr tt-rtl') - .addClass('tt-ltr'); - + this.typeaheadView.dir = 'ltr'; this.inputView.getLanguageDirection.andReturn('rtl'); - this.inputView.trigger('whitespaceChange'); + + this.inputView.trigger('whitespaceChanged'); }); - it('should update language class name', function() { - expect(this.typeaheadView.$node).toHaveClass('tt-rtl'); - expect(this.typeaheadView.$node).not.toHaveClass('tt-ltr'); + it('should update styling', function() { + expect(this.typeaheadView.$node).toHaveCss({ direction: 'rtl' }); + expect(this.dropdownView.setLanguageDirection) + .toHaveBeenCalledWith('rtl'); }); }); }); - describe('when inputView triggers queryChange', function() { - it('should show the dropdown menu', function() { - this.inputView.trigger('queryChange'); - expect(this.dropdownView.show).toHaveBeenCalled(); + describe('when inputView triggers queryChanged', function() { + it('should open the dropdown menu', function() { + this.inputView.trigger('queryChanged'); + expect(this.dropdownView.open).toHaveBeenCalled(); }); it('should clear hint', function() { - this.inputView.trigger('queryChange'); + this.inputView.trigger('queryChanged'); expect(this.inputView.setHintValue).toHaveBeenCalledWith(''); }); it('should clear suggestions', function() { - this.inputView.trigger('queryChange'); + this.inputView.trigger('queryChanged'); expect(this.dropdownView.clearSuggestions).toHaveBeenCalled(); }); it('should call dropdownView.renderSuggestions for each dataset', function() { - this.inputView.trigger('queryChange'); + this.inputView.trigger('queryChanged'); expect(this.dropdownView.renderSuggestions.callCount).toBe(3); }); describe('if language direction has changed', function() { beforeEach(function() { - this.typeaheadView.$node - .removeClass('tt-ltr tt-rtl') - .addClass('tt-ltr'); - + this.typeaheadView.dir = 'ltr'; this.inputView.getLanguageDirection.andReturn('rtl'); - this.inputView.trigger('queryChange'); + + this.inputView.trigger('queryChanged'); }); - it('should update language class name', function() { - expect(this.typeaheadView.$node).toHaveClass('tt-rtl'); - expect(this.typeaheadView.$node).not.toHaveClass('tt-ltr'); + it('should update styling', function() { + expect(this.typeaheadView.$node).toHaveCss({ direction: 'rtl' }); + expect(this.dropdownView.setLanguageDirection) + .toHaveBeenCalledWith('rtl'); }); }); }); - describe('when inputView triggers focus', function() { + describe('when inputView triggers focused', function() { beforeEach(function() { - this.inputView.trigger('focus'); + this.inputView.trigger('focused'); }); - it('should show the dropdown menu', function() { - expect(this.dropdownView.show).toHaveBeenCalled(); + it('should open the dropdown menu', function() { + expect(this.dropdownView.open).toHaveBeenCalled(); }); }); - describe('when inputView triggers esc', function() { + describe('when inputView triggers escKeyed', function() { beforeEach(function() { this.inputView.getQuery.andReturn('reset'); - this.inputView.trigger('esc'); + this.inputView.trigger('escKeyed'); }); - it('should hide dropdown', function() { - expect(this.dropdownView.hide).toHaveBeenCalled(); + it('should close dropdown', function() { + expect(this.dropdownView.close).toHaveBeenCalled(); }); it('should reset input value to user query', function() { @@ -229,18 +228,17 @@ describe('TypeaheadView', function() { }); }); - describe('when inputView triggers up', function() { - + describe('when inputView triggers upKeyed', function() { describe('if modifier key was pressed', function() { beforeEach(function() { this.$e = $.extend($.Event('keydown'), { keyCode: 38, shiftKey: true }); spyOn(this.$e, 'preventDefault'); - this.inputView.trigger('up', this.$e); + this.inputView.trigger('upKeyed', this.$e); }); - it('should show the dropdown menu', function() { - expect(this.dropdownView.show).toHaveBeenCalled(); + it('should open the dropdown menu', function() { + expect(this.dropdownView.open).toHaveBeenCalled(); }); it('should not prevent default browser behavior', function() { @@ -257,11 +255,11 @@ describe('TypeaheadView', function() { this.$e = $.extend($.Event('keydown'), { keyCode: 38 }); spyOn(this.$e, 'preventDefault'); - this.inputView.trigger('up', this.$e); + this.inputView.trigger('upKeyed', this.$e); }); it('should show the dropdown menu', function() { - expect(this.dropdownView.show).toHaveBeenCalled(); + expect(this.dropdownView.open).toHaveBeenCalled(); }); it('should prevent default browser behavior', function() { @@ -274,18 +272,18 @@ describe('TypeaheadView', function() { }); }); - describe('when inputView triggers down', function() { + describe('when inputView triggers downKeyed', function() { describe('if modifier key was pressed', function() { beforeEach(function() { this.$e = $.extend($.Event('keydown'), { keyCode: 40, shiftKey: true }); spyOn(this.$e, 'preventDefault'); - this.inputView.trigger('down', this.$e); + this.inputView.trigger('downKeyed', this.$e); }); - it('should show the dropdown menu', function() { - expect(this.dropdownView.show).toHaveBeenCalled(); + it('should open the dropdown menu', function() { + expect(this.dropdownView.open).toHaveBeenCalled(); }); it('should not prevent default browser behavior', function() { @@ -302,11 +300,11 @@ describe('TypeaheadView', function() { this.$e = $.extend($.Event('keydown'), { keyCode: 40 }); spyOn(this.$e, 'preventDefault'); - this.inputView.trigger('down', this.$e); + this.inputView.trigger('downKeyed', this.$e); }); - it('should show the dropdown menu', function() { - expect(this.dropdownView.show).toHaveBeenCalled(); + it('should open the dropdown menu', function() { + expect(this.dropdownView.open).toHaveBeenCalled(); }); it('should prevent default browser behavior', function() { @@ -319,7 +317,7 @@ describe('TypeaheadView', function() { }); }); - describe('when inputView triggers tab', function() { + describe('when inputView triggers tabKeyed', function() { beforeEach(function() { this.$e = $.extend($.Event('keydown'), { keyCode: 9 }); spyOn(this.$e, 'preventDefault'); @@ -329,7 +327,7 @@ describe('TypeaheadView', function() { beforeEach(function() { this.inputView.getHintValue.andReturn(''); - this.inputView.trigger('tab', this.$e); + this.inputView.trigger('tabKeyed', this.$e); }); it('should not update input value', function() { @@ -346,7 +344,7 @@ describe('TypeaheadView', function() { this.inputView.getQuery.andReturn('app'); this.inputView.getHintValue.andReturn('apple'); - this.inputView.trigger('tab', this.$e); + this.inputView.trigger('tabKeyed', this.$e); }); it('should update input value', function() { @@ -359,7 +357,7 @@ describe('TypeaheadView', function() { }); }); - describe('when inputView triggers left', function() { + describe('when inputView triggers leftKeyed', function() { beforeEach(function() { this.inputView.getQuery.andReturn('app'); this.inputView.getHintValue.andReturn('apple'); @@ -371,7 +369,7 @@ describe('TypeaheadView', function() { beforeEach(function() { this.inputView.getLanguageDirection.andReturn('ltr'); - this.inputView.trigger('left'); + this.inputView.trigger('leftKeyed'); }); it('should not update input value', function() { @@ -383,7 +381,7 @@ describe('TypeaheadView', function() { beforeEach(function() { this.inputView.getLanguageDirection.andReturn('rtl'); - this.inputView.trigger('left'); + this.inputView.trigger('leftKeyed'); }); it('should update value of input', function() { @@ -395,7 +393,7 @@ describe('TypeaheadView', function() { beforeEach(function() { this.inputView.isCursorAtEnd.andReturn(false); - this.inputView.trigger('left'); + this.inputView.trigger('leftKeyed'); }); it('should not update input value', function() { @@ -404,7 +402,7 @@ describe('TypeaheadView', function() { }); }); - describe('when inputView triggers right', function() { + describe('when inputView triggers rightKeyed', function() { beforeEach(function() { this.inputView.getQuery.andReturn('app'); this.inputView.getHintValue.andReturn('apple'); @@ -416,7 +414,7 @@ describe('TypeaheadView', function() { beforeEach(function() { this.inputView.getLanguageDirection.andReturn('ltr'); - this.inputView.trigger('right'); + this.inputView.trigger('rightKeyed'); }); it('should update input value', function() { @@ -428,7 +426,7 @@ describe('TypeaheadView', function() { beforeEach(function() { this.inputView.getLanguageDirection.andReturn('rtl'); - this.inputView.trigger('right'); + this.inputView.trigger('rightKeyed'); }); it('should not update input value', function() { @@ -440,7 +438,7 @@ describe('TypeaheadView', function() { beforeEach(function() { this.inputView.isCursorAtEnd.andReturn(false); - this.inputView.trigger('right'); + this.inputView.trigger('rightKeyed'); }); it('should not update input value', function() { @@ -465,9 +463,9 @@ describe('TypeaheadView', function() { }); }); - describe('if dropdown menu is closed', function() { + describe('if dropdown menu is not visible', function() { it('should not show hint', function() { - this.dropdownView.isOpen.andReturn(false); + this.dropdownView.isVisible.andReturn(false); this.inputView.getInputValue.andReturn('san '); this.dropdownView.getFirstSuggestion .andReturn({ value: 'desert sand' }); @@ -480,7 +478,7 @@ describe('TypeaheadView', function() { describe('if top suggestion\'s value begins with query', function() { it('should show hint', function() { - this.dropdownView.isOpen.andReturn(true); + this.dropdownView.isVisible.andReturn(true); this.inputView.getInputValue.andReturn('san '); this.dropdownView.getFirstSuggestion .andReturn({ value: 'san francisco' }); @@ -495,7 +493,7 @@ describe('TypeaheadView', function() { describe('if top suggestion\'s value does not begin with query', function() { it('should not show hint', function() { - this.dropdownView.isOpen.andReturn(true); + this.dropdownView.isVisible.andReturn(true); this.inputView.getInputValue.andReturn('san '); this.dropdownView.getFirstSuggestion .andReturn({ value: 'desert sand' });