Статьи

Как имитировать интерфейс iGoogle

Дважды в месяц мы возвращаемся к любимым постам наших читателей на протяжении всей истории Nettuts +.

В этом уроке я покажу вам, как создать настраиваемый интерфейс с виджетами. Готовый продукт представляет собой гладкий и ненавязчиво-подобный интерфейс, похожий на iGoogle , который имеет множество потенциальных приложений!

Готовый проект

Во-первых, давайте перечислим, что именно мы будем здесь создавать и какие у него будут возможности:

  • Этот интерфейс будет содержать несколько виджетов.
  • Каждый виджет может быть свернут, удален и отредактирован.
  • Пользователь может сортировать виджеты по трем отдельным столбцам (используя технику перетаскивания).
  • Пользователь сможет редактировать цвет и заголовок каждого виджета.
  • Каждый виджет может содержать любое количество обычного HTML-контента, текста, изображений, flash и т. Д.

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

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

Макет простой три столбца один; каждый столбец содержит виджеты:

расположение

Каждый виджет имеет «дескриптор», который пользователь может использовать для перемещения виджета.

Наряду с базовой библиотекой jQuery мы также собираемся использовать библиотеку пользовательского интерфейса jQuery и, в частности, « сортируемые » и « перетаскиваемые » модули. Это позволит довольно просто добавить желаемую функцию перетаскивания. Вы должны получить персональную загрузку библиотеки пользовательского интерфейса, в которой есть то, что нам нужно. (Отметьте «сортируемое» поле)


Каждый столбец будет неупорядоченным списком ( UL ), а каждый виджет в столбцах будет элементом списка ( LI ):

Первый столбец:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<ul id=»column1″ class=»column»>
 
    <li class=»widget red»>
        <div class=»widget-head»>
            <h3>Widget title</h3>
        </div>
        <div class=»widget-content»>
 
            <p>The content…</p>
        </div>
    </li>
    <li class=»widget blue»>
        <div class=»widget-head»>
 
            <h3>Widget title</h3>
        </div>
        <div class=»widget-content»>
            <p>The content…</p>
 
        </div>
    </li>
</ul>

Приведенный выше код представляет первый столбец слева и два виджета в каждом элементе списка. Как показано в плане, будет три столбца — три неупорядоченных списка.


Мы будем использовать два CSS StyleSheets, один из которых будет содержать все основные стили, а второй StyleSheet будет содержать только стили, требуемые улучшениями JavaScript. Причина, по которой мы разделяем их таким образом, заключается в том, что люди без включенного JavaScript не тратят свою пропускную способность при загрузке, которую они не собираются использовать.

Вот inettuts.css :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/* Reset */
body,img,p,h1,h2,h3,h4,h5,h6,ul,ol {margin:0;
/* End Reset */
     
body {font-size:0.8em;
a {color:white;}
     
/* Colours */
.color-yellow {background:#f2bc00;}
.color-red {background:#dd0000;}
.color-blue {background:#148ea4;}
.color-white {background:#dfdfdf;}
.color-orange {background:#f66e00;}
.color-green {background:#8dc100;}
.color-yellow h3,.color-white h3,.color-green h3
    {color:#000;}
.color-red h3,.color-blue h3,.color-orange h3
    {color:#FFF;}
/* End Colours */
     
/* Head section */
#head {
    background: #000 url(img/head-bg.png) repeat-x;
    height: 100px;
}
#head h1 {
    line-height: 100px;
    color: #FFF;
    text-align: center;
    background: url(img/inettuts.png) no-repeat center;
    text-indent: -9999em
}
/* End Head Section */
     
/* Columns section */
#columns .column {
    float: left;
    width: 33.3%;
        /* Min-height: */
        min-height: 400px;
        height: auto !important;
        height: 400px;
}
     
/* Column dividers (background-images) : */
    #columns #column1 { background: url(img/column-bg-left.png) no-repeat right top;
    #columns #column3 { background: url(img/column-bg-right.png) no-repeat left top;
         
#columns #column1 .widget { margin: 30px 35px 30px 25px;
#columns #column3 .widget { margin: 30px 25px 30px 35px;
#columns .widget {
    margin: 30px 20px 0 20px;
    padding: 2px;
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
}
#columns .widget .widget-head {
    color: #000;
    overflow: hidden;
    width: 100%;
    height: 30px;
    line-height: 30px;
}
#columns .widget .widget-head h3 {
    padding: 0 5px;
    float: left;
}
#columns .widget .widget-content {
    background: #333 url(img/widget-content-bg.png) repeat-x;
    padding: 5px;
    color: #DDD;
    -moz-border-radius-bottomleft: 2px;
    -moz-border-radius-bottomright: 2px;
    -webkit-border-bottom-left-radius: 2px;
    -webkit-border-bottom-right-radius: 2px;
    line-height: 1.2em;
    overflow: hidden;
}
/* End Columns section */

В приведенной выше таблице стилей нет ничего слишком сложного. Обычно было бы лучше использовать изображения вместо свойства CSS3 border-radius для создания закругленных углов (для кросс-браузерной выгоды), но на самом деле они не являются неотъемлемой частью макета — добавление border-radius происходит быстро и безболезненно.

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

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

1
2
3
4
5
6
7
8
9
#columns .column {
    float: left;
    width: 33.3%;
     
    /* Min-height: */
        min-height: 400px;
        height: auto !important;
        height: 400px;
}

Мы сосредоточимся на второй таблице стилей позже, когда добавим JavaScript.

Вот предварительный просмотр того, что мы получили, только CSS / HTML (и некоторые изображения):

расположение

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

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

Мы начнем с глобального объекта под названием « iNettuts » — он будет действовать как единственное занятое пространство имен проекта (плюс зависимости, такие как jQuery). Под ним мы будем кодировать основные функциональные возможности сайта, который использует jQuery и его библиотеку пользовательского интерфейса.

inettuts.js :

1
2
3
4
5
6
7
8
var iNettuts = {
    settings : {
       // Some simple settings will go here.
    },
    init : function(){
        // The method which starts it all…
    }
};

Метод init будет вызван, когда документ будет готов к манипулированию (т. Е. Когда DOM загружен и готов). Несмотря на то, что доступны различные методы, было доказано, что самый быстрый способ инициализировать ваш код в этом событии — это вызвать его из нижней части документа. Также имеет смысл ссылаться на все скрипты внизу, чтобы не замедлять загрузку остальной части страницы:

01
02
03
04
05
06
07
08
09
10
11
12
13
<body>
     
    <!— page content —>
 
     
     
    <!— Bottom of document —>
    <script type=»text/javascript» src=»http://jqueryjs.googlecode.com/files/jquery-1.2.6.min.js»></script>
    <script type=»text/javascript» src=»inettuts.js»></script>
 
    <script type=»text/javascript» src=»jquery-ui-personalized-1.6rc2.min.js»></script>
     
</body>

Как я уже сказал, будет объект settings который будет содержать все глобальные настройки, необходимые для этой функции. У нас также будут отдельные объекты настроек виджетов, это означает, что можно будет создавать настройки для каждого виджета.

settings объекта (под iNettuts ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
settings : {
    /* Specify selectors */
    columns : ‘.column’,
    widgetSelector: ‘.widget’,
    handleSelector: ‘.widget-head’,
    contentSelector: ‘.widget-content’,
    /* Settings for the Widgets: */
    widgetDefault : {
        movable: true,
        removable: true,
        collapsible: true,
        editable: true,
        colorClasses : [‘yellow’,’red’,’blue’,’white’,’orange’,’green’]
    },
    /* Individual Widget settings: */
    widgetIndividual : {
        intro : {
            movable: false,
            removable: false,
            collapsible: false
        },
        gallery : {
            colorClasses : [‘yellow’,’red’,’white’]
        }
    }
}

Да, настроек достаточно много, но если мы хотим максимального повторного использования кода, это необходимо. Большинство из вышесказанного говорит само за себя. Как вы можете видеть, мы настроили объект widgetDefault который содержит настройки по умолчанию для каждого виджета; если вы хотите переопределить эти настройки, тогда скрипт потребует от вас присвоить виджету id (в HTML), а затем создать новый набор правил. У нас есть два набора правил (объектов), которые переопределяют их значения по умолчанию, ‘ intro ‘ и ‘ gallery ‘. Таким образом, эти правила, указанные в объекте «gallery», будут применяться только к этому виджету:

01
02
03
04
05
06
07
08
09
10
11
12
<li class=»widget blue» id=»gallery»>
    <div class=»widget-head»>
        <h3>Instructions</h3>
    </div>
 
    <div class=»widget-content»>
        <ul>
            <li>To move a widget…</li>
        </ul>
    </div>
 
</li>

Объект getWidgetSettings (под iNettuts ):

1
2
3
4
5
6
getWidgetSettings : function(id) {
    var settings = this.settings;
    return (id&&settings.widgetIndividual[id]) ?
        $.extend({},settings.widgetDefault,settings.widgetIndividual[id])
        : settings.widgetDefault;
}

Этот метод вернет объект с настройками любого конкретного виджета. Если у виджета нет id (атрибута HTML), то он просто вернет настройки по умолчанию, в противном случае он посмотрит, есть ли у этого виджета собственные настройки, если он это сделает, то он вернет настройки по умолчанию и настройки этого виджета объединятся в один объект (индивидуальные настройки виджета имеют приоритет).

Ранее я упоминал, что у нас есть дополнительная таблица стилей, которая потребуется для улучшений JavaScript.

Вот таблица стилей (inettuts.js.css):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* JS-Enabled CSS */
     
.widget-head a.remove {
    float: right;
    display: inline;
    background: url(img/buttons.gif) no-repeat -24px 0;
    width: 14px;
    height: 14px;
    margin: 8px 4px 8px 0;
    text-indent: -9999em;
    outline: none;
}
     
.widget-head a.edit {
    float: right;
    display: inline;
    background: url(img/buttons.gif) no-repeat;
    width: 24px;
    height: 14px;
    text-indent: -9999em;
    margin: 8px 4px 8px 4px;
    outline: none;
}
     
.widget-head a.collapse {
    float: left;
    display: inline;
    background: url(img/buttons.gif) no-repeat -52px 0;
    width: 14px;
    height: 14px;
    text-indent: -9999em;
    margin: 8px 0 8px 4px;
    outline: none;
}
     
.widget-placeholder { border: 2px dashed #999;}
#column1 .widget-placeholder { margin: 30px 35px 0 25px;
#column2 .widget-placeholder { margin: 30px 20px 0 20px;
#column3 .widget-placeholder { margin: 30px 25px 0 35px;
     
.edit-box {
    overflow: hidden;
    background: #333 url(img/widget-content-bg.png) repeat-x;
    margin-bottom: 2px;
    padding: 10px 0;
}
     
.edit-box li.item {
    padding: 10px 0;
    overflow: hidden;
    float: left;
    width: 100%;
    clear: both;
}
     
.edit-box label {
    float: left;
    width: 30%;
    color: #FFF;
    padding: 0 0 0 10px;
}
     
.edit-box ul.colors li {
    width: 20px;
    height: 20px;
    border: 1px solid #EEE;
    float: left;
    display: inline;
    margin: 0 5px 0 0;
    cursor: pointer;
}

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

Метод, который присоединяет эту таблицу стилей, называется ‘attachStylesheet’:

1
2
3
attachStylesheet : function (href) {
    return $(‘<link href=»‘ + href + ‘» rel=»stylesheet» type=»text/css» />’).appendTo(‘head’);
}

Вышеуказанным способом добавляется ссылка на заголовок документа. Когда новый элемент ссылки добавляется в документ через DOM, браузер загружает его и применяет свои правила CSS, как и для любой обычной жестко закодированной связанной таблицы стилей. При этом помните, что правила наследования и специфичности CSS все еще применяются.

Виджеты

Следующая часть урока, вероятно, самая сложная, поэтому делайте это медленно.

Мы хотим добавить еще один метод в наш глобальный объект iNettuts, назовем его makeSortable:

1
2
3
makeSortable : function () {
    // This function will make the widgets ‘sortable’!
}

Кстати, «метод» — это просто причудливое имя, данное «функциям», которые были назначены свойствам объекта. В этом случае наш объект называется 'iNettuts' поэтому 'makeSortable' — это метод 'iNettuts'

Этот новый метод возьмет параметры, которые мы указали в объекте 'settings' и сделает требуемый элемент сортируемым.

Во-первых, мы хотим убедиться, что все, что нам нужно, легко доступно в этом новом методе:

1
2
3
4
5
makeSortable : function () {
    var iNettuts = this, // *1
        $ = this.jQuery, // *2
        settings = this.settings;
}

* 1: Будет только один экземпляр нашего глобального объекта, но нужно просто создать несколько экземпляров или, если мы хотим переименовать глобальный объект, было бы неплохо установить новую переменную (в данном случае ‘iNettuts’) в ключевое слово this, которое ссылается на объект, в котором находится этот метод. Будьте осторожны, ключевое слово «this» немного чудовищно и не всегда ссылается на то, что вы думаете, оно делает!

* 2: В самом верху объекта iNettuts мы поместили новое свойство: ‘ jQuery : $ ‘. В стремлении к максимальному повторному использованию кода мы не хотим, чтобы наш скрипт конфликтовал с какими-либо другими библиотеками, которые также используют символ знака доллара (например, библиотека Prototype ). Так, например, если вы переименовали jQuery в JQLIB, вы можете изменить свойство jQuery на JQLIB, и скрипт продолжит работать должным образом. 2-я строка в приведенном выше коде вообще не нужна, если мы не хотим этого, мы можем просто использовать this.jQuery().ajQueryFunction() вместо $() в этом методе.

* 3: Опять же, в этом нет необходимости, мы просто создаем небольшой ярлык, поэтому вместо того, чтобы вводить « this.settings » в этом методе, нам нужно всего лишь набрать « settings ».

Следующим шагом является определение набора сортируемых элементов (т.е. виджетов, которые будут перемещаться). Помните, что в settings мы сделали возможным установить свойство с именем 'movable' в true или false . Если для ‘movable’ установлено значение false , то по умолчанию или для отдельных виджетов мы должны учитывать это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
 * (using the dollar prefix on $sortableItems is a convention when a variable references a jQuery object)
 */
   
$sortableItems = (function () {
     
    // Define an empty string which can add to within the loop:
    var notSortable = »;
     
    // Loop through each widget within the columns:
    $(settings.widgetSelector,$(settings.columns)).each(function (i) {
         
        // If the ‘movable’ property is set to false:
        if (!iNettuts.getWidgetSettings(this.id).movable) {
             
            // If this widget has NO ID:
            if(!this.id) {
                 
                // Give it an automatically generated ID:
                this.id = ‘widget-no-id-‘ + i;
                 
            }
         
            // Add the ID to the ‘notSortable’ string:
            notSortable += ‘#’ + this.id + ‘,’;
        }
         
    });
     
    /*
    * This function will return a jQuery object containing
    * those widgets which are movable.
    */
    return $(‘> li:not(‘ + notSortable + ‘)’, settings.columns);
})();

Теперь у нас есть набор элементов DOM, на которые есть ссылки в объекте jQuery, который возвращается из вышеуказанных функций. Мы можем немедленно использовать это:

01
02
03
04
05
06
07
08
09
10
11
$sortableItems.find(settings.handleSelector).css({
    cursor: ‘move’
}).mousedown(function (e) {
    $(this).parent().css({
        width: $(this).parent().width() + ‘px’
    });
}).mouseup(function () {
    if(!$(this).parent().hasClass(‘dragging’)) {
        $(this).parent().css({width:»});
    }
});

Итак, мы ищем то, что было определено как «дескриптор» в подвижных виджетах (внутри sortableItems ), а затем мы применяем новое свойство CSS-курсора «move» к каждому; это должно сделать очевидным, что каждый виджет является подвижным.

Функции mousedown и mouseup необходимы для решения некоторых проблем с перетаскиванием … Поскольку мы хотим, чтобы эта страница и все элементы внутри нее расширялись при изменении размера браузера, мы не установили явную ширину виджетов (список Предметы). Когда один из этих элементов списка сортируется, он становится абсолютно позиционированным (при перетаскивании), что означает, что он растянется до составной ширины содержимого. Вот пример:

При перетаскивании виджет растягивается на длину страницы

Вот что должно происходить:

При перетаскивании виджет имеет правильную ширину!

Чтобы это произошло, мы явно установили ширину виджета такой, какой она была до начала перетаскивания. Модуль 'sortable' пользовательского интерфейса имеет свойство, в котором вы можете поместить функцию, которая будет запускаться, когда виджет начинает сортироваться (т.е. когда он начинает перетаскиваться), к сожалению, этого недостаточно для нас, потому что он выполняется слишком поздно; нам нужно установить ширину до того, как модуль «sortable» вступит в силу — лучший способ сделать это — запустить функцию в mousedown дескриптора (в данном случае «handle» — это полоса в верхней части каждого виджета). ).

1
2
3
4
5
6
// mousedown function:
// Traverse to parent (the widget):
$(this).parent().css({
    // Explicitely set width as computed width:
    width: $(this).parent().width() + ‘px’
});

Если мы оставим это так, то когда вы уроните виджет в определенном месте и

В браузере виджет не изменится в размере. Чтобы предотвратить это, нам нужно написать функцию, которая будет привязана к событию mouseup дескриптора:

01
02
03
04
05
06
07
08
09
10
11
// mouseup function:
// Check if widget is currently in the process of dragging:
if(!$(this).parent().hasClass(‘dragging’)) {
    // If it’s not then reset width to »:
    $(this).parent().css({width:»});
} else {
    // If it IS currently being dragged then we want to
    // temporarily disable dragging, while widget is
    // reverting to original position.
    $(settings.columns).sortable(‘disable’);
}

Класс «перетаскивания» добавляется к тому свойству «start» сортируемого модуля, о котором мы говорили ранее. (мы напишем этот код позже)

Вот как выглядит наш метод makeSortable:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
makeSortable : function () {
    var iNettuts = this,
        $ = this.jQuery,
        settings = this.settings,
         
        $sortableItems = (function () {
            var notSortable = »;
            $(settings.widgetSelector,$(settings.columns)).each(function (i) {
                if (!iNettuts.getWidgetSettings(this.id).movable) {
                    if(!this.id) {
                        this.id = ‘widget-no-id-‘ + i;
                    }
                    notSortable += ‘#’ + this.id + ‘,’;
                }
            });
            return $(‘> li:not(‘ + notSortable + ‘)’, settings.columns);
        })();
     
    $sortableItems.find(settings.handleSelector).css({
        cursor: ‘move’
    }).mousedown(function (e) {
        $sortableItems.css({width:»});
        $(this).parent().css({
            width: $(this).parent().width() + ‘px’
        });
    }).mouseup(function () {
        if(!$(this).parent().hasClass(‘dragging’)) {
            $(this).parent().css({width:»});
        } else {
            $(settings.columns).sortable(‘disable’);
        }
    });
}

Далее, все еще в 'makeSortable' нам нужно инициализировать модуль 'makeSortable' :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
makeSortable : function () {
    // ………………………
    // BEGINNING OF METHOD (above)
    // ………………………
     
    // Select the columns and initiate ‘sortable’:
    $(settings.columns).sortable({
     
        // Specify those items which will be sortable:
        items: $sortableItems,
         
        // Connect each column with every other column:
        connectWith: $(settings.columns),
         
        // Set the handle to the top bar:
        handle: settings.handleSelector,
         
        // Define class of placeholder (styled in inettuts.js.css)
        placeholder: ‘widget-placeholder’,
         
        // Make sure placeholder size is retained:
        forcePlaceholderSize: true,
         
        // Animated revent lasts how long?
        revert: 300,
         
        // Delay before action:
        delay: 100,
         
        // Opacity of ‘helper’ (the thing that’s dragged):
        opacity: 0.8,
         
        // Set constraint of dragging to the document’s edge:
        containment: ‘document’,
         
        // Function to be called when dragging starts:
        start: function (e,ui) {
            $(ui.helper).addClass(‘dragging’);
        },
         
        // Function to be called when dragging stops:
        stop: function (e,ui) {
         
            // Reset width of units and remove dragging class:
            $(ui.item).css({width:»}).removeClass(‘dragging’);
             
            // Re-enable sorting (we disabled it on mouseup of the handle):
            $(settings.columns).sortable(‘enable’);
             
        }
         
    });
     
}

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

Следующим шагом является предоставление пользователю возможности свернуть виджеты, закрыть (удалить) виджеты и редактировать определенные элементы в каждом виджете.

Мы собираемся поместить все это в один метод, мы назовем его 'addWidgetControls' :

1
2
3
addWidgetControls : function () {
    // This function will add controls to each widget!
}

Как и в случае с 'makeSortable' мы хотим установить следующие переменные в начале:

1
2
3
4
5
addWidgetControls : function () {
    var iNettuts = this,
        $ = this.jQuery,
        settings = this.settings;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Loop through each widget:
$(settings.widgetSelector, $(settings.columns)).each(function () {
 
    /* Merge individual settings with default widget settings */
    var thisWidgetSettings = iNettuts.getWidgetSettings(this.id);
     
    // (if «removable» option is TRUE):
    if (thisWidgetSettings.removable) {
     
        // Add CLOSE (REMOVE) button & functionality
         
    }
     
    // (if «removable» option is TRUE):
    if (thisWidgetSettings.editable) {
     
        // Add EDIT button and functionality
         
    }
     
    // (if «removable» option is TRUE):
    if (thisWidgetSettings.collapsible) {
     
        // Add COLLAPSE button and functionality
         
    }
         
});

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

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

  • ЗАКРЫТЬ (удалить): эта кнопка удалит виджет из DOM. Вместо того, чтобы просто удалить его немедленно, мы применим эффект, который затемнит виджет и затем сдвинет его занимаемое пространство.
  • РЕДАКТИРОВАТЬ : эта кнопка, при нажатии на которую открывается раздел «окно редактирования» внутри виджета. В этом разделе редактирования пользователь может изменить название виджета и его цвет. Чтобы закрыть раздел «Редактировать», пользователь должен снова нажать на ту же кнопку «Редактировать» — так что в основном эта кнопка переключает раздел «Редактировать».
  • COLLAPSE : эта кнопка переключается между стрелкой вверх и стрелкой вниз в зависимости от того, свернут ли виджет или нет. Свертывание виджета просто скрывает его содержимое, поэтому единственным видимым элементом виджета будет дескриптор (полоса в верхней части каждого виджета).

Теперь мы знаем, чего хотим, поэтому мы можем начать писать: (фрагменты ниже полны комментариев, поэтому обязательно прочитайте код!)

ЗАКРЫТЬ (удалить):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// (if «removable» option is TRUE):
if (thisWidgetSettings.removable) {
     
    // Create new anchor element with class of ‘remove’:
    $(‘<a href=»#» class=»remove»>CLOSE</a>’).mousedown(function (e) {
     
        // Stop event bubbling:
        e.stopPropagation();
            
    }).click(function () {
     
        // Confirm action — make sure that the user is sure:
        if(confirm(‘This widget will be removed, ok?’)) {
         
            // Animate widget to an opacity of 0:
            $(this).parents(settings.widgetSelector).animate({
                opacity: 0
            },function () {
             
                // When animation (opacity) has finished:
                // Wrap in DIV (explained below) and slide up:
                $(this).wrap(‘<div/>’).parent().slideUp(function () {
                 
                    // When sliding up has finished, remove widget from DOM:
                    $(this).remove();
                     
                });
            });
        }
         
        // Return false, prevent default action:
        return false;
         
    })
     
    // Now, append the new button to the widget handle:
    .appendTo($(settings.handleSelector, this));
     
}

РЕДАКТИРОВАТЬ :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* (if «editable» option is TRUE) */
if (thisWidgetSettings.editable) {
     
    // Create new anchor element with class of ‘edit’:
    $(‘<a href=»#» class=»edit»>EDIT</a>’).mousedown(function (e) {
         
        // Stop event bubbling
        e.stopPropagation();
         
    }).toggle(function () {
        // Toggle: (1st state):
         
        // Change background image so the button now reads ‘close edit’:
        $(this).css({backgroundPosition: ‘-66px 0′, width: ’55px’})
             
            // Traverse to widget (list item):
            .parents(settings.widgetSelector)
                 
                // Find the edit-box, show it, then focus <input/>:
                .find(‘.edit-box’).show().find(‘input’).focus();
                 
        // Return false, prevent default action:
        return false;
         
    },function () {
        // Toggle: (2nd state):
         
        // Reset background and width (will default to CSS specified in StyleSheet):
        $(this).css({backgroundPosition: », width: »})
             
            // Traverse to widget (list item):
            .parents(settings.widgetSelector)
                 
                // Find the edit-box and hide it:
                .find(‘.edit-box’).hide();
        // Return false, prevent default action:
        return false;
 
    })
     
    // Append this button to the widget handle:
    .appendTo($(settings.handleSelector,this));
     
    // Add the actual editing section (edit-box):
    $(‘<div class=»edit-box» style=»display:none;»/>’)
        .append(‘<ul><li class=»item»><label>Change the title?</label><input value=»‘ + $(‘h3’,this).text() + ‘»/></li>’)
        .append((function(){
             
            // Compile list of available colours:
            var colorList = ‘<li class=»item»><label>Available colors:</label><ul class=»colors»>’;
             
            // Loop through available colors — add a list item for each:
            $(thisWidgetSettings.colorClasses).each(function () {
                colorList += ‘<li class=»‘ + this + ‘»/>’;
            });
             
            // Return (to append function) the entire colour list:
            return colorList + ‘</ul>’;
             
        })())
         
        // Finish off list:
        .append(‘</ul>’)
         
        // Insert the edit-box below the widget handle:
        .insertAfter($(settings.handleSelector,this));
         
}

Свернуть :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// (if ‘collapsible’ option is TRUE)
if (thisWidgetSettings.collapsible) {
     
    // Create new anchor with a class of ‘collapse’:
    $(‘<a href=»#» class=»collapse»>COLLAPSE</a>’).mousedown(function (e) {
         
        // Stop event bubbling:
        e.stopPropagation();
         
 
    }).toggle(function () {
        // Toggle: (1st State):
         
        // Change background (up-arrow to down-arrow):
        $(this).css({backgroundPosition: ‘-38px 0’})
         
            // Traverse to widget (list item):
            .parents(settings.widgetSelector)
                // Find content within widget and HIDE it:
                .find(settings.contentSelector).hide();
                 
        // Return false, prevent default action:
        return false;
         
    },function () {
        // Toggle: (2nd State):
         
        // Change background (up-arrow to down-arrow):
        $(this).css({backgroundPosition: »})
         
            // Traverse to widget (list item):
            .parents(settings.widgetSelector)
             
                // Find content within widget and SHOW it:
                .find(settings.contentSelector).show();
                 
        // Return false, prevent default action:
        return false;
         
    })
     
    // Prepend that ‘collapse’ button to the widget’s handle:
    .prependTo($(settings.handleSelector,this));
}

Пузырьки событий или «распространение» — это когда при нажатии на элемент событие проходит через DOM к элементу самого высокого уровня с событием, аналогичным событию, которое вы только что вызвали в исходном элементе. Если бы мы не остановили распространение в приведенных выше фрагментах ( e.stopPropagation(); ) для события mouseDown каждой добавленной кнопки, то событие mouseDown дескриптора (родителя кнопок) также сработает, и, таким образом, начнется перетаскивание. просто удерживая мышь над одной из кнопок — мы не хотим, чтобы это произошло; мы хотим, чтобы перетаскивание начиналось только тогда, когда пользователь наводит указатель мыши прямо на ручку и нажимает вниз.

Мы написали код, который будет вставлять поля редактирования в документ в правильных местах. — Мы добавили поле ввода, чтобы пользователи могли изменять заголовок виджета, а также добавили список доступных цветов. Итак, теперь нам нужно перебрать каждый новый блок редактирования (скрытый от просмотра) и указать, как эти элементы могут взаимодействовать с:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Loop through each edit-box (under each widget that has an edit-box)
$(‘.edit-box’).each(function () {
     
    // Assign a function to the onKeyUp event of the input:
    $(‘input’,this).keyup(function () {
         
        // Traverse UP to widget and find the title, set text to
        // the input element’s value — if the value is longer
        // than 20 characters then replace remainder characters
        // with an elipsis (…).
        $(this).parents(settings.widgetSelector).find(‘h3′).text( $(this).val().length>20 ? $(this).val().substr(0,20)+’…’ : $(this).val() );
         
    });
     
    // Assing a function to the Click event of each colour list-item:
    $(‘ul.colors li’,this).click(function () {
         
        // Define colorStylePattern to match a class with prefix ‘color-‘:
        var colorStylePattern = /\bcolor-[\w]{1,}\b/,
             
            // Define thisWidgetColorClass as the colour class of the widget:
            thisWidgetColorClass = $(this).parents(settings.widgetSelector).attr(‘class’).match(colorStylePattern)
        // If a class matching the pattern does exist:
        if (thisWidgetColorClass) {
             
            // Traverse to widget:
            $(this).parents(settings.widgetSelector)
             
                // Remove the old colour class:
                .removeClass(thisWidgetColorClass[0])
                 
                // Add new colour class (nb ‘this’ refers to clicked list item):
                .addClass($(this).attr(‘class’).match(colorStylePattern)[0]);
                 
        }
         
        // Return false, prevent default action:
        return false;
         
    });
});

Окна редактирования теперь полностью функциональны. Весь приведенный выше код находится в addWidgetControls .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
addWidgetControls : function () {
    var iNettuts = this,
        $ = this.jQuery,
        settings = this.settings;
         
    $(settings.widgetSelector, $(settings.columns)).each(function () {
        var thisWidgetSettings = iNettuts.getWidgetSettings(this.id);
         
        if (thisWidgetSettings.removable) {
            $(‘<a href=»#» class=»remove»>CLOSE</a>’).mousedown(function (e) {
                e.stopPropagation();
            }).click(function () {
                if(confirm(‘This widget will be removed, ok?’)) {
                    $(this).parents(settings.widgetSelector).animate({
                        opacity: 0
                    },function () {
                        $(this).wrap(‘<div/>’).parent().slideUp(function () {
                            $(this).remove();
                        });
                    });
                }
                return false;
            }).appendTo($(settings.handleSelector, this));
        }
         
        if (thisWidgetSettings.editable) {
            $(‘<a href=»#» class=»edit»>EDIT</a>’).mousedown(function (e) {
                e.stopPropagation();
            }).toggle(function () {
                $(this).css({backgroundPosition: ‘-66px 0′, width: ’55px’})
                    .parents(settings.widgetSelector)
                        .find(‘.edit-box’).show().find(‘input’).focus();
                return false;
            },function () {
                $(this).css({backgroundPosition: », width: »})
                    .parents(settings.widgetSelector)
                        .find(‘.edit-box’).hide();
                return false;
            }).appendTo($(settings.handleSelector,this));
            $(‘<div class=»edit-box» style=»display:none;»/>’)
                .append(‘<ul><li class=»item»><label>Change the title?</label><input value=»‘ + $(‘h3’,this).text() + ‘»/></li>’)
                .append((function(){
                    var colorList = ‘<li class=»item»><label>Available colors:</label><ul class=»colors»>’;
                    $(thisWidgetSettings.colorClasses).each(function () {
                        colorList += ‘<li class=»‘ + this + ‘»/>’;
                    });
                    return colorList + ‘</ul>’;
                })())
                .append(‘</ul>’)
                .insertAfter($(settings.handleSelector,this));
        }
         
        if (thisWidgetSettings.collapsible) {
            $(‘<a href=»#» class=»collapse»>COLLAPSE</a>’).mousedown(function (e) {
                e.stopPropagation();
            }).toggle(function () {
                $(this).css({backgroundPosition: ‘-38px 0’})
                    .parents(settings.widgetSelector)
                        .find(settings.contentSelector).hide();
                return false;
            },function () {
                $(this).css({backgroundPosition: »})
                    .parents(settings.widgetSelector)
                        .find(settings.contentSelector).show();
                return false;
            }).prependTo($(settings.handleSelector,this));
        }
    });
     
    $(‘.edit-box’).each(function () {
        $(‘input’,this).keyup(function () {
            $(this).parents(settings.widgetSelector).find(‘h3′).text( $(this).val().length>20 ? $(this).val().substr(0,20)+’…’ : $(this).val() );
        });
        $(‘ul.colors li’,this).click(function () {
             
            var colorStylePattern = /\bcolor-[\w]{1,}\b/,
                thisWidgetColorClass = $(this).parents(settings.widgetSelector).attr(‘class’).match(colorStylePattern)
            if (thisWidgetColorClass) {
                $(this).parents(settings.widgetSelector)
                    .removeClass(thisWidgetColorClass[0])
                    .addClass($(this).attr(‘class’).match(colorStylePattern)[0]);
            }
            return false;
             
        });
    });
     
}

Теперь, когда мы написали большую часть JavaScript, мы можем написать метод инициализации и инициализировать скрипт!

1
2
3
4
5
6
// Additional method within ‘iNettuts’ object:
init : function () {
    this.attachStylesheet(‘inettuts.js.css’);
    this.addWidgetControls();
    this.makeSortable();
}

Теперь, чтобы начать все это:

1
2
// Right at the very end of inettuts.js
iNettuts.init();

Для ясности, это общая структура нашего объекта iNettuts в котором объясняется каждый из его методов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
var iNettuts = {
     
    /* Set’s jQuery identifier: */
    jQuery : $,
     
    settings : {
         
        /* Name : settings
         * Type : Object
         * Purpose : Object to store preferences for widget behaviour
         */
          
    },
 
    init : function () {
         
        /* Name : init
         * Type : Function
         * Purpose : Initialise methods to be run when page has loaded.
         */
          
    },
     
    getWidgetSettings : function (id) {
         
        /* Name : getWidgetSettings
         * Type : Function
         * Parameter : id of widget
         * Purpose : Get default and per-widget settings specified in
         * the settings object and return a new object
         * combining the two, giving per-widget settings
         * precedence obviously.
         */
          
    },
     
    addWidgetControls : function () {
         
        /* Name : settings
         * Type : Function
         * Purpose : Adds controls (eg ‘X’ close button) to each widget.
         */
          
    },
     
    attachStylesheet : function (href) {
         
        /* Name : settings
         * Type : Function
         * Parameter : href location of stylesheet to be added
         * Purpose : Creates new link element with specified href and
         * appends to <head>
         */
          
    },
     
    makeSortable : function () {
         
        /* Name : settings
         * Type : Function
         * Purpose : Makes widgets sortable (draggable/droppable) using
         * the jQuery UI ‘sortable’ module.
         */
          
    }
   
};

Готовый проект

Мы полностью закончили, интерфейс должен быть полностью работоспособным. Я тестировал его на своем ПК (под управлением Windows XP) в следующих браузерах: Firefox 2, Firefox 3, Opera 9.5, Safari 3, IE6, IE7 & Chrome.

Примечание: в IE есть несколько проблем. В частности, он не устанавливает размер заполнителя правильно, плюс есть некоторые проблемы с CSS в IE6 (что и следовало ожидать).

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

  • Например, вы можете использовать его в своем блоге, предоставив пользователю возможность сортировать виджеты вашего блога на боковой панели — затем вы можете сохранить их предпочтения в файле cookie, чтобы при возврате пользователя виджеты были в том же порядке.
  • Если вы добавите систему аутентификации пользователя и базу данных, то получите простой iGoogle.
  • Сам плагин «sortable» может быть использован для сортировки любых элементов, они не обязательно должны быть виджетами.

Независимо от того, собираетесь ли вы использовать это в проекте или нет, я надеюсь, что вы чему-то научились сегодня!