1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * The long-press delay in milliseconds before long-press handler is
7 * invoked.
8 * @const
9 * @type {number}
10 */
11var LONGPRESS_DELAY_MSEC = 500;
12
13/**
14 * The maximum number of elements in one keyset rule.
15 * @const
16 * @type {number}
17 */
18var MAXIMUM_NUM_OF_RULE_ELEMENTS = 3;
19
20/**
21 * The minumum number of elements in one keyset rule.
22 * @const
23 * @type {number}
24 */
25var MINIMUM_NUM_OF_RULE_ELEMENTS = 2;
26
27/**
28 * The index of event type element in the splitted keyset rule.
29 * @const
30 * @type {number}
31 */
32var EVENT_TYPE = 0;
33
34/**
35 * The index of toKeyset element in the splitted keyset rule.
36 * @const
37 * @type {number}
38 */
39var TO_KEYSET = 1;
40
41/**
42 * The index of nextKeyset element in the splitted keyset rule.
43 * @const
44 * @type {number}
45 */
46var NEXT_KEYSET = 2;
47
48/**
49 * The index offset of toKeyset and nextKeyset elements in splitted keyset
50 * rule array and the array in keysetRules.
51 * @const
52 * @type {number}
53 */
54var OFFSET = 1;
55
56/**
57 * The minumum number of elements in one keyset rule.
58 * @const {number}
59 */
60var MINIMUM_NUM_OF_RULE_ELEMENTS = 2;
61
62Polymer('kb-key-base', {
63  repeat: false,
64  invert: false,
65  longPressTimer: null,
66  pointerId: undefined,
67
68  /**
69   * The keyset transition rules. It defines which keyset to transit to on
70   * which key events. It consists at most four rules (down, up, long, dbl).
71   * If no rule is defined for a key event, the event wont trigger a keyset
72   * change.
73   * @type {Object.<string, Array.<string>}
74   */
75  keysetRules: null,
76
77  ready: function() {
78    if (this.toKeyset) {
79      // Parsing keyset rules from toKeyset attribute string.
80      // An rule can be defined as: (down|up|long|dbl):keysetid[:keysetid]
81      // and each rule are separated by a semicolon. The first element
82      // defines the event this rule applies to. The second element defines
83      // what keyset to transit to after event received. The third optional
84      // element defines what keyset to transit to after a key is pressed in
85      // the new keyset. It is useful when you want to transit to a not
86      // locked keyset. For example, after transit to a upper case keyset,
87      // it may make sense to transit back to lower case after user typed
88      // any key at the upper case keyset.
89      var rules =
90          this.toKeyset.replace(/(\r\n|\n|\r| )/g, '').split(';');
91      this.keysetRules = {};
92      var self = this;
93      rules.forEach(function(element) {
94        if (element == '')
95          return;
96        var keyValues = element.split(':', MAXIMUM_NUM_OF_RULE_ELEMENTS);
97        if (keyValues.length < MINIMUM_NUM_OF_RULE_ELEMENTS) {
98          console.error('Invalid keyset rules: ' + element);
99          return;
100        }
101        self.keysetRules[keyValues[EVENT_TYPE]] = [keyValues[TO_KEYSET],
102            (keyValues[NEXT_KEYSET] ? keyValues[NEXT_KEYSET] : null)];
103      });
104    }
105  },
106
107  down: function(event) {
108    this.pointerId = event.pointerId;
109    var detail = this.populateDetails('down');
110    this.fire('key-down', detail);
111    this.longPressTimer = this.generateLongPressTimer();
112  },
113  out: function(event) {
114    this.classList.remove('active');
115    clearTimeout(this.longPressTimer);
116  },
117  up: function(event) {
118    this.generateKeyup();
119  },
120
121  /**
122   * Releases the pressed key programmatically and fires key-up event. This
123   * should be called if a second key is pressed while the first key is not
124   * released yet.
125   */
126  autoRelease: function() {
127    this.generateKeyup();
128  },
129
130  /**
131   * Drops the pressed key.
132   */
133  dropKey: function() {
134    this.classList.remove('active');
135    clearTimeout(this.longPressTimer);
136    this.pointerId = undefined;
137  },
138
139  /**
140   * Populates details for this key, and then fires a key-up event.
141   */
142  generateKeyup: function() {
143    if (this.pointerId === undefined)
144      return;
145
146    // Invalidates the pointerId so the subsequent pointerup event is a
147    // noop.
148    this.pointerId = undefined;
149    clearTimeout(this.longPressTimer);
150    var detail = this.populateDetails('up');
151    this.fire('key-up', detail);
152  },
153
154  /**
155   * Character value associated with the key. Typically, the value is a
156   * single character, but may be multi-character in cases like a ".com"
157   * button.
158   * @return {string}
159   */
160  get charValue() {
161    return this.invert ? this.hintText : (this.char || this.textContent);
162  },
163
164  /**
165   * Hint text value associated with the key. Typically, the value is a
166   * single character.
167   * @return {string}
168   */
169  get hintTextValue() {
170    return this.invert ? (this.char || this.textContent) : this.hintText;
171  },
172
173  /**
174   * Handles a swipe flick that originated from this key.
175   * @param {detail} detail The details of the swipe.
176   */
177  onFlick: function(detail) {
178    if (!(detail.direction & SwipeDirection.UP) || !this.hintTextValue)
179      return;
180    var typeDetails = {char: this.hintTextValue};
181    this.fire('type-key', typeDetails);
182  },
183
184  /**
185   * Returns a subset of the key attributes.
186   * @param {string} caller The id of the function which called
187   *     populateDetails.
188   */
189  populateDetails: function(caller) {
190    var detail = {
191      char: this.charValue,
192      toLayout: this.toLayout,
193      repeat: this.repeat
194    };
195
196    switch (caller) {
197      case ('up'):
198        if (this.keysetRules && this.keysetRules.up != undefined) {
199          detail.toKeyset = this.keysetRules.up[TO_KEYSET - OFFSET];
200          detail.nextKeyset = this.keysetRules.up[NEXT_KEYSET - OFFSET];
201        }
202        break;
203      case ('down'):
204        if (this.keysetRules && this.keysetRules.down != undefined) {
205          detail.toKeyset = this.keysetRules.down[TO_KEYSET - OFFSET];
206          detail.nextKeyset = this.keysetRules.down[NEXT_KEYSET - OFFSET];
207        }
208        break;
209      default:
210        break;
211    }
212    return detail;
213  },
214
215  generateLongPressTimer: function() {
216    return this.async(function() {
217      var detail = {
218        char: this.charValue,
219        hintText: this.hintTextValue
220      };
221      if (this.keysetRules && this.keysetRules.long != undefined) {
222        detail.toKeyset = this.keysetRules.long[TO_KEYSET - OFFSET];
223        detail.nextKeyset = this.keysetRules.long[NEXT_KEYSET - OFFSET];
224      }
225      this.fire('key-longpress', detail);
226    }, null, LONGPRESS_DELAY_MSEC);
227  },
228});
229