Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Практика создания виджетов

Свои события, подписка-уведомление

  1. Свои события на элементах
  2. Свои события на любых объектах
  3. Меню «на событиях»

Паттерн «подписка-уведомление» заключается в том, что произвольные объекты, не обязательно DOM, получают возможность генерировать события. Их получают те, кто подписался на них.

Таким образом разные сущности могут общаться между собой.

Шаблонизация

  1. Шаблонка «на строках»
    1. Синтаксис шаблона
    2. Шаблонка изнутри
  2. Шаблонка «на узлах»
  3. Использование шаблонок
    1. Точки прикрепления
    2. Автоприкрепление
  4. Итого

Шаблон — это заготовка (обычно строка HTML), которая путём подстановки значений (текст сообщения, цена и т.п.) превращается в сообщение/товар и т.п.

Шаблон позволяет вынести текст из скрипта и изложить его в более поддерживаемом виде.

Практика, практика, практика!

Создайте всплывающую подсказку над элементом.

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

Вы можете посмотреть поведение подсказки в ифрейме ниже, наведя курсор на ссылку.

Открыть в новом окне

Способ добавления подсказки к элементу:

new Tooltip(elem, "Вот <b>такая</b> подсказка!");

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

Исходный документ: tutorial/browser/events/tooltip-fixed-src/index.html.

P.S. Возможно, вам понадобятся функции fixEvent и getCoords. Они приложены к документу в файле lib.js.

P.P.S. Можно сверстать подсказки и при помощи CSS, если элементы заранее известны и их мало. JS-подход более универсален и не зависит от вёрстки.

Решение, шаг 1
Решение
Решение, шаг 1

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

  1. Наведение на элемент mouseover
  2. Уход с элемента mouseout

Скелет кода:

function Tooltip(elem, text) {

  var offsetFromElement = 10; // отступ подсказки от элемента 10px
  var tooltipElem; // тут будет DOM-элемент для подсказки

  elem.onmouseout = function() {
    hide(); // спрятать подсказку
  };

  elem.onmouseover = function() {
    show(); // показать подсказку
  };
}

Решение, шаг 2
Решение, шаг 2

Функция show должна при первом показе сгенерировать элемент с подсказкой, а затем — показать в нужном месте страницы.

Для генерации подсказки добавим вспомогательную функцию getTooltipElem(). Она будет возвращать существующий элемент, если он есть, а если нет — генерировать новый.

function getTooltipElem() {
  if (!tooltipElem) {
    tooltipElem = document.createElement('div');
    tooltipElem.className = 'tooltip';
    tooltipElem.innerHTML = text;
  }
  return tooltipElem;
}

Основная настройка вида подсказки будет в CSS-классе tooltip.

Например:

.tooltip {
  position:absolute;
  left: -9999px; /* за пределами экрана */
  z-index:100; /* подсказка должна перекрывать другие элементы */
  background: #F0FFF0;
  padding: 5px;
  border: 1px dashed green;
  text-align: center;
}

Как правильно отпозиционировать подсказку? Для начала, по горизонтали.

Центр подсказки должен быть ровно над центром элемента, который имеет координату getCoords(elem).left + elem.offsetWidth/2.

Если поставить tooltipElem.left в это значение — результат будет выглядеть так:

Дополнительно нужно сдвинуть подсказку на половину собственной ширины влево:

Теперь отпозиционируем по вертикали.

Это гораздо проще: нужно взять координату getCoords(elem).top и вычесть из неё высоту подсказки и дополнительный отступ:

Для того, чтобы определить высоту подсказки offsetHeight, она должна быть в DOM со свойством display, не равным none.

Поэтому мы позиционируем её за пределами видимой области, поставив left:-9999px.

В качестве альтренативы можно было бы использовать для скрытия visibility:hidden. Это свойство, в отличие от display:none, оставляет элементу размеры, просто делает его невидимым.

Полный код решения демонстрирует эти подходы на tutorial/browser/events/tooltip-fixed/index.html.

Создайте всплывающую подсказку, следующую за курсором.

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

Вы можете посмотреть поведение подсказки в ифрейме ниже, наведя курсор на правый-нижний угол.

Открыть в новом окне

Способ добавления подсказки к элементу:

new Tooltip(elem, "Вот <b>такая</b> подсказка!");

Естественные пожелания:

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

Исходный документ: tutorial/browser/events/tooltip-moving-src/index.html.

К нему подключён файл с функцией fixEvent.

Решение, шаг 1
Решение
Решение, шаг 1

Три события имеют отношение к подсказке:

  1. При mouseover — показать.
  2. При mousemove — показать на новом месте.
  3. При mouseout — спрятать.

…Но при заходе на элемент происходят сразу оба события: mouseover и mousemove на нём же. Зачем вызывать код для показа два раза? Можно оставить его только в mousemove.

Стиль элемента подсказки:

.tooltip {
  position:absolute;
  left: -9999px; /* изначально спрятана за пределы экрана */
  z-index:100; /* подсказка должна перекрывать другие элементы */
  ...
}

Решение, шаг 2
Решение, шаг 2

Скелет кода:

function Tooltip(elem, text) {

  var offsetFromCursor = 10; // отступ от курсора 10px
  var tooltipElem; // тут будет элемент-подсказка

  elem.onmouseout = function() {
    hide();                   // спрятать подсказку
  };

  elem.onmousemove = function(e) {
    e = fixEvent(e);
    show(e.pageX, e.pageY);   // показать рядом с курсором
  };

  function getTooltipElem() { // создать элемент-подсказку
    if (!tooltipElem) {
      tooltipElem = document.createElement('div');
      tooltipElem.className = 'tooltip'; // CSS подсказки
      tooltipElem.innerHTML = text;
    }
    return tooltipElem;
  }

  function hide() {
    document.body.removeChild(getTooltipElem());
  };

  function show(pageX, pageY) {
    показать getTooltipElem() для координат курсора pageX/pageY
  }
}

Для показа на нужном месте — как правило, подойдут простые вычисления. Функция show ниже сдвигает элемент на offsetFromCursor относительно переданных её координат курсора:

function show(pageX, pageY) {
  var tooltipElem = getTooltipElem();

  var newLeft = pageX + offsetFromCursor;
  var newTop = pageY + offsetFromCursor;

  tooltipElem.left = newLeft + 'px';
  tooltipElem.top = newTop + 'px';
}

Основной сложный момент здесь — определить, когда подсказка вылезает за правую/нижнюю границы окна. Для этого нужно эти границы вычислить:

var scrollY = window.pageYOffset || document.documentElement.scrollTop;
var winBottom = scrollY + document.documentElement.clientHeight;

var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
var winRight  = scrollX + document.documentElement.clientWidth;

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

Проверим, не вылезет ли подсказка за границы, и если это так — перенесём ее:

if (newLeft + tooltipElem.offsetWidth > winRight) {
  // правая граница подсказки вылезает за экран
  newLeft -= tooltipElem.offsetWidth;
  newLeft -= offsetFromCursor+2; // (*)
}

if (newTop + tooltipElem.offsetHeight > winBottom) {
  // нижняя граница подсказки вылезает за экран
  newTop -= tooltipElem.offsetHeight;
  newTop -= offsetFromCursor+2;  // (**)
}

Здесь есть еще две тонкости.

  1. Для того, чтобы можно было получить offsetHeight/offsetWidth, подсказка должна быть скрыта при помощи visibility:hidden или left/top за пределами экрана, но не display:none.
  2. В строках (*) и (**) подсказку сдвигаем немного левее/выше курсора (на 2 пикселя).

    Главное чтобы подсказка не оказалась под курсором. Если такое произойдет, то подсказка перекроет элемент, и в результате события mousemove станут происходить уже на подсказке, а не на элементе, и их обработка сломается.

Полное решение: tutorial/browser/events/tooltip-moving/index.html.

Реализуйте интерфейс для смены размера изображения мышью.

Пусть изображение «тянется» при захвате его снизу, справа и в правом-нижнем углу, вот так:
Открыть в новом окне

По окончанию смены размеров должно быть событие resize c новыми размерами.

Синтаксис:

var resizeMe = new Resizeable(document.getElementById('heroes'));

resizeMe.on("resize", function(newWidth, newHeight) {
  // вывести изменения
  var info = "ширина:" + newWidth + ", высота:" + newHeight;
  document.getElementById('info').innerHTML = info;
});

Минимальный размер изображения 3x3, меньше ресайзить нельзя.

В исходном документе tutorial/browser/events/resizeable-src/index.html находится картинка handle-se.png для правого-нижнего угла и dom.js, eventer.js с вспомогательными функциями.

Решение, шаг 1
Решение
Решение, шаг 1

Для захвата проще всего создать дополнительные DIV'ы и отпозиционировать их сбоку и справа-снизу IMG. При нажатии на них начинать смену размера.

CSS-структура:

<div class="resize-wrapper" style="width: 503px; height: 285px;">
  <img src="heroes.jpg" id="heroes" style="width:500px;height:282px">
  <div class="resize-handle-s"></div>
  <div class="resize-handle-e"></div>
  <div class="resize-handle-se"></div>
</div>

Внешний DIV подстраивается под размер картинки и имеет position:relative. Внутри него расположены абсолютно позиционированные «ручки» для захвата.

Стиль:

.resize-wrapper {
      position: relative;
    }
    .resize-wrapper img {
      display: block;
    }

    .resize-handle-se {
      width: 16px;
      height: 16px;
      position: absolute;
      bottom: 0;
      right: 0;
      background: url(handle-se.png) no-repeat;
      cursor: se-resize;
    }

    .resize-handle-s {
      height: 3px;
      width:100%;
      position: absolute;
      bottom: 0;
      background: gray;
      cursor: s-resize;
    }

    .resize-handle-e {
      width: 3px;
      height:100%;
      position: absolute;
      right: 0;
      top: 0;
      background: gray;
      cursor: e-resize;
    }

Для обозначения низа используется буква «s» (south, «юг» по-английски), обозначения правой стороны — буква «e» (east, «восток» по-английски).

Использование сторон света для обозначения направления можно часто встретить в скриптах.

Решение, шаг 2
Решение, шаг 2

Алгоритм:

  1. При инициализации IMG оборачивается во внешнюю обертку и обкладывается DIV'ами, на которых ловим mousedown. Обертка нужна, чтобы абсолютно позиционировать DIV'ы внутри неё под/сбоку изображения.
  2. При наступлении mousedown, начинаем ловить document.mousemove и подгонять картинку под размер, а обертку — под картинку.

    Желательно заодно отменить браузерное выделение и Drag’n’Drop, возвратив false из собработчиков событий mousedown и dragstart.

  3. При наступлении mouseup — конец.
Решение, шаг 3
Решение, шаг 3

Более подробно детали отмечены в комментариях к решению: tutorial/browser/events/resizeable/index.html

Реализуйте интерфейс для выбора фрагмента изображения мышью.

Нажмите на изображении и проведите мышью для выбора:
Открыть в новом окне

По окончанию смены размеров должно быть событие crop c left/top координатами и width/height размерами выбранной области.

Синтаксис:

var croppable = new Croppable(document.getElementById('heroes'));

croppable.on("crop", function(dims) {
  // вывести координаты и размеры crop-квадрата относительно изображения
});

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

В исходном документе tutorial/browser/events/croppable-src/index.html находится картинка и dom.js, eventer.js с вспомогательными функциями.

HTML/CSS
Решение
HTML/CSS

Область выделения — это DIV, серого цвета, полупрозрачный, с рамкой:

.crop-area {
  position: absolute;
  background: #F5DEB3;
  opacity: 0.3;
  filter:alpha(opacity=30); /* IE opacity */
  border: 1px dashed black;
}

Решение
Решение

Решение: tutorial/browser/events/croppable/index.html

Обратите внимание: обработчики mousemove/mouseup ставятся на document, не на элемент.

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

Создайте раскрывающееся по наведению меню:

Меню должно нормально реагировать:

  • При движении курсора внутри себя, наведении на ссылки.
  • При наведении курсора на меню, с меню и затем обратно (скрытие-раскрытие).

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

var menu = new SlidingMenu({
  elem: document.getElementById('sweeties') 
});

menu.on("select", function(value) {
  document.getElementById("selected").innerHTML = value;
});

В исходном документе tutorial/browser/animation/sliding-hover-src/index.html находится DOM-структура для меню. К заданию прикреплён файл с функцией animate, delta-функциями и библиотека событий eventer.

Решение
Решение

Решение: .

На основе слайдера из задачи Слайдер создайте графический компонент, который умеет возвращать/получать значение.

Синтаксис:

var slider = new Slider({
  elem: document.getElementById('slider'), 
  max: 100 // слайдер на самой правой позиции соответствует 100
});

Метод setValue устанавливает значение:

slider.setValue(50);

У слайдера должно быть два события: slide при каждом передвижении и change при отпускании мыши (установке значения).

Пример использования:

slider.on("slide", function(value) {
  document.getElementById('slide').innerHTML = value;
});
slider.on("change", function(value) {
  document.getElementById('change').innerHTML = value;
});

В действии:

  • Дизайн слайдера, ширина/высота элементов должна быть изменяемой через CSS, без необходимости трогать JS-код.
  • Центр бегунка должен располагаться над значением. Например, он должен быть в центре для 50 при максимуме 100.

Исходный документ — работающий слайдер из задачи Слайдер : tutorial/browser/events/slider-src/index.html.

Значение <-> позиция
Решение
Значение <-> позиция

Как сопоставить позицию слайдера и значение?

Для этого посмотрим крайние значения слайдера. Допустим, размер бегунка 10px.

Раз центр соответствует значению, то крайнее левое значение будет соответствовать центру на 5px, а крайнее правой — центру на 5px от правой границы:

Соответственно, ширина области изменения будет sliderElem.clientWidth - thumbElem.clientWidth. Далее её можно уже поделить на части, количество пикселей на значение будет:

pixelsPerValue = (sliderElem.clientWidth-thumbElem.clientWidth) / max;

Может получиться так, что это значение будет дробным, меньше единицы. Например, если max = 1000, а ширина слайдера 110 (пробег 100), то будет 0.1 пикселя на значение.

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

Крайнее левое значение thumbElem.style.left равно нулю, крайнее правой — как раз ширине доступной области sliderElem.clientWidth - thumbElem.clientWidth. Поэтому можно получив значение, поделив его на pixelsPerValue:

value = Math.round( newLeft / pixelsPerValue);

Полное решение
Полное решение

Итоговое решение: tutorial/browser/events/slider/index.html.

Создайте виджет окна для чата.

Окно — это DIV, который можно переносить, взявшись за заголовок. По нажатию на «Послать» данные передаются в содержание окна.

Посмотреть пример в новом окне.

Синтаксис:

new DraggableWindow("Чат с Петей");

  • Возможно появление прокрутки внутри окна, если сообщений много. Сообщения не должны вылезать вовне окна.
  • Окно нельзя вытащить за пределы экрана, даже резкими движениями мыши. Влево-вправо-вверх оно вообще не должно вылезать за границу, а вниз — только до заголовка. Попробуйте перемещать его в
    примере, чтобы увидеть.
  • Для задания DOM-структуры окна используйте шаблон. Может быть создано несколько окон.

Исходный документ: tutorial/browser/dnd/window-src/index.html

Подсказки
Решение
Подсказки
  • Так как высота и ширина окна известны, вёрстка внутри может содержать точные пиксельные размеры.
  • При обработке события document.onmousemove, мы вычисляем новые координаты left/top и смотрим, вылезает ли окно за границы. Если да — меняем left/top на максимально возможные, чтобы не вылезало.
  • На форме вешаем обработчик onsubmit, т.к. иначе Enter в поле отправить её на сервер.
Решение

Эта задача является продолжением задачи Переносимые окна.

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

Общее управление окнами должен осуществлять объект WindowManager.

Синтаксис:

WindowManager.addWindow("Чат с Петей");
WindowManager.addWindow("Чат с Машей");

Посмотреть пример в новом окне.

  • Окна перекрывают друг-друга по z-index. Максимальный z-index должен быть ограничен: от 1 до количества окон.
  • Информация от окна к менеджеру передаётся через события.

В качестве исходного документа можно взять решение предыдущей задачи: tutorial/browser/dnd/window/index.html, добавив к нему eventer.js.

Решение

Создайте тройной селектор даты, который выступает как единый компонент. То есть, можно подписываться на "select" сразу у этого компонента.

Конструктор:

var dateSelector = new DateSelector({ 
  yearFrom: 2010,  // начальный год в селекторе
  yearTo: 2020,    // конечный год в селекторе
  value: new Date(2012,2,31) // текущая выбранная дата
});

События:

  • selected — при изменении даты.

Методы:

  • setValue(date, disableEvent) — устанавливает дату date. Если второй аргумент true, то событие не генерируется.
  • getElement() — возвращает DOM-элемент для компоненты для вставки в документ. При необходимости (первый пока) создаёт DOM.

Использование - добавление в документ:

var elem = dateSelector.getElement();
document.getElementById('selector').appendChild(elem);

Использование - подписка на изменение и вывод значения:

dateSelector.on("select", function() {
  document.getElementById('value').innerHTML = this.getValue();
});

Пример в действии:

Исходный документ: tutorial/browser/events/dateselector-src/index.html.

P.S. При выборе месяца дни должны подстраваться под него. Чтобы не было доступно 31 февраля.

Решение

Создайте календарь.

Конструктор:

var calendar = new Calendar({ 
  year: 2012, // календарь для года 2012
  month: 2  // месяц - март (нумерация с нуля!)
});

События:

  • selected — при изменении даты.

Методы:

  • setValue(date, disableEvent) — устанавливает дату date. Если второй аргумент true, то событие не генерируется.
  • getElement() — возвращает DOM-элемент для компоненты для вставки в документ. При необходимости (первый пока) создаёт DOM.

Использование - добавление в документ:

var container = document.getElementById('widget')
container.appendChild(calendar.getElement());

Использование - подписка на изменение и вывод значения:

calendar.on("select", function() {
  document.getElementById('value').innerHTML = this.getValue();
})

Пример в действии:

Исходный документ: tutorial/browser/events/calendar-src/index.html.

Решение

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

Конструктор:

var datePicker = new DatePicker({
  template: template, // шаблон (см. исходный файл внизу)
  year: 2012, // календарь для года 2012
  month: 2 // месяц - март (нумерация с нуля!)
});

События:

  • selected — при изменении даты.

Методы:

  • setValue(date, disableEvent) — устанавливает дату date. Если второй аргумент true, то событие не генерируется.
  • getElement() — возвращает DOM-элемент для компоненты для вставки в документ. При необходимости (первый пока) создаёт DOM.

Использование - добавление в документ:

document.body.appendChild(datePicker.getElement());

Использование - подписка на изменение и вывод значения:

datePicker.on("select", function(value) {
  document.getElementById("value").innerHTML  = value;
});

Пример в действии:

Исходный документ: tutorial/browser/events/datepicker-src/index.html.

В решении используйте готовый компонент — календарь из задачи Календарь.

Решение
Решение

Решение: tutorial/browser/events/datepicker/index.html.

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


Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!