Заметки с тегом «ui» :: Хранитель заметок

noteskeeper.ru

Персональный журнал для заметок Владимира Кузнецова

Заметки с тегом «ui»

Навигация по ссылкам в списках

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

Список авторов статей в блоге

Каждый элемент в списке содержит имя, фотографию и краткую биографию автора. Имя и фотография обрамлены ссылками на архив статей этого автора.

<ul class="authors">
  <li class="author">
    <div class="author-userpic">
      <a class="author-archive-link" title="Vicki Moyes" href="/author/vicki/">
        <img class="author-pic photo" alt="Vicki Moyes" src="/img/users/vicki-moyes.jpg">
      </a>
    </div>
    <p class="author-title">
      <a href="/author/vicki/" class="author-title-archive-link">Vicki Moyes</a>
    </p>
    <p class="author-bio">Director of Moyes Gliders.
      Organizer of annual Forbes competition.</p>
  </li>
</ul>

Используем tabindex=-1

Элементы, обрамленные ссылками

При переключении фокуса с помощью клавиши «Tab» пользователь каждый раз сначала попадает на ссылку с фотографии, затем на ссылку с имени.

Проектируя интерфейс, первостепенно внимание уделяется пользователям, работающим с мышью или touch-интерфейсом. Кому-то из них привычнее и удобнее кликать по картинкам, а кто-то кликает только по явно обозначенным ссылкам. Поэтому удалить ссылку, обрамляющую фотографию, нельзя. Но зато можно сделать так, чтобы она не участвовала в последовательности переключения фокуса с клавиатуры. Для этого добавим ей атрибут tabindex="-1".

Переходы только по именам авторов

Такой нехитрый прием позволяет значительно улучшить впечатления пользователя от интерфейса и сделать его поведение более предсказуемым.

Одна ссылка

В этом примере два находящиеся рядом объекта (фотография и имя автора) должны вести на одну и ту же страницу. Технически их можно обернуть одной ссылкой. В HTML5 внутри строчного элемента a могут находиться как строчные, так и блочные элементы.

<a href="page.html">
    <div><img src="img1.jpg" alt="Page image"></div>
    <p>Page title</p>
</a>

Фрагмент кода валиден на 100%, только если используется в HTML5 документе.

Порядок элементов виджета Sortable

У виджета jQuery Sortable помимо метода serialize есть еще другой метод, который можно использовать для сохранения порядка сортировки элементов, — toArray. Его отличие заключается в том, что он возвращает массив id элементов, а не просто строку.

<ul id="search-engines">
  <li id="item-1">Яндекс</li>
  <li id="item-3">Bing </li>
  <li id="item-2">Google</li>
</ul>

Получаем массив

var engines = $("#search-engines").sortable("toArray");

В результате в переменной engines будет массив ["item-1", "item-3", "item-2"].

toArray будет полезен, скорее всего, в ситуациях, когда требуется пост-обработка данных (например, фильтрация или переименование ключей) перед сохранением.

Навигация с помощью клавиатуры

Часто Web-приложения используют Javascript для того, чтобы подражать обычным программам и использовать элементы управления, такие как деревья, закладки и т.п. Важную часть взаимодействия пользователя и интерфейса при таком подражании составляет навигация по элементам управления с помощью клавиатуры.

Любой элемент в документе может получить фокус, если у него указан атрибут tabindex. Он не является частью спецификации HTML 4.01, но фактически реализован во всех современных браузерах.

Значение атрибута tabindex

Атрибут может принимать любые числовые значения. Явное указание атрибута позволяет получить фокус с помощью указателя мыши или программно через метод focus(). Отсутствие атрибута устанавливает обычную реакцию элемента. В этом случае фокус могут получить только элементы формы, ссылки и некоторые другие.

Поведение при навигации с помощью клавиши «Tab»

  • Отрицательное

    Элемент игнорируется

  • Нулевое

    Последовательность задается положением элемента в документе

  • Положительное

    Значение явно задает порядок элементов при перемещении фокуса

Отслеживание сфокусированного элемента

События focus и blur могут быть использованы для любого элемента. Но так как нет стандартного интерфейса, для определения получил элемент фокус или нет, то нужно будет самостоятельно обрабатывать и запоминать это состояние.

Не следует предполагать, что фокус может меняться только с помощью клавиатуры или мышки, потому что вспомогательные технологии, такие как «чтение с экрана» могут самостоятельно устанавливать фокус на элементы.

Динамическое изменение атрибута tabIndex

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

Отслеживание фокуса у элемента document

Что касается событий focus и blur, то их может генерировать не только конкретный элемент, но и весь документ в целом. В jQuery 1.4 разработчики добавили синтетические события focusin и focusout для корректной делегации родительским элементам DOM-событий focus и blur соответственно. Прикрепив обработчик этих событий к документу, можно отслеживать активность окна браузера.

Выбор диапазона дат с помощью jQuery UI Datepicker

Для очередного проекта понадобился виджет для выбора даты. Очевидным, по крайней мере для меня, решением было jQuery UI Datepicker. Но когда позже понадобилось выбирать не одну дату, а диапазон, я попал в тупик. В API не было ни какого упоминания о такой возможности. Поисковики все как один выдавали какие-то «самоделки» далеко не первой свежести.

Копнув глубже исходники Datepicker, я все-таки обнаружил возможность выбора диапазона. Реализуется все логика через событие «onSelect».

У экземпляра Datepicker есть флаг stayOpen. Если его установить внутри события, то календарь не закроется после клика, а занесет выбранную дату в поле rangeStart и позволит выбрать еще дату. Когда будет выбран конец диапазона, нужно сбросить флаг stayOpen и обновить содержимое текстового поля.

(function ($) {

    $.fn.daterange = function () {
        // опции
        var opts = $.extend({
            "dateFormat": "dd.mm.yy",
            "changeMonth": false,
            "changeYear": false,
            "numberOfMonths": 2,
            "rangeSeparator": "-"
        }, arguments[0] || {}, {
            // обработчики событий datepicker
            // закрытие
            "onClose": function (dateText, inst) {
                if ($.isFunction(opts.callback)) {
                    opts.callback.apply(this, arguments);
                }
            },
            // выбор даты
            "onSelect": function (dateText, inst) {
                var textStart;
                if (!inst.rangeStart) {
                    inst.stayOpen = true;
                } else {
                    inst.stayOpen = false;
                    textStart = $.datepicker.formatDate(opts.dateFormat, inst.rangeStart);
                    if (textStart !== dateText) {
                        $(this).val(textStart + " " + opts.rangeSeparator + " " + dateText);
                    }
                }
            }
        });

        return this.each(function () {
            var input = $(this);
            if (input.is("input")) {
                input.datepicker(opts);
            }
        });
    };

}(jQuery));

Еще одно событие, которое может пригодиться, это «onClose». Оно будет сгенерировано в момент закрытия календаря. В плагине я просто делегирую все исполнение внешнему обработчику, но на практике мне удобно было добавить туда больше функционала.

Плагин может быть проинициализирован любыми параметрами, которые используются в Datepicker. Так как обработчик события «onClose» переопределяется, то вместо него используется поле callback.

Сглаживание с суб-пиксельной точностью

Каждый пиксель на стандартном LCD-мониторе состоит из трех независимых компонент: красной, зеленой и синей. Из-за маленького размера этих суб-пикселей наш глаз объединяет их в один пиксель со сплошным цветом. Обычно, чтобы обеспечить сглаживание яркость всех цветовых компонент задается одинаковой, что в результате дает оттенки серого. При отображении с суб-пиксельной точностью яркость каждой цветовой компоненты задается независимо, что воспринимается как увеличенная разрешающая способность монитора. Пиксели уже не воспринимаются как одиночные, а связываются со своими соседями, что позволяет плавно произвести сглаживание. Однако по краям глифов при таком сглаживании образуются цветовые искажения.

Сглаживание с суб-пиксельной точностью

Техника сглаживания с суб-пиксельной точностью опирается на идеально ровную сетку пикселей. Эти условия обеспечиваются только на LCD-мониторах. Сетка CRT-мониторов имеет смещение по горизонтали от линии к линии, что делает эту технику крайне сложной для воспроизведения. Даже LCD-мониторы имеются различия в расположении суб-пикселей, которые должны быть учтены. У одних мониторов порядок следования RGB, а у других — BGR.

Обработчики события и функции обратного вызова в jQuery UI

Интересной особенностью jQuery UI для меня стала обработка событий и запуск функций обратного вызова. У обычных событий цепочки функций-обработчиков выполняются от начала и до конца или пока не будут прерваны через метод stopImmediatePropagation. К сожалению, в отличие от YUI 3, в API jQuery не предоставлена возможность определить, когда закончится выполняться эта цепочка. Зато в jQuery UI виджеты генерируют события, в которых такая возможность есть.

При конфигурировании виджета можно задать функцию обратного вызова (callback) для любого события, а так же подписаться на это событие извне. По сути, callback и цепочка обработчиков вызываются одним и тем же приватным методом _trigger. Но callback всегда вызывается последним вне зависимости от результата работы цепочки функций. А так как им обоим передается ссылка на одно и тот же событие, то в callback можно проверить статус события после обработчиков через методы isDefaultPrevented, isPropagationStopped или isImmediatePropagationStopped.

Более того, метод _trigger возвращает значение true, если все функции были выполнены и ни одна из них не вызвала preventDefault. В противном случае он вернет значение false. Это свойство фабрики виджетов тоже можно использовать для управления логикой работы событий.

Простой пример виджета:

$.widget("my.input", {
    _init: function () {
        var me = this;
        // подписываемся на некоторые события элемента input,
        // чтобы отслеживать его изменение
        me.element.find(".input").bind('keyup change blur', function (event) {
            var value = $(this).val(), opts = me.options, data;
            // исколючаем повторяющие события
            if (!opts.inProgress && value != opts.value) {
                try {
                    opts.inProgress = true;
                    data = {'current': value, 'last': opts.value};
                    me._change(data, me._trigger("change", event, data));
                } finally {
                    opts.inProgress = false;
                }
            }
        });
    },

    _change: function (data, status) {
        $(this.element).toggleClass("error", !status);
        this.options.value = data.current;
    }
});

$.extend($.my.input, {
    defaults: {
        value: "",
        inProgress: false
    }
});

$(function () {
    $("#demo")
            .input({'change': function (e, data) {
                // функция обратного вызова
                // будет выполнена самой последней при обработке события
                return data.current <= 1000;
            }})
            .bind('inputchange', function (e, data) {
                // специальный обработчик, который прерывает цепочку событий,
                // если встречается определенная строка — 007
                if (data.current == "007") {
                    e.stopImmediatePropagation();
                    return false;
                }
            })
            .bind('inputchange', function (e, data) {
                // еще один специальный обработчик
                $(".log", this).prepend("<p>" + data.current + "</p>");
            });
});

Этот виджет отслеживает вводимые в поле данные, ведет лог этих данных и осуществляет валидацию. В примере я использую метод stopImmediatePropagation, чтобы прервать выполнение цепочки. Если это специально не требуется, то лучше использовать preventDefault или возвращать false в обработчике.

Итак, если в поле ввести «007», то в лог ничего не запишется, а виджету будет назначен класс «error». Так же этот класс будет назначен, если значение в поле — число, больше 1000.

Метод, возвращающий данные, в jQuery UI

При написании виджетов для jQuery UI важно не забыть объявить метод, который возвращает какие-то данные (getter). По умолчанию фабрика виджетов вернет ссылку на корневой элемент, чтобы обеспечить цепочечные вызовы.

Пусть у нас есть метод getFields, который должен вернуть коллекцию элементов, которые использует виджет.

$.widget("foo.bar", {
    …
    getFields: function () {
            var fields = [];
            $.each(this._fields, function () {
                fields.push(this.element);
            });
            return $(fields);
    }
});

Пока мы не сделаем такое объявление, метод будет возвращать не то, что мы ожидаем.

$.extend($.foo.bar, {
    getter: "getFields",
    defaults: {}
});

Если у виджета несколько таких методов, то их имена при описании разделяются пробелом.

Обновление: В jQuery UI 1.8 изменилась политика в отношении таких методов. Теперь не нужно специально объявлять их. Когда метод возвращает значение undefined, оно заменяется на корневой элемент экземпляра виджета, что позволяет строить вызовы цепочкой. В любом другом случае это значение передается без изменений.

Верстаем формы

Красивые и ровные формы очень легко сверстать без использования таблиц.

Тестовая форма

Предположим, что все поля будут выровнены по левому краю, а надписи к ним располагаются слева и выровнены по правому краю.

Структура HTML, например, может быть такой:

<fieldset>
    <legend>Личные данные</legend>
    <div>
        <label for="lastname">Фамилия</label>
        <input type="text" value="" id="lastname" name="lastname"/>
    </div>
    <div>
        <label for="firstname">Имя</label>
        <input type="text" value="" id="firstname" name="firstname"/>
    </div>
    <div>
        <label for="middlename">Отчество</label>
        <input type="text" value="" id="middlename" name="middlename"/>
    </div>
    <div>
        <label for="birthday">Дата рождения</label>
        <input type="text" value="" id="birthday" name="birthday" class="date short"/>
        <p>день / месяц / год</p>
    </div>
    <div class="singleline">
        <label for="phone">Телефон</label>
        <span>+</span>
        <input type="text" value="" id="phone" name="phonecountry" class="compact"/>
        <span>(</span>
        <input type="text" value="" name="phonecity" class="compact"/>
        <span>)</span>
        <input type="text" value="" name="phonenumber" class="short"/>
        <p>код страны ( код города ) номер</p>
    </div>
</fieldset>

Не важно где будут находиться надписи в коде — до или после поля, так как они будут абсолютно позиционироваться с левой стороны. Задать отступ, где расположиться надпись можно либо с помощью свойства margin-left у элемента input, либо с помощью свойства padding-left у элемента div. Второй способ, на мой взгляд, предпочтительнее, так как комментарии к полю (элемент p) будут естественным образом выравниваться относительно него и не потребуется дополнительного оформления.

div.singleline {
    white-space: nowrap;
}

label {
    line-height: 1em;
    text-align: right;
    width: 160px;
    left: 0;
    top: 2px;
    position: absolute;
}

input, textarea {
    width: 200px;
}

input.short {
    width: 6em;
}

input.compact {
    width: 3em;
}

input.checkbox {
    width: auto;
    margin: 0;
    border: 0 none;
}

label.checkbox {
    width: 260px;
    top: 0;
    left: 190px;
    text-align: left;
}

div {
    padding-bottom: 0.3em;
    padding-left: 170px;
    position: relative;
}

p {
    margin: 0;
    font-size: 10px;
    width: 200px;
}

Для таких элементов как checkbox или radio естественно будет расположение надписи справа от него. По этому для таких случаев элементу label нужно добавить класс checkbox. В качестве альтернативного варианта, можно назначить ему стиль position: relative. В этом случае будет важно, чтобы в коде надпись следовала после поля.

Напоминаю, что форма отправляет только поля, у которых указан атрибут name. А элементы label ссылаются на поля по id.

Запомнить порядок сортировки jQuery UI Sortable

В сервисе Google «Вопросы и ответы» одним пользователем был задан этот вопрос и, к моему удивлению, очень долго никто не давал на него ответа. А на самом деле эта задача очень просто решается благодаря встроенному методу serialize.

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

Сохраняем ссылку на корневой элемент сортируемого списка. Он не однократно потребуется нам в дальнейшем. Хоть эта функция и работает очень быстро, но так получить доступ можно будет еще быстрее.

var root = $('#items');

Назначаем всем элементам списка уникальные идентификаторы. Это обязательное условие правильной работы метода serialize. Префикс может быть произвольным, но в наших интересах, чтобы он был как можно короче. В качестве разделителя можно использовать символы подчеркивание, равенство или дефис.

$('> *', root).each(function (index) {
    this.id = 'item-' + index;
});

Инициализируем виджет Sortable и перехватываем событие update, чтобы сохранять состояние списка.

root.sortable({
    'update': function (event, ui) {
        var order = $(this).sortable('serialize');
        $.cookies.set('sortable', order);
    }
});

Восстанавливаем порядок элементов списка после загрузки страницы.

var c = $.cookies.get('sortable');
if (c) {
    $.each(c.split('&'), function () {
        var id = this.replace('[]=', '-');
        $('#' + id).appendTo(root);
    });
}

Пробегаемся по всем элементам, сохраненным в cookie, и перемещаем их в конец списка. Если был сохранен порядок для всех элементов, то они естественным образом займут свои места. Если для каких-то элементов позиция не сохранялась, то они «всплывут» наверх списка.

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

jQuery UI виджет, имитирующий кнопку

В статьях jQuery UI API и Фабрика элементов интерфейса jQuery UI я кратко рассказал о внутреннем устройстве надстройки jQuery UI. На примере кнопки я покажу, как можно писать очень полезные виджеты.

Задача: Написать виджет, имитирующий нажатие и блокирование кнопки. Он так же должен оповещать подписчиков о событиях, происходящих с этой кнопкой.

Объявляем наш виджет.

$.widget("trv.button", {

Так как я его изначально писал для проекта под кодовым названием «TRAVEL», то и пространство имен у меня будет особое, а не стандартное «ui». Если вы будете использовать свое пространство имен (а я настоятельно рекомендую вам это, чтобы избежать путаницы), то не забудьте переименовать классы CSS.

Далее объявляем инициализирующий метод нашего будущего виджета.

    _init: function() {
        var me = this;
        me.element
            .bind('mousedown.' + me.name, function() {
                me._down();
                return false;
            })
            .bind('mouseup.' + me.name, function () {
                me._up();
                return false;
            })
            .bind('click.' + me.name, function(event) {
                if (!me._getData('disabled')) {
                    me._trigger('press', event);
                }
                return false;
            });
    },

В этом методе подписываемся на события, связанные с нажатием левой кнопки мыши — «mousedown», «mouseup» и «click». Я не буду создавать дополнительные элементов для оформления кнопки, так-как предполагаю, что вся необходимая верстка присутствует на странице.

    destroy: function() {
        var me = this;
        me.element
            .unbind('mousedown.' + me.name)
            .unbind('mouseup.' + me.name)
            .unbind('click.' + me.name);
    },

Лично я в своей практике не применял метод удаления виджета, но тем не мене создал его, как предписывает руководство.

Следующие методы носят исключительно вспомогательный характер и не доступны извне.

    _down: function () {
        var me = this;

        if (!me._getData('disabled')) {
            me.element
                .addClass(me.widgetBaseClass + '-pressed')
                .one('mouseout.' + me.name, function () {
                    me._up();
                    me.element.one('mouseover.' + me.name, function () {
                        me._down();
                    });
                });

            $(document).one('mouseup.' + me.name, function () {
                me._up();
            });
        }
    },

Имитируем нажатие кнопки, добавляя CSS класс, и отслеживаем момент, когда указатель мышки покинет область кнопки, чтобы вернуть кнопку в исходное состояние.

Стоит заметить, что событие «mouseup» ловится на документе потому, что когда указатель мышки покинет элемент, то он уже не сможет ловить это событие.

    _up: function () {
        var me = this;
        me.element
            .unbind('mouseout.' + me.name)
            .unbind('mouseover.' + me.name)
            .removeClass(this.widgetBaseClass + '-pressed');
    }

Убираем дополнительный CSS класс с элемента, чтобы вернуть кнопку в исходное состояние, и отписываемся от событий «mouseout» и «mouseover».

});

Верстка кнопки может варьироваться вне зависимости от кода. Например, я использую такую:

<span class="trv-button">
    <span class="first-child">
        <em>Press me!</em>
    </span>
</span>

Кнопка полностью состоит из строчных элементов и теоретически может быть размещена внутри текста.

Когда на нее нажимают, то к основному элементу добавляется класс «trv-button-press». Этот класс автоматически создается из названия виджета и суффикса «-press».

Отслеживать нажатие кнопки можно через функцию обратного вызова «press» или соответствующее событие «buttonpress». Выбирайте, что вам больше нравится.