Опубликован: 28.02.2016 | Уровень: для всех | Доступ: платный
Лекция 11:

Дополнение

< Лекция 10 || Лекция 11: 123456

Темы

Одной из самых замечательных особенностей jQuery UI является возможность менять "шкурки" всех виджетов разом, и для этого даже предусмотрена специальная утилита – ThemeRoller [http://jqueryui.com/themeroller/]:


Если в какой-то момент времени потребуется внести изменения в тему, то откройте файл jquery-ui-#.#.##-custom.css и найдёте строчку начинающуюся с текста "To view and modify this theme, visit http://..." и таки пройдите по указанной ссылке, и уже используя ThemeRoller внесите необходимые изменения.

Пишем свой виджет

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

Первое, о чём стоит рассказать, это то, что правила написания плагинов для jQuery слишком вальяжны, что не способствует их качеству. При создании jQuery UI, походу, решили пойти путём стандартизации процесса написания плагинов и виджетов, я не могу сказать насколько задумка удалась, но стало явно лучше чем было. Начну с описания каркаса для вашего виджета:

$.widget("book.expose", {
// настройки по умолчанию
options: {
color: "red"
},
// инициализация widget
// вносим изменения в DOM и вешаем обработчики
_create: function() {
this.element; // искомый объект в jQuery обёртке
this.name; // имя - expose
this.namespace; // пространство – book
this.element.on("click."+this.eventNamespace, function(){
console.log("click");
})
},
// метод отвечает за применение настроек
_setOption: function( key, value ) {
// применяем изменения настроек
this._super("_setOption", key, value );
},
// метод _destroy должен быть антиподом к _create
// он должен убрать все изменения внесенные изменения в DOM
// и убрать все обработчики, если таковые были
_destroy: function() {
this.element.unbind('.'+this.eventNamespace);
}
});

Поясню для тех кто не прочёл комментарии:

  • options – хранилище настроек виджета для конкретного элемента
  • _create() – отвечает за инициализацию виджета – тут должны происходить изменения в DOM'е, и "вешаться" обработчики событий
  • _destroy() – антипод для "_create()" – должен подчистить всё, что мы намусорили
  • _setOption(key, value) – данный метод будет вызван при попытке изменить какие-либо настройки:
$("#my").expose({key:value})

Наблюдательный глаз заметит, что все перечисленные методы начинаются со знака подчёркивания – это такой способ выделить "приватные" методы, которые недоступны для запуска, и если мы попытаемся запустить "$('#my').expose('_destroy')", то получим ошибку. Но учтите – это лишь договорённость, соблюдайте, её!

Для обхода договорённости о приватности можно использовать метод "data()":

$("#my").data("expose")._destroy() // место для смайла "(evil)"

В данном примере, я постарался задать хороший тон написания виджетов – я "повесил" обработчики событий в namespace, это даст в дальнейшем возможность контролировать происходящее без необходимости залазить в код виджета, это "true story".

Код описанный в методе "_destroy()" – избыточен, т.к. он и так выполняется в публичном "destroy()", приведён тут для наглядности.

А для ленивых, чтобы не прописывать каждый раз "eventNamespace" в обработчиках событий, разработчики добавили в версии 1.9.0 два метода: "_on()" и "_off()", первый принимает два параметра:

  • DOM элемент, или селектор, или jQuery объект
  • набор обработчиков событий в виде объекта

Все перечисленные события будут "висеть" в пространстве "eventNamespace", т.е. результат будет предположительно одинаковым:

this._on(this.element, {
mouseover:function(event) {
console.log("Hello mouse");
},
mouseout:function(event) {
console.log("Bye mouse");
}
});

Второй метод – "_off()" – позволяет выборочно отключать обработчики:

this._off(this.element, "mouseout click");

Ну каркас баркасом, пора переходить к функционалу – добавим произвольную функцию с произвольным функционалом:

callMe:function(){
console.log("Allo?");
}

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

// изнутри
this.callMe()
// извне
$("#my").expose("callMe")

Если ваша функция принимает параметры, то передача оных осуществляется следующим способом:

$("#my").expose("callMe", "Hello!")

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

_create: function() {
var self = this; // вот он!
this.element.on("click."+this.eventNamespace, function(){
// тут используем self, т.к. this уже указывает на
// элемент по которому кликаем
self.callMe();
})
},

Хорошо идём, теперь поговорим о событиях – для более гибкой разработки и внедрения виджетов предусмотрен функционал по созданию произвольных событий и их "прослушиванию":

// инициируем событие
this._trigger("incomingCall");
// подписываемся на событие при инициализации виджета
$("#my").expose({
incommingCall: function(ev) {
console.log("din-don");
}
})
// или после, используя в качестве имени события
// имя виджета + имя события
$("#my").bind("exposeincomingCall", function(){
console.log("tru-lya-lya")
});

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

_delay() – данная функция работает как "setTimeout()", вот только контекст переданной функции будет указывать на сам виджет (это чтобы не заморачиваться с областью видимости)

_hoverable() и _focusable() – данным методам необходимо скармливать элементы для которых необходимо отслеживать события "hover" и "focus", чтобы автоматически добавит к ним классы "ui-state-hover" и "ui-state-focus" при наступлении оных

_hide() и _show() – эти два метода появились в версии 1.9.0, они созданы дабы стандартизировать поведение виджетов при использовании методов анимации, настройки принято прятать в опциях под ключами "hide" и "show" соответственно. Использовать методы следует следующим образом:

options: {
hide: {
effect: "slideDown", // настройки эквиваленты вызову
duration: 500 // .slideDown( 500)
}
}
// внутри виджета следует использовать вызовы _hide() и _show()
this._hide( this.element, this.options.hide, function() {
// это наша функция обратного вызова
console.log('спрятали');
});

Существует ещё пару методов, которые реализованы за нас:

enable: function() {
return this._setOption( "disabled", false );
},
disable: function() {
return this._setOption( "disabled", true );
},

Фактически, данный функции создают синоним для вызова:

$("#my").expose({ "disabled": true }) // или false

Наша задача сводится лишь к отслеживанию данного флага в методе "_setOption()".

Примеру быть, возможно этот виджет и не будет популярен, зато он наглядно демонстрирует как создавать виджеты для jQuery UI.

<!DOCTYPE html>
<html dir="ltr" lang="en-US">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Пример widget'а для jQuery UI</title>
    <link rel="profile" href="http://gmpg.org/xfn/11"/>
    <link rel="shortcut icon" href="http://anton.shevchuk.name/favicon.ico"/>
    <link rel="stylesheet" href="css/styles.css"/>
    <style>
        .expose-widget {
            position: absolute;
            z-index: 999;
            background-color: rgba(0, 0, 0, 0.42);
            display: none;
        }
    </style>
    <script type="text/javascript" src="js/jquery.js"></script>
    <script type="text/javascript" src="js/jquery-ui.js"></script>
    <script type="text/javascript" src="js/code.js"></script>
	<script>
        (function( $, undefined ) {
            $.widget( "book.expose", {
                version: "1.0.0",
                // наши настройки
                options: {
                    color: null, // цвет фона
                    speed: 500,  // скорость анимации
                    show: 500,   // настройки для метода _show()
                    hide: {      // настройки для метода _hide()
                        effect: "fade",
                        duration: 500
                    }
                },
                // инициализация
                _create: function() {
                    var self = this;
                    // выбранному элементу добавляем наш класс
                    // и вешаем обработчик события "клик"
                    this.element
                        .addClass( "book-widget" )
                        .on('click.expose-widget.'+this.eventNamespace, function() {
                            // вызываем метод виджета
                            // виджет доступен в переменной self
                            if (self.element.data('expose-it')) {
                                self.unmask();
                            } else {
                                self.mask();
                            }
                            return false;
                        });
                    // нам необходимо инициировать окружение лишь один раз для всех элементов
                    // это наш флаг
                    var totalInited = $.data(document.body, 'expose');
                    if (!totalInited) {
                        // изменяем флаг - у нас уже запущена инициализация
                        $.data(document.body, 'expose', 1);
                        // инициализация верхнего "бокса"
                        // и добавление в DOM
                        var $topBox = $('<div></div>');
                            $topBox.addClass("expose-widget");
                            $topBox.appendTo(document.body);
                        // сохраняем на него ссылку в реестре
                        $.data(document.body, 'topBox',    $topBox);
                        // клонируем верхний бокс
                        // добавляем и сохраняем
                        var $bottomBox = $topBox.clone();
                            $bottomBox.appendTo(document.body);
                        $.data(document.body, 'bottomBox', $bottomBox);
                        // повторяем для левого
                        var $leftBox = $topBox.clone();
                            $leftBox.appendTo(document.body);
                        $.data(document.body, 'leftBox',   $leftBox);
                        // и для правого
                        var $rightBox = $topBox.clone();
                            $rightBox.appendTo(document.body);
                        $.data(document.body, 'rightBox',  $rightBox);
                        // все боксы разом
                        $allBox = $topBox.add($bottomBox).add($leftBox).add($rightBox);
                        $.data(document.body, 'allBox',  $allBox);
                        // вешаем единый обработчик события для всех боксов
                        $(document.body).on('click.expose-widget', '.expose-widget', function() {
                            // вызов метода виджета
                            self.unmask();
                            return false;
                        });
                        // кидаем событие, на него можно подписаться лишь при инициализации виджета
                        // иначе толку не будет - будет поздно
                        this._trigger('build');
                    } else {
                        // отслеживаем кол-во виджетов
                        totalInited++;
                        $.data(document.body, 'expose', totalInited);
                    }
                    // вызываем метод виджета для применения цвета
                    this._setCurrentColor();
                },
                // ломаем всё что строили
                _destroy: function() {
                    // подчищаем наш элемент
                    this.element
                        .removeClass( "book-widget" )
                        // данный метод избыточен, т.к. это делает за нас публичный метод destroy()
                        .off('.'+this.eventNamespace);
                    // декрементим кол-во инициализированных виджетов
                    var totalInited = $.data(document.body, 'expose');
                    totalInited--;
                    $.data(document.body, 'expose', totalInited);
                    // если больше нет виджетов
                    // удаляем вспомогательные боксы
                    if (totalInited==0) {
                        $.data(document.body, 'allBox').remove();
                        $(document.body).off('.expose-widget');
                        this._trigger('destroy');
                    }
                },
                // применяем настройки
                _setOption: function( key, value ) {
                    if (key == 'disabled') {
                        // тут должен быть код по отключению виджета
                    }
                    if (key == 'speed') {
                        this.options.show = value;
                        this.options.hide.duration = value;
                    }
                    this._super( key, value );
                },
                // применяем настройки цвета
                _setCurrentColor: function() {
                    this.options.color = $.data(document.body, 'topBox').css('background-color');
                },
                // изменяем цвет
                color: function(value) {
                    $.data(document.body, 'allBox').css('background-color', value);
                    this._setCurrentColor();
                },
                // прячем боксы
                unmask: function() {
                    var self = this;
                    // получаем набор
                    var $allBox = $.data(document.body, 'allBox');
                    // вызываем функцию анимации
                    this._hide($allBox, this.options.hide, function() {
                        // бросаем событие по завершению
                        self._trigger('unmask')
                    });
                    this._resetFlag();
                    // изменяем флаг
                    this.element.data('expose-it', false);
                },
                // показываем боксы
                mask: function() {
                    var self = this;
                    // берём все "боксы"
                    var $allBox = $.data(document.body, 'allBox');
                    var $topBox = $.data(document.body, 'topBox');
                    var $bottomBox = $.data(document.body, 'bottomBox');
                    var $leftBox = $.data(document.body, 'leftBox');
                    var $rightBox = $.data(document.body, 'rightBox');
                    // вычисляем текущие координаты элемента и размеры окна
                    var offset = this.element.offset();
                    var height = this.element.outerHeight();
                    var width  = this.element.outerWidth();
                    var winWidth = $(document).width();
                    var winHeight = $(document).height();
                    var topSettings = {
                        top:0,
                        left:0,
                        width:winWidth,
                        height:offset.top,
                        backgroundColor:this.options.color
                    };
                    var leftSettings = {
                        top:offset.top,
                        left:0,
                        width:offset.left,
                        height:height,
                        backgroundColor:this.options.color
                    };
                    var rightSettings = {
                        top:offset.top,
                        left:offset.left+width,
                        width:winWidth - (offset.left+width),
                        height:height,
                        backgroundColor:this.options.color
                    };
                    var bottomSettings = {
                        top:offset.top+height,
                        left:0,
                        width:winWidth,
                        height:winHeight - (offset.top+height),
                        backgroundColor:this.options.color
                    };
                    // проверяем - отображается у нас боксы в данный момент времени или нет
                    if ($topBox.is(':hidden')) {
                        // применяем настройки
                        $topBox.css(topSettings);
                        $leftBox.css(leftSettings);
                        $rightBox.css(rightSettings);
                        $bottomBox.css(bottomSettings);
                        // показываем все наши боксы
                        this._show( $allBox, this.options.show, function() {
                            // событие mask
                            self._trigger('mask');
                        });
                    } else {
                        this._resetFlag();
                        // применяем настройки через анимацию
                        $topBox.animate(topSettings, this.options.speed);
                        $leftBox.animate(leftSettings, this.options.speed);
                        $rightBox.animate(rightSettings, this.options.speed);
                        $bottomBox.animate(bottomSettings, this.options.speed, function(){
                            // событие mask
                            self._trigger('mask');
                        });
                    }
                    // изменяем флаг
                    this.element.data('expose-it', true);
                },
                _resetFlag:function() {
                    // workaround - надо "сбросить" флаг для всех других элементов
                    // надо будет придумать более красивое решение
                    $.each($.cache, function(i, el) {
                        if (el.data != undefined && el.data.exposeIt != undefined) {
                            el.data.exposeIt = false;
                        }
                    });
                }
            });
        })( jQuery );
	</script>
</head>
<body>
    <div id="content" class="wrapper box">
        <menu label="Try...">
			<a href="tabs.html" title="go prev" class="button alignleft" rel="prev">← Prev </a>
            <a href="index.html" title="back to Index" class="button alignleft" rel="index">Index §</a>
            <a href="#" title="reload" class="button alignleft" onclick="window.location.reload();return false">Reload ¤</a>
            <hr/>
            <pre><code><em>// run widget and try to click on article</em>
$(<span>'article'</span>).expose()</code></pre>
            <button type="button" class="code">Run Code</button>
            <pre><code contenteditable="true"><em>// configuration was changed</em>
$(<span>'article'</span>).expose({speed:2000})</code></pre>
            <button type="button" class="code">Run Code</button>
            <pre><code contenteditable="true"><em>// run widget and try to click on image</em>
$(<span>'article img'</span>).expose({speed:100})</code></pre>
            <button type="button" class="code">Run Code</button>
            <pre><code><em>// try widget API - trigger</em>
$(<span>'article:eq(0)'</span>).bind(<span>'exposemask'</span>,
    function(){
        $(this).css(<span>'color'</span>, <span>'red'</span>);
    });
$(<span>'article:eq(0)'</span>).bind(<span>'exposeunmask'</span>,
    function(){
        $(this).css(<span>'color'</span>, <span>'black'</span>);
    })</code></pre>
            <button type="button" class="code">Run Code</button>
            <pre><code><em>// try widget API - call method</em>
$(<span>'article:eq(1)'</span>).expose(<span>'mask'</span>)</code></pre>
            <button type="button" class="code">Run Code</button>
            <pre><code contenteditable="true"><em>// try widget API - call method+args</em>
$(<span>'article:eq(1)'</span>).expose(
    <span>'color'</span>,
    <span>'rgba(255, 0, 0, 0.5)'</span>
)</code></pre>
            <button type="button" class="code">Run Code</button>
        </menu>
        <header>
            <h1>widget</h1>
            <h2>Пишем своё виджет для jQuery UI</h2>
        </header>
        <article>
            <h2>Article</h2>
            <p>
                <img src="images/photo-bumblebee-tumb.jpg" alt="Bumblebee" class="left" width="200"/>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus rutrum,
            lectus eu varius consectetur, libero velit hendrerit augue, ut posuere enim neque
            in libero. Donec eget sagittis nibh. Suspendisse sed tincidunt urna. Cras quis
            euismod neque. Maecenas auctor ultricies posuere. Pellentesque luctus pulvinar dui
            eget semper. Donec sodales odio eu sapien varius luctus. Donec dictum feugiat diam
            at malesuada. Sed nec massa in augue condimentum faucibus quis ut diam. Quisque
            nisl sem, semper nec vulputate vel, mattis sit amet justo. Aliquam purus felis,
            tempor at scelerisque quis, tincidunt in neque. Etiam ut risus diam. Pellentesque
            fermentum risus id elit feugiat cursus. Ut fringilla dictum diam, sed iaculis
            lorem pulvinar ut. Cras vel elit id velit commodo viverra sit amet vel orci.</p>
        </article>
        <article>
            <h2>Article</h2>
            <p>
                <img src="images/photo-chamomile-tumb.jpg" alt="Chamomile" class="left" width="200"/>
            Duis in vestibulum sem. Cras euismod tincidunt dui, et scelerisque tellus condimentum vel.
            Maecenas et urna sit amet risus fermentum rhoncus nec porttitor ligula. Maecenas sit amet
            turpis enim, ut iaculis est. Duis feugiat, lacus id placerat porttitor, lorem augue gravida
            nisi, eu porta eros risus et lectus. Maecenas vestibulum nunc vel ipsum tincidunt sit amet
            blandit sapien bibendum. Proin vel vulputate nisl. Duis tempor imperdiet placerat. Pellentesque
            faucibus consequat magna, et bibendum nisl egestas non. Pellentesque sit amet mattis augue.
            Aenean at diam tincidunt purus sollicitudin gravida non in nisi. Fusce bibendum, magna in
            adipiscing mattis, sem risus fringilla mi, nec gravida lectus lectus at nibh. Suspendisse
            adipiscing elementum laoreet. Suspendisse sem erat, varius quis aliquet vitae, dapibus sed
            nibh. Nullam iaculis sem at mauris faucibus in vestibulum libero pretium. Aliquam eu turpis
            libero. Fusce et ultrices lectus.</p>
        </article>
        <article>
            <h2>Article</h2>
            <p>
                <img src="images/photo-maple-leaf-tumb.jpg" alt="Maple Leaf" class="left" width="200"/>
            Ut consequat commodo mauris, eu dignissim justo congue vel. Etiam commodo tincidunt diam,
            laoreet ullamcorper sapien egestas quis. Etiam auctor rutrum ante, at tincidunt elit lacinia
            non. Pellentesque molestie tellus sit amet est sodales nec rutrum leo pharetra. Donec lacinia
            ipsum vitae massa accumsan ullamcorper. Maecenas commodo lacus turpis. Proin sit amet mauris
            sem, imperdiet faucibus lorem. Fusce ullamcorper consectetur ligula vel pretium. Sed et elit
            vitae orci adipiscing condimentum id sed turpis. Morbi ultrices feugiat ullamcorper. Fusce at
            magna dolor. Sed sit amet risus massa, quis imperdiet libero. Proin justo purus, sodales nec
            cursus et, sollicitudin at nulla. Vivamus eget nibh tellus, sit amet facilisis ante.</p>
        </article>
        <footer>
            ©copyright 2014 Anton Shevchuk — <a href="http://anton.shevchuk.name/jquery-book/">jQuery Book</a>
        </footer>
        <script type="text/javascript">
            var _gaq = _gaq || [];
            _gaq.push(['_setAccount', 'UA-1669896-2']);
            _gaq.push(['_trackPageview']);
            (function() {
             var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
             ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
             var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
            })();
        </script>
	</div>
</body>
</html>

Будьте внимательны, с выходом jQuery UI версии 1.9.0 были внесены правки в Widget API, следовательно, большинство доступной инфор- мации устарело, так что читайте официальную документацию [http://wiki.jqueryui.com/w/page/12138135/Widget%20factory], а ещё лучше – заглядывайте в код готовых виджетов "от производителя"

Информация по теме разработки виджетов:

< Лекция 10 || Лекция 11: 123456
Наталья Маркова
Наталья Маркова
Ярослав Гаевой
Ярослав Гаевой

10 марта 2016 c 20:13 до 22:39 я сдавал экзамен. Однако, за два месяца статус не изменился: "Задание не проверено"

Когда ожидать проверки?

Руслан Жанбосынов
Руслан Жанбосынов
Россия
Дмитрий Молокоедов
Дмитрий Молокоедов
Россия, Новосибирск, НГПУ, 2009