Автор статьи Натаниэль Библер в блоге Envy.

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

Вы ошиблись кликом? Вы действительно уверены, что хотите этого? Абсолютно уверен?

В разработке программного обеспечения особое внимание уделяется уверенному кодированию. Например, в Confident Ruby Авди Гримм связывает взаимодействие кода с капитаном корабля, отдающим приказы. Никаких колебаний или оговорок, иначе люди могут погибнуть. Эти окна подтверждения представляют такое резервирование. Эта незащищенность взаимодействия вызывает у вас и у ваших пользователей чувство страха и, в конечном счете, неудобный пользовательский опыт.

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

Подтверждение Без подтверждения

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

Pingdom, Svtle и даже видеоигра Destiny от Bungie имеют гораздо лучшие альтернативы. Они избегают немедленного запуска сомнительного действия и вместо этого вводят небольшую вынужденную паузу, которая дает пользователю достаточно времени, чтобы подумать о своем действии и даже отменить его до того, как действие начнется.

На языке Ember это отложенное действие, где время задержки представлено как прогресс. Итак, давайте попробуем реализовать это.

Шаблоны приложений

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

На данный момент мы назовем наш компонент hold-to-trigger, так как компонент просто обеспечивает интерактивность задержки указанного действия и не диктует его представление:

<!-- app/templates/index.hbs -->

{{hold-to-trigger class="kudos" action="giveKudos" delay=250}}
Hold To Give Kudos

{{#hold-to-trigger class="dismantle" action="dismantleWeapon" tagName="button" delay=750}}
  Hold This To Dismantle
{{/hold-to-trigger}}

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

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

Шаблон компонента

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

<!-- app/templates/components/hold-to-trigger.hbs -->

{{yield}}
<span style="{{transitionStyle}}"></span>

Независимо от того, с каким содержимым вызывается компонент, он немедленно возвращается для рендеринга. Затем, после того, как этот контент будет отображен, компонент добавит простой span, которым он будет манипулировать через атрибут style.

Между приложением и шаблоном компонента, который заботится обо всем HTML. Мы оставим весь CSS для представления, стилей и анимации приложению (не волнуйтесь, позже будут примеры). Далее, пришло время для некоторой логики.

Компонент

На данный момент мы будем следить только за событиями мыши, чтобы понять общую логику. Мы всегда будем запускать часы, когда кто-то наведет курсор мыши (начнет щелчок) на компонент. Если они останутся с нажатой мышью на время задержки, компонент выполнит желаемое действие. Если они отведут мышь от компонента или отпустят щелчок раньше времени, мы немедленно отключим часы и никогда не запустим действие.

Поскольку Ember CLI является стандартной средой сборки приложений Ember, мы будем кодировать наш компонент, используя ES6/ES2015:

// app/components/hold-to-trigger.js

import Ember from 'ember';

export default Ember.Component.extend({
  classNameBindings: ['isPressed', 'isTriggered'],
  classNames: ['hold-to-trigger'],
  delay: 750,
  isPressed: false,
  isTriggered: false,


  init: function() {
    this._super();
    this._registerStartHandlers();
  },


  startByEvent: function() {
    if (this._timer) { return; }

    this.off('mouseDown', this, this.startByEvent);
    this.on('mouseLeave', this, this.cancelByEvent);
    this.on('mouseUp', this, this.cancelByEvent);
    this._startTimer();
  },

  cancelByEvent: function() {
    this.off('mouseLeave', this, this.cancelByEvent);
    this.off('mouseUp', this, this.cancelByEvent);
    this._cancelTimer();
  },


  transitionStyle: Ember.computed(function() {
    var delay = this.get('delay');
    var durations = Ember.EnumerableUtils.map([
      '-webkit-transition-duration',
      '-moz-transition-duration',
      'transition-duration'
    ], function(property) {
      return property + ': ' + delay + 'ms';
    }).join(';');

    return Ember.String.htmlSafe(durations);
  }).property('delay'),


  _cancelTimer: function() {
    this._removeTimer();
    this._registerStartHandlers();
  },

  _registerStartHandlers: function() {
    this.on('mouseDown', this, this.startByEvent);
  },

  _removeTimer: function() {
    Ember.run.cancel(this._timer);
    delete this._timer;
    this.set('isPressed', false);
  },

  _startTimer: function() {
    this._timer = Ember.run.later(this, this._timerExpired, this.get('delay'));
    this.set('isPressed', true);
    this.set('isTriggered', false);
  },

  _timerExpired: function() {
    this.set('isTriggered', true);
    this.off('mouseLeave', this, this.cancelByEvent);
    this.off('mouseUp', this, this.cancelByEvent);
    this._removeTimer();
    this.$().blur();
    this.sendAction();
    this._registerStartHandlers();
  }
});

Когда компонент инициализируется, init зарегистрирует обработчик нажатия мыши. Когда мышь нажата, Ember.run.later() регистрируется для запуска _timerExpired после того, как сконфигурированный класс delay и is-pressed добавляется к элементу компонента. Когда _timerExpired выполняется, он выполняет некоторую очистку, устанавливает флаг isTriggered, который добавляет класс is-triggered к элементу компонента, и наш компонент снова начинает ждать следующего события нажатия мыши.

Но как насчет действий по отмене? Пока таймер активен, наш компонент прослушивает движение мыши вверх и вниз, что немедленно Ember.run.cancel() запускает наш таймер, выполняет очистку и снова сбрасывает компонент для следующего события нажатия мыши. В этом случае is-triggered не будет применяться к нашему прогрессу span.

В качестве отступления: есть некоторая очистка и оптимизация обработчика событий, которые мы могли бы сделать, используя this.one() вместо this.on() для событий мыши и, таким образом, полностью избегая вызовов this.off(). Однако, если мы решим добавить события клавиатуры или касания в этот компонент позже, нам снова потребуются стандартные вызовы on() и off() для лучшего взаимодействия.

Теперь сделай это красиво

У нас есть HTML и логика компонента. Давайте сделаем это красиво и посмотрим на это в действии.

Ниже приведен пример таблицы стилей Sass. Если вы хотите использовать его, обязательно npm install --save broccoli-sass ember-cli-autoprefixer и перезапустите ember server:

/* app/styles/app.sass */

.hold-to-trigger
  position: relative

.dismantle
  span
    background-color: #ff0
    bottom: 0
    height: 4px
    left: 0
    position: absolute
    transition: width 250ms linear
    width: 0
    animation: pulsate 0.2s infinite alternate
  &.is-pressed span
    width: 100%
  &.is-triggered span
    transition: none

.kudos
  border-radius: 50%
  border: 2px solid black
  height: 50px
  width: 50px
  span
    background-color: #000
    border-radius: 50%
    display: block
    height: 100%
    transform: scale(0.2)
    transition: transform 250ms linear, background-color 250ms linear
    width: 100%
  &.is-pressed span
    background-color: #f00
    transform: scale(1)
  &.is-triggered span
    transition: none

=keyframes($name)
  @-webkit-keyframes #{$name}
    @content
  @-moz-keyframes #{$name}
    @content
  @-ms-keyframes #{$name}
    @content
  @keyframes #{$name}
    @content

+keyframes(pulsate)
  from
    background-color: #900
  to
    background-color: #f00

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

Когда компонент переходит из своего базового состояния в состояние is-pressed, стиль изменяется, чтобы раскрасить фон, задать масштабирование или изменить ширину. Но элемент имеет правило CSS transition, которое заставляет браузер использовать графический процессор для вычисления различий и анимации изменения в течение заданного времени. Поскольку элемент span нашего компонента устанавливает transition-duration уровня элемента, те, что указаны в приведенном выше CSS, являются просто значениями по умолчанию и переопределяются, чтобы точно соответствовать времени задержки действия компонента.

Когда таймер компонента истекает и действие запускается, состояние is-triggered сбрасывает элемент, полностью отключая переход. Наконец, следующий is-pressed запускает процесс заново.

Если это не имеет смысла или вы просто хотите увидеть еще несколько красивых анимированных картинок, то вот он в действии:

Следующие шаги

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

В будущем поищите последующий пост, в котором я вернусь к этому компоненту, чтобы добавить поддержку сенсорных устройств и мобильных устройств, поддержку клавиатуры, а также роли и состояния WAI-ARIA для программ чтения с экрана.