Lecaw

Учимся создавать Web-сайты

Форма с интерфейсом на естественном языке с настраиваемыми элементами ввода (LUI) \ Обработка естественного языка

May 22, 2013
CSS
99hits

Есть что-то действительно интересное в интерфейсах, построенных на естественном языке и после посещения невероятного Escape Flight, мы решили сделать нечто подобное с формами пользовательских элементов. Идея состоит в том, чтобы превратить классическую форму в такую, которая обрабатывает естественный язык, для получения информации от пользователя. Для этого мы создадим небольшую фразу, где некоторые слова и её части – элементы выбора и ввода текста. Преобразуем элементы выбора таким образом, чтобы мы могли настроить их.

ДЕМО Исходные файлы

 

Формы на естественном языке подходят не для всех UI. У них есть несколько ограничений, но они могут быть более привлекательными для пользователей, если используются в правильном месте и правильным способом. Так или иначе, мы решили поэкспериментировать и поделиться результатом.

HTML разметка

HTML структура будет состоять из формы и нескольких элементов выбора и ввода текста:

<form id="nl-form" class="nl-form">
    I feel to eat 
    <select>
        <option value="1" selected>any food</option>
        <option value="2">Indian</option>
        <option value="3">French</option>
        <option value="4">Japanese</option>
        <option value="2">Italian</option>
    </select>
    <br />in a
    <select>
        <option value="1" selected>standard</option>
        <option value="2">fancy</option>
        <option value="3">hip</option>
        <option value="4">traditional</option>
        <option value="2">fine</option>
    </select>
    restaurant
    <br />at 
    <select>
        <option value="1" selected>anytime</option>
        <option value="1">7 p.m.</option>
        <option value="2">8 p.m.</option>
        <option value="3">9 p.m.</option>
    </select>
    in <input type="text" value="" placeholder="any city" data-subline="For example: <em>Los Angeles</em> or <em>New York</em>"/>
    <div class="nl-submit-wrap">
        <button class="nl-submit" type="submit">Find a restaurant</button>
    </div>
    <div class="nl-overlay"></div>
</form>

Мы хотим преобразовать элементы выбора в виде выпадающего списка:

<div class="nl-field nl-dd">
    <a class="nl-field-toggle">any food</a>
    <ul>
        <li class="nl-dd-checked">any food</li>
        <li>Indian</li>
        <li>French</li>
        <li>Japanese</li>
        <li>Italian</li>
    </ul>
</div>

Изначально, мы скроем выпадающий список и когда мы щелкнем по переключателю, отобразим список с небольшим переходом. Как только мы щелкаем по одному из элементов списка, мы заменим значение переключателя выбранным элементом.

Элемент ввода текста будет также преобразован:

<div class="nl-field nl-ti-text">
    <a class="nl-field-toggle">any city</a>
    <ul>
        <li class="nl-ti-input">
            <input type="text" value="" placeholder="any city" />
            <button class="nl-field-go">Go</button>
        </li>
        <li class="nl-ti-example">For example: <em>Los Angeles</em> or <em>New York</em></li>
    </ul>
</div>

CSS

Для начала добавим иконки:

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

Установим общие стили для формы:

.nl-form {
    width: 100%;
    margin: 0.3em auto 0 auto;
    font-size: 4em;
    line-height: 1.5;
}

Давайте очистим стандартный стиль у всех списков:

.nl-form ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

Нормализуем элементы формы и установим общий стиль:

.nl-form input,
.nl-form select,
.nl-form button {
    border: none;
    background: transparent;
    font-family: inherit;
    font-size: inherit;
    color: inherit;
    font-weight: inherit;
    line-height: inherit;
    display: inline-block;
    padding: 0;
    margin: 0;
}

Перейдем к элементу выбора. Помните это - пользовательский класс, который мы устанавливаем для преобразованного выпадающего списка и текстового поля:

.nl-field {
    display: inline-block;
    position: relative;
}

Используя свойство inline-block, сохраним элемент в потоке нашего предложения.

Когда мы щелкаем по переключателю, нам нужно чтобы список раскрылся. Для этого мы должны установить высокий z-index для поля выбора:

.nl-field.nl-field-open {
    z-index: 10000;
}

У самого переключателя, который является видимой частью в предложении, будет следующий стиль:

.nl-field-toggle,
.nl-form input,
.nl-form select  {
    line-height: inherit;
    display: inline-block;
    color: #b14943;
    cursor: pointer;
    border-bottom: 1px dashed #b14943;
}

При отключенном JS, мы будем использовать стиль для стандартной формы.

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

.nl-field ul {
    position: absolute;
    visibility: hidden;
    background: #76C3BD;
    left: -0.5em;
    top: 50%;
    font-size: 80%;
    opacity: 0;
    transform: translateY(-40%) scale(0.9);
    transition: visibility 0s 0.3s, opacity 0.3s, transform 0.3s;
}
 
.nl-field.nl-field-open ul {
    visibility: visible;
    opacity: 1;
    transform: translateY(-50%) scale(1);
    transition: visibility 0s 0s, opacity 0.3s, transform 0.3s;
}

Стиль для элементов выпадающего списка будет следующим:

.nl-field ul li {
    color: #fff;
    position: relative;
}
 
.nl-dd ul li {
    padding: 0 1.5em 0 0.5em;
    cursor: pointer;
    white-space: nowrap;
}
 
.nl-dd ul li.nl-dd-checked {
    color: #478982;
}
 
.no-touch .nl-dd ul li:hover {
    background: rgba(0,0,0,0.05);
}
 
.no-touch .nl-dd ul li:hover:active {
    color: #478982;
}

Давайте определим иконки для некоторых из наших элементов:

.nl-dd ul li.nl-dd-checked:before,
.nl-submit:before,
.nl-field-go:before {
    font-family: 'nlicons';
    speak: none;
    font-style: normal;
    font-weight: normal;
    font-variant: normal;
    text-transform: none;
    -webkit-font-smoothing: antialiased;
}
 
.nl-dd ul li.nl-dd-checked:before {
    content: "\e000";
    position: absolute;
    right: 1em;
    font-size: 50%;
    line-height: 3;
}

У элемента ввода текста будет минимальная ширина:

.nl-ti-text ul {
    min-width: 8em;
}
.nl-ti-text ul li.nl-ti-input input {
    width: 100%;
    padding: 0.2em 2em 0.2em 0.5em;
    border-bottom: none;
    color: #fff;
}

Иконка стрелки будет иметь свойство position: absolute, и будет находится на правой стороне:

.nl-form .nl-field-go {
    position: absolute;
    right: 0;
    top: 0;
    height: 100%;
    cursor: pointer;
    background: rgba(0,0,0,0.1);
    width: 1.8em;
    text-align: center;
    color: transparent;
}

У самой иконки будет следующий стиль:

.nl-field-go:before {
    content: "\e001";
    font-size: 75%;
    color: #fff;
    width: 100%;
    line-height: 2.5;
    display: block;
}

Давайте изменим цвет заполненного текста, чтобы он вписывался в цветовую схему:

input::-webkit-input-placeholder {
    color: rgba(255,255,255,0.8);
}
 
input:active::-webkit-input-placeholder ,
input:focus::-webkit-input-placeholder {
    color: rgba(255,255,255,0.2);
}
 
input::-moz-placeholder {
    color: rgba(255,255,255,0.8);
}
 
input:active::-moz-placeholder,
input:focus::-moz-placeholder {
    color: rgba(255,255,255,0.2);
}
 
input:-ms-input-placeholder {  
    color: rgba(255,255,255,0.8);
}
 
input:active::-ms-input-placeholder ,
input:focus::-ms-input-placeholder {
    color: rgba(255,255,255,0.2);
}

Для текста с описанием элемента, установим наименьший размер шрифта:

.nl-ti-text ul li.nl-ti-example {
    font-size: 40%;
    font-style: italic;
    font-weight: 400;
    padding: 0.4em 1em;
    color: rgba(0,0,0,0.2);
    border-top: 1px dashed rgba(255,255,255,0.7);
}
 
.nl-ti-text ul li.nl-ti-example em {
    color: #fff
}

Давайте стилизуем кнопку ввода и иконку:

.nl-submit-wrap {
    margin-top: 0.4em;
}
 
.nl-form .nl-submit {
    line-height: 3;
    text-transform: uppercase;
    cursor: pointer;
    position: relative;
    background: #76C3BD;
    color: #fff;
    padding: 0 1em 0 0;
    font-size: 40%;
    font-weight: bold;
    letter-spacing: 3px;
}
 
.nl-submit:before {
    content: "\e001";
    color: #fff;
    float: left;
    padding: 0 0.7em;
    margin: 0 0.8em 0 0;
    background: #69B1A9;
}
 
.no-touch .nl-form .nl-submit:hover,
.no-touch .nl-form .nl-submit:active {
    background: #69B1A9;
}
 
.no-touch .nl-form .nl-submit:hover:before {
    background: #58a199;
}

Наложение будет фиксированным, отобразим его тогда, когда один из списков будет открыт, используя общий одноуровневый селектор:

.nl-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.5);
    opacity: 0;
    z-index: 9999;
    visibility: hidden;
    transition: visibility 0s 0.3s, opacity 0.3s;
}
 
.nl-field.nl-field-open ~ .nl-overlay {
    opacity: 1;
    visibility: visible;
    -webkit-transition-delay: 0s;
    -moz-transition-delay: 0s;
    transition-delay: 0s;
}

Добавим некоторые медиа запросы для того, чтобы скорректировать размеры на устройствах с небольшим экраном:

@media screen and (max-width: 45em) {
    .nl-form {
        font-size: 3em;
    }
}
 
@media screen and (max-width: 25em) {
    .nl-form {
        font-size: 2em;
    }
}

JavaScript

Для начала определим объект, представляющий элемент формы:

function NLForm( el ) { 
    // элемент формы
    this.el = el;
    // наложение
    this.overlay = this.el.querySelector( '.nl-overlay' );
    // массив с полями
    this.fields = [];
    // счетчик для каждого поля
    this.fldOpen = -1;
    this._init();
}

Далее создадим некоторую структуру, в которой заменим все элементы выбора и ввода текста. Мы определим объект, который представляет каждое из этих пользовательских полей – NLField.

NLForm.prototype = {
    _init : function() {
        var self = this;
        Array.prototype.slice.call( this.el.querySelectorAll( 'select' ) ).forEach( function( el, i ) {
            self.fldOpen++;
            self.fields.push( new NLField( self, el, 'dropdown', self.fldOpen ) );
        } );
        Array.prototype.slice.call( this.el.querySelectorAll( 'input' ) ).forEach( function( el, i ) {
            self.fldOpen++;
            self.fields.push( new NLField( self, el, 'input', self.fldOpen ) );
        } );
    },
    ...
}
 
function NLField( form, el, type, idx ) {
    this.form = form;
    // оригинальный HTML элемент
    this.elOriginal = el;
    this.pos = idx;
    this.type = type;
    this._create();
    this._initEvents();
}
NLField.prototype = {
    _create : function() {
        if( this.type === 'dropdown' ) {
            this._createDropDown(); 
        }
        else if( this.type === 'input' ) {
            this._createInput();    
        }
    },
    ...
}

Структура будет разной, в зависимости от того, какой тип поля выбран (поле выбора или поле ввода).

NLField.prototype = {
    ...
    _createDropDown : function() {
        var self = this;
        this.fld = document.createElement( 'div' );
        this.fld.className = 'nl-field nl-dd';
        this.toggle = document.createElement( 'a' );
        this.toggle.innerHTML = this.elOriginal.options[ this.elOriginal.selectedIndex ].innerHTML;
        this.toggle.className = 'nl-field-toggle';
        this.optionsList = document.createElement( 'ul' );
        var ihtml = '';
        Array.prototype.slice.call( this.elOriginal.querySelectorAll( 'option' ) ).forEach( function( el, i ) {
            ihtml += self.elOriginal.selectedIndex === i ? '<li class="nl-dd-checked">' + el.innerHTML + '</li>' : '<li>' + el.innerHTML + '</li>';
            // selected index value
            if( self.elOriginal.selectedIndex === i ) {
                self.selectedIdx = i;
            }
        } );
        this.optionsList.innerHTML = ihtml;
        this.fld.appendChild( this.toggle );
        this.fld.appendChild( this.optionsList );
        this.elOriginal.parentNode.insertBefore( this.fld, this.elOriginal );
        this.elOriginal.style.display = 'none';
    },
    _createInput : function() {
        var self = this;
        this.fld = document.createElement( 'div' );
        this.fld.className = 'nl-field nl-ti-text';
        this.toggle = document.createElement( 'a' );
        this.toggle.innerHTML = this.elOriginal.placeholder;
        this.toggle.className = 'nl-field-toggle';
        this.optionsList = document.createElement( 'ul' );
        this.getinput = document.createElement( 'input' );
        this.getinput.setAttribute( 'type', 'text' );
        this.getinput.placeholder = this.elOriginal.placeholder;
        this.getinputWrapper = document.createElement( 'li' );
        this.getinputWrapper.className = 'nl-ti-input';
        this.inputsubmit = document.createElement( 'button' );
        this.inputsubmit.className = 'nl-field-go';
        this.inputsubmit.innerHTML = 'Go';
        this.getinputWrapper.appendChild( this.getinput );
        this.getinputWrapper.appendChild( this.inputsubmit );
        this.example = document.createElement( 'li' );
        this.example.className = 'nl-ti-example';
        this.example.innerHTML = this.elOriginal.getAttribute( 'data-subline' );
        this.optionsList.appendChild( this.getinputWrapper );
        this.optionsList.appendChild( this.example );
        this.fld.appendChild( this.toggle );
        this.fld.appendChild( this.optionsList );
        this.elOriginal.parentNode.insertBefore( this.fld, this.elOriginal );
        this.elOriginal.style.display = 'none';
    },
    ...
}

Для выпадающего списка мы обновим поле со значением, выбранным из списка опций. После того как пользователь введет требуемое значение, он может подтвердить изменения нажав на клавишу Enter, на кнопку c иконкой или просто по наложению, чтобы обновить значение формы.

NLField.prototype = {
    ...
    _initEvents : function() {
        var self = this;
        this.toggle.addEventListener( 'click', function( ev ) { ev.preventDefault(); ev.stopPropagation(); self._open(); } );
        this.toggle.addEventListener( 'touchstart', function( ev ) { ev.preventDefault(); ev.stopPropagation(); self._open(); } );
 
        if( this.type === 'dropdown' ) {
            var opts = Array.prototype.slice.call( this.optionsList.querySelectorAll( 'li' ) );
            opts.forEach( function( el, i ) {
                el.addEventListener( 'click', function( ev ) { ev.preventDefault(); self.close( el, opts.indexOf( el ) ); } );
                el.addEventListener( 'touchstart', function( ev ) { ev.preventDefault(); self.close( el, opts.indexOf( el ) ); } );
            } );
        }
        else if( this.type === 'input' ) {
            this.getinput.addEventListener( 'keydown', function( ev ) {
                if ( ev.keyCode == 13 ) {
                    self.close();
                }
            } );
            this.inputsubmit.addEventListener( 'click', function( ev ) { ev.preventDefault(); self.close(); } );
            this.inputsubmit.addEventListener( 'touchstart', function( ev ) { ev.preventDefault(); self.close(); } );
        }
 
    },
    _open : function() {
        if( this.open ) {
            return false;
        }
        this.open = true;
        this.form.fldOpen = this.pos;
        var self = this;
        this.fld.className += ' nl-field-open';
    },
    close : function( opt, idx ) {
        if( !this.open ) {
            return false;
        }
        this.open = false;
        this.form.fldOpen = -1;
        this.fld.className = this.fld.className.replace(/\b nl-field-open\b/,'');
 
        if( this.type === 'dropdown' ) {
            if( opt ) {
                // удаляем класс nl-dd-checked из предыдущей опции
                var selectedopt = this.optionsList.children[ this.selectedIdx ];
                selectedopt.className = '';
 
                opt.className = 'nl-dd-checked';
                this.toggle.innerHTML = opt.innerHTML;
 
                // обновление выбранного индексного значения
                this.selectedIdx = idx;
                // обновление значения исходного элемента выбора
                this.elOriginal.value = this.elOriginal.children[ this.selectedIdx ].value;
            }
        }
        else if( this.type === 'input' ) {
            this.getinput.blur();
            this.toggle.innerHTML = this.getinput.value.trim() !== '' ? this.getinput.value : this.getinput.placeholder;
            this.elOriginal.value = this.getinput.value;
        }
    }
    ...
}

ДЕМО Исходные файлы

 

Дополнительная информация

Влерий Аликин - веб-разработчик & дизайнер. Соучредитель и член команды Lecaw.

Эл. почта
RATTING:
(0 голосов)