summaryrefslogtreecommitdiffstats
path: root/client/lib/escapeActions.js
blob: 1297cfb05de82de598eacb0f91658c55fe32298c (plain)
1
2
3
4
5
6
7
8
9
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// Pressing `Escape` should close the last opened “element” and only the last
// one. Components can register themselves using a label a condition, and an
// action. This is used by Popup or inlinedForm for instance. When we press
// escape we execute the action which have a valid condition and his the highest
// in the label hierarchy.
EscapeActions = {
  _actions: [],

  // Executed in order
  hierarchy: [
    'textcomplete',
    'popup-back',
    'popup-close',
    'modalWindow',
    'inlinedForm',
    'detailsPane',
    'multiselection',
    'sidebarView'
  ],

  register: function(label, action, condition = () => true, options = {}) {
    const priority = this.hierarchy.indexOf(label);
    if (priority === -1) {
      throw Error('You must define the label in the EscapeActions hierarchy');
    }

    let enabledOnClick = options.enabledOnClick;
    if (_.isUndefined(enabledOnClick)) {
      enabledOnClick = true;
    }

    let noClickEscapeOn = options.noClickEscapeOn;

    this._actions[priority] = {
      priority,
      condition,
      action,
      noClickEscapeOn,
      enabledOnClick
    };
  },

  executeLowest: function() {
    return this._execute({
      multipleAction: false
    });
  },

  executeAll: function() {
    return this._execute({
      multipleActions: true
    });
  },

  executeUpTo: function(maxLabel) {
    return this._execute({
      maxLabel: maxLabel,
      multipleActions: true
    });
  },

  clickExecute: function(target, maxLabel) {
    return this._execute({
      maxLabel: maxLabel,
      multipleActions: false,
      isClick: true,
      clickTarget: target
    });
  },

  _stopClick: function(action, clickTarget) {
    if (! _.isString(action.noClickEscapeOn))
      return false;
    else
      return $(clickTarget).closest(action.noClickEscapeOn).length > 0;
  },

  _execute: function(options) {
    const maxLabel = options.maxLabel;
    const multipleActions = options.multipleActions;
    const isClick = !! options.isClick;
    const clickTarget = options.clickTarget;

    let executedAtLeastOne = false;
    let maxPriority;

    if (! maxLabel)
      maxPriority = Infinity;
    else
      maxPriority = this.hierarchy.indexOf(maxLabel);

    for (let i = 0; i < this._actions.length; i++) {
      let currentAction = this._actions[i];
      if (currentAction.priority > maxPriority)
        return executedAtLeastOne;

      if (isClick && this._stopClick(currentAction, clickTarget))
        return executedAtLeastOne;

      let isEnabled = currentAction.enabledOnClick || ! isClick;
      if (isEnabled && currentAction.condition()) {
        currentAction.action();
        executedAtLeastOne = true;
        if (! multipleActions)
          return executedAtLeastOne;
      }
    }
    return executedAtLeastOne;
  }
};

// MouseTrap plugin bindGlobal plugin. Adds a bindGlobal method to Mousetrap
// that allows you to bind specific keyboard shortcuts that will still work
// inside a text input field.
//
// usage:
// Mousetrap.bindGlobal('ctrl+s', _saveChanges);
//
// source:
// https://github.com/ccampbell/mousetrap/tree/master/plugins/global-bind
var _globalCallbacks = {};
var _originalStopCallback = Mousetrap.stopCallback;

Mousetrap.stopCallback = function(e, element, combo, sequence) {
  var self = this;

  if (self.paused) {
    return true;
  }

  if (_globalCallbacks[combo] || _globalCallbacks[sequence]) {
    return false;
  }

  return _originalStopCallback.call(self, e, element, combo);
};

Mousetrap.bindGlobal = function(keys, callback, action) {
  var self = this;
  self.bind(keys, callback, action);

  if (keys instanceof Array) {
    for (var i = 0; i < keys.length; i++) {
      _globalCallbacks[keys[i]] = true;
    }
    return;
  }

  _globalCallbacks[keys] = true;
};

// Pressing escape to execute one escape action. We use `bindGloabal` vecause
// the shortcut sould work on textarea and inputs as well.
Mousetrap.bindGlobal('esc', function() {
  EscapeActions.executeLowest();
});

// On a left click on the document, we try to exectute one escape action (eg,
// close the popup). We don't execute any action if the user has clicked on a
// link or a button.
$(document).on('click', function(evt) {
  if (evt.button === 0 &&
    $(evt.target).closest('a,button,.is-editable').length === 0) {
    EscapeActions.clickExecute(evt.target, 'multiselection');
  }
});