Расширяемая форма поиска на CSS

В последнее время, нас часто стали спрашивать про различные формы поиска и сегодня я хочу поделиться с вами одним из способов создания формы поиска с нуля, для начала взгляните на демонстрационный пример, чтобы понять, с чем мы сегодня будем работать. Цель состоит в том, чтобы получить максимальную совместимость с мобильными устройствами и старыми браузерами (от IE8). На первый взгляд, кажется, что данная форма поиска довольно простая в реализации, но воспользуемся некоторыми хитростями для того, чтобы заставить ее работать должным образом.

Сложность

Итак, что нам нужно:

  • Изначально мы отображаем только иконку поиска.
  • При нажатии, поле поиска должно выскользнуть из под иконки.
  • Компонент должен быть полностью адаптивным.
  • После ввода запроса, должна быть возможность запуска поиска по нажатию клавиши Enter, либо нажав на иконку поиска.
  • Если поле ввода пустое, то при нажатии на иконку поиска, поле должно исчезнуть.
  • Также необходимо чтобы поле ввода исчезало, при нажатии за пределами панели поиска, независимо от того пустое поле ввода или нет.
  • Для улучшенной работы на сенсорных устройствах, мы добавим поддержку сенсорных событий.

HTML разметка

Создадим основной контейнер, форму, текст, добавим несколько элементов ввода и иконку:

<div id="sb-search" class="sb-search">
    <form>
        <input class="sb-search-input" placeholder="Enter your search term..." type="search" value="" name="search" id="search">
        <input class="sb-search-submit" type="submit" value="">
        <span class="sb-icon-search"></span>
    </form>
</div>

Обычно, мы использовали псевдо-элементы для создания иконки, но этот способ не подходит для замены элементов, например замена на форму поиска, поэтому в данном случае для иконки мы будем использовать элемент span.

CSS

Следуя нашим требованиям, мы должны убедиться, что у нас видна кнопка с иконкой поиска. Все остальное должны быть скрыто. Давайте зададимся вопросом - что происходит, когда мы расширяем строку поиска (основную оболочку). Как мы это делаем? Воспользуемся свойством overflow: hidden и увеличиваем ширину оболочки sb-search.

Итак, начнем со стиля sb-search. Мы сделаем, прижмем её к правой стороне и установим свойство overflow со значением hidden. Ширина должна быть 60px, но так как нам нужно будет анимировать ширину до 100%, может возникнуть проблема в мобильных браузерах, так как там плохо реализован переход от пиксельного значения в процентное. Как правило, они просто пропускают сам момент перехода. Поэтому вместо этого мы определим min-width со значением 60px и ширину 0%.

Мы также установим переход для ширины и свойства -webkit-backface-visibility: hidden, чтобы избежать некоторых трассировок ввода на мобильных браузерах:

.sb-search {
    position: relative;
    margin-top: 10px;
    width: 0%;
    min-width: 60px;
    height: 60px;
    float: right;
    overflow: hidden;
 
    -webkit-transition: width 0.3s;
    -moz-transition: width 0.3s;
    transition: width 0.3s;
 
    -webkit-backface-visibility: hidden;
}

Все что выходит за пределы этого контейнера, будет скрыто.

Теперь, давайте настроим позиционирование строки поиска. Мы установим процентную ширину так, чтобы когда мы расширяем родительский элемент, поле ввода будет расширяться вместе с ним. Установка нужной высоты, размера шрифта и отступов будет гарантировать, что текст будет находиться по центру (свойство line-height не будет работать в IE8, так что давайте установим вместо этого отступы).
Поле ввода с абсолютной позицией, казалось бы, не так уж и необходимо, но это решает малоприятный момент - иногда случается, что при закрытии поиска: поле ввода остается видимым в правой стороне экрана, на небольшой промежуток времени.

.sb-search-input {
    position: absolute;
    top: 0;
    right: 0;
    border: none;
    outline: none;
    background: #fff;
    width: 100%;
    height: 60px;
    margin: 0;
    z-index: 10;
    padding: 20px 65px 20px 20px;
    font-family: inherit;
    font-size: 20px;
    color: #2c3e50;
}
 
input[type="search"].sb-search-input {
    -webkit-appearance: none;
    -webkit-border-radius: 0px;
}

Кроме того, мы удаляем стандартные стили поиска WebKit браузеров.

Давайте определим цвет текста:

.sb-search-input::-webkit-input-placeholder {
    color: #efb480;
}
 
.sb-search-input:-moz-placeholder {
    color: #efb480;
}
 
.sb-search-input::-moz-placeholder {
    color: #efb480;
}
 
.sb-search-input:-ms-input-placeholder {
    color: #efb480;
}

Теперь, перейдем к кнопке отправить и иконке поиска. Мы знаем, что необходимо разместить их в одном месте, так что давайте поместим их в правом углу и установим одинаковые размеры. Так как они будут находиться поверх друг друга, установим свойство position: absolute:

.sb-icon-search,
.sb-search-submit  {
    width: 60px;
    height: 60px;
    display: block;
    position: absolute;
    right: 0;
    top: 0;
    padding: 0;
    margin: 0;
    line-height: 60px;
    text-align: center;
    cursor: pointer;
}

Изначально, должна быть активируемая по щелчку кнопка с иконкой. Затем, после появления поля ввода, необходимо чтобы он также был активируемым по щелчку. Поэтому установим z-index для поля в -1 и сделаем его прозрачным, таким образом, изначально мы всегда будем видеть только иконку поиска:

.sb-search-submit {
    background: #fff; 
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /* IE 8 */
    filter: alpha(opacity=0); /* IE 5-7 */
    opacity: 0;
    color: transparent;
    border: none;
    outline: none;
    z-index: -1;
}

Некоторые разработчики, вместо этого, устанавливают прозрачный фон? Но это не будет работать правильным образом в IE, так как элемент будет не активен. Поэтому вместо этого мы используем сплошной цвет фона и нулевую непрозрачность.

У элемента span будет высокий z-index на начальном этапе. Используем псевдо-элемент :before для иконки:

.sb-icon-search {
    color: #fff;
    background: #e67e22;
    z-index: 90;
    font-size: 22px;
    font-family: 'icomoon';
    speak: none;
    font-style: normal;
    font-weight: normal;
    font-variant: normal;
    text-transform: none;
    -webkit-font-smoothing: antialiased;
}
 
.sb-icon-search:before {
    content: "\e000";
}

Теперь перейдем к стилю ссылки:

@font-face {
    font-family: 'icomoon';
    src:url('../fonts/icomoon/icomoon.eot');
    src:url('../fonts/icomoon/icomoon.eot?#iefix') format('embedded-opentype'),
        url('../fonts/icomoon/icomoon.woff') format('woff'),
        url('../fonts/icomoon/icomoon.ttf') format('truetype'),
        url('../fonts/icomoon/icomoon.svg#icomoon') format('svg');
    font-weight: normal;
    font-style: normal;
}

Мы можем просто установить ширину оболочки sb-search в 100%, при добавлении класса sb-search-open.

.sb-search.sb-search-open,
.no-js .sb-search {
    width: 100%;
}

Давайте поменяем цвет иконки поиска и поместим её под полем, для этого установим z-index с наименьшим значением:

.sb-search.sb-search-open .sb-icon-search, 
.no-js .sb-search .sb-icon-search { 
    background: #da6d0d; color: #fff; z-index: 11; 
}

И наконец, устанавливаем z-index для кнопки отправки с высоким значением, чтобы мы могли её активировать:

.sb-search.sb-search-open .sb-search-submit,
.no-js .sb-search .sb-search-submit {
    z-index: 90;
}

JavaScript

Начнем с переключения класса sb-search-open. При нажатии на основную оболочку (sb-search), добавляем класс sb-search-open, аналогично, при нажатии на кнопку отправки, удаляем этот класс, только если поле поиска пустое. В противном случае мы отправляем форму. Чтобы не вызывать удаление класса, при нажатии на поле поиска (так как наш триггер - вся оболочка), мы должны предотвратить событие нажатия на том элементе. Это означает, что при нажатии на поле поиска не будет происходить вызов события нажатия на его родительском элементе.

;( function( window ) {
     
    function UISearch( el, options ) {  
        this.el = el;
        this.inputEl = el.querySelector( 'form > input.sb-search-input' );
        this._initEvents();
    }
 
    UISearch.prototype = {
        _initEvents : function() {
            var self = this,
                initSearchFn = function( ev ) {
                    if( !classie.has( self.el, 'sb-search-open' ) ) {
                        ev.preventDefault();
                        self.open();
                    }
                    else if( classie.has( self.el, 'sb-search-open' ) && /^\s*$/.test( self.inputEl.value ) ) { 
                        self.close();
                    }
                }
 
            this.el.addEventListener( 'click', initSearchFn );
            this.inputEl.addEventListener( 'click', function( ev ) { ev.stopPropagation(); });
        },
        open : function() {
            classie.add( this.el, 'sb-search-open' );
        },
        close : function() {
            classie.remove( this.el, 'sb-search-open' );
        }
    }
 
    window.UISearch = UISearch;
 
} )( window );

Далее, добавим события для удаления класса sb-search-open, которые будут отвечать за нажатие в любом месте окна браузера, за пределами строки поиска.

;( function( window ) {
     
    function UISearch( el, options ) {  
        this.el = el;
        this.inputEl = el.querySelector( 'form > input.sb-search-input' );
        this._initEvents();
    }
 
    UISearch.prototype = {
        _initEvents : function() {
            var self = this,
                initSearchFn = function( ev ) {
                    ev.stopPropagation();
                     
                    if( !classie.has( self.el, 'sb-search-open' ) ) { 
                        ev.preventDefault();
                        self.open();
                    }
                    else if( classie.has( self.el, 'sb-search-open' ) && /^\s*$/.test( self.inputEl.value ) ) { 
                        self.close();
                    }
                }
 
            this.el.addEventListener( 'click', initSearchFn );
            this.inputEl.addEventListener( 'click', function( ev ) { ev.stopPropagation(); });
        },
        open : function() {
            var self = this;
            classie.add( this.el, 'sb-search-open' );
            var bodyFn = function( ev ) {
                self.close();
                this.removeEventListener( 'click', bodyFn );
            };
            document.addEventListener( 'click', bodyFn );
        },
        close : function() {
            classie.remove( this.el, 'sb-search-open' );
        }
    }
 
    window.UISearch = UISearch;
 
} )( window );

Кроме того, при нажатии на иконку поиска, необходимо сфокусировать поле. Так как это вызывает некоторый судорожный переход в мобильных браузерах (одновременно с этим открывается клавиатура), мы должны этого избежать. Когда мы закрываем строку поиска, добавляем функцию .blur() на иконке.

;( function( window ) {
 
    // http://stackoverflow.com/a/11381730/989439
    function mobilecheck() {
        var check = false;
        (function(a){if(/(android|ipad|playbook|silk|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
        return check;
    }
 
    !String.prototype.trim && (String.prototype.trim = function() {
        return this.replace(/^\s+|\s+$/g, '');
    });
     
    function UISearch( el, options ) {  
        this.el = el;
        this.inputEl = el.querySelector( 'form > input.sb-search-input' );
        this._initEvents();
    }
 
    UISearch.prototype = {
        _initEvents : function() {
            var self = this,
                initSearchFn = function( ev ) {
                    ev.stopPropagation();
                    self.inputEl.value = self.inputEl.value.trim();
                     
                    if( !classie.has( self.el, 'sb-search-open' ) ) { 
                        ev.preventDefault();
                        self.open();
                    }
                    else if( classie.has( self.el, 'sb-search-open' ) && /^\s*$/.test( self.inputEl.value ) ) { 
                        self.close();
                    }
                }
 
            this.el.addEventListener( 'click', initSearchFn );
            this.inputEl.addEventListener( 'click', function( ev ) { ev.stopPropagation(); });
        },
        open : function() {
            var self = this;
            classie.add( this.el, 'sb-search-open' );
            if( !mobilecheck() ) {
                this.inputEl.focus();
            }
            var bodyFn = function( ev ) {
                self.close();
                this.removeEventListener( 'click', bodyFn );
            };
            document.addEventListener( 'click', bodyFn );
        },
        close : function() {
            this.inputEl.blur();
            classie.remove( this.el, 'sb-search-open' );
        }
    }
 
    window.UISearch = UISearch;
 
} )( window );

Чтобы все работало гладко на мобильных устройствах, добавим соответствующие сенсорные события. Добавление preventDefault в функцию initSearchFn предотвратит касание и событие клика.

;( function( window ) {
     
    // http://stackoverflow.com/a/11381730/989439
    function mobilecheck() {
        var check = false;
        (function(a){if(/(android|ipad|playbook|silk|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
        return check;
    }
     
    // http://www.jonathantneal.com/blog/polyfills-and-prototypes/
    !String.prototype.trim && (String.prototype.trim = function() {
        return this.replace(/^\s+|\s+$/g, '');
    });
 
    function UISearch( el, options ) {  
        this.el = el;
        this.inputEl = el.querySelector( 'form > input.sb-search-input' );
        this._initEvents();
    }
 
    UISearch.prototype = {
        _initEvents : function() {
            var self = this,
                initSearchFn = function( ev ) {
                    ev.stopPropagation();
                    self.inputEl.value = self.inputEl.value.trim();
                     
                    if( !classie.has( self.el, 'sb-search-open' ) ) { 
                        ev.preventDefault();
                        self.open();
                    }
                    else if( classie.has( self.el, 'sb-search-open' ) && /^\s*$/.test( self.inputEl.value ) ) { 
                        ev.preventDefault();
                        self.close();
                    }
                }
 
            this.el.addEventListener( 'click', initSearchFn );
            this.el.addEventListener( 'touchstart', initSearchFn );
            this.inputEl.addEventListener( 'click', function( ev ) { ev.stopPropagation(); });
            this.inputEl.addEventListener( 'touchstart', function( ev ) { ev.stopPropagation(); } );
        },
        open : function() {
            var self = this;
            classie.add( this.el, 'sb-search-open' );
            if( !mobilecheck() ) {
                this.inputEl.focus();
            }
            var bodyFn = function( ev ) {
                self.close();
                this.removeEventListener( 'click', bodyFn );
                this.removeEventListener( 'touchstart', bodyFn );
            };
            document.addEventListener( 'click', bodyFn );
            document.addEventListener( 'touchstart', bodyFn );
        },
        close : function() {
            this.inputEl.blur();
            classie.remove( this.el, 'sb-search-open' );
        }
    }
 
    window.UISearch = UISearch;
 
} )( window );

Наконец, для браузеров, которые не поддерживают AddEventListener и RemoveEventListener мы используем EventListener Polyfill.

// EventListener | @jon_neal | //github.com/jonathantneal/EventListener
!window.addEventListener && window.Element && (function () {
    function addToPrototype(name, method) {
        Window.prototype[name] = HTMLDocument.prototype[name] = Element.prototype[name] = method;
    }
 
    var registry = [];
 
    addToPrototype("addEventListener", function (type, listener) {
        var target = this;
 
        registry.unshift({
            __listener: function (event) {
                event.currentTarget = target;
                event.pageX = event.clientX + document.documentElement.scrollLeft;
                event.pageY = event.clientY + document.documentElement.scrollTop;
                event.preventDefault = function () { event.returnValue = false };
                event.relatedTarget = event.fromElement || null;
                event.stopPropagation = function () { event.cancelBubble = true };
                event.relatedTarget = event.fromElement || null;
                event.target = event.srcElement || target;
                event.timeStamp = +new Date;
 
                listener.call(target, event);
            },
            listener: listener,
            target: target,
            type: type
        });
 
        this.attachEvent("on" + type, registry[0].__listener);
    });
 
    addToPrototype("removeEventListener", function (type, listener) {
        for (var index = 0, length = registry.length; index < length; ++index) {
            if (registry[index].target == this && registry[index].type == type && registry[index].listener == listener) {
                return this.detachEvent("on" + type, registry.splice(index, 1)[0].__listener);
            }
        }
    });
 
    addToPrototype("dispatchEvent", function (eventObject) {
        try {
            return this.fireEvent("on" + eventObject.type, eventObject);
        } catch (error) {
            for (var index = 0, length = registry.length; index < length; ++index) {
                if (registry[index].target == this && registry[index].type == eventObject.type) {
                    registry[index].call(this, eventObject);
                }
            }
        }
    });
})();
ДЕМО СКАЧАТЬ

Перевод статьи Expanding Search Bar Deconstructed

Тэги: formsearch

Вход

Уважаемый пользователь! Мы обнаружили, что вы используете AdBlock и вынуждены скрыть часть материалов на нашем сайте. Siteacademy существует и развивается за счет доходов от рекламы. Просим внести наш сайт в список исключений или отключить Блокировщик рекламы на нашем сайте.