Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / ui / keyboard / resources / main.js
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 (function(exports) {
5   /**
6    * Alignment options for a keyset.
7    * @param {Object=} opt_keyset The keyset to calculate the dimensions for.
8    *    Defaults to the current active keyset.
9    */
10   var AlignmentOptions = function(opt_keyset) {
11     var keyboard = document.getElementById('keyboard');
12     var keyset = opt_keyset || keyboard.activeKeyset;
13     this.calculate(keyset);
14   }
15
16   AlignmentOptions.prototype = {
17     /**
18      * The width of a regular key in logical pixels.
19      * @type {number}
20      */
21     keyWidth: 0,
22
23     /**
24      * The horizontal space between two keys in logical pixels.
25      * @type {number}
26      */
27     pitchX: 0,
28
29     /**
30      * The vertical space between two keys in logical pixels.
31      * @type {number}
32      */
33     pitchY: 0,
34
35     /**
36      * The width in logical pixels the row should expand within.
37      * @type {number}
38      */
39     availableWidth: 0,
40
41     /**
42      * The x-coordinate in logical pixels of the left most edge of the keyset.
43      * @type {number}
44      */
45     offsetLeft: 0,
46
47     /**
48      * The x-coordinate of the right most edge in logical pixels of the keyset.
49      * @type {number}
50      */
51     offsetRight: 0,
52
53     /**
54      * The height in logical pixels of all keys.
55      * @type {number}
56      */
57     keyHeight: 0,
58
59     /**
60      * The height in logical pixels the keyset should stretch to fit.
61      * @type {number}
62      */
63     availableHeight: 0,
64
65     /**
66      * The y-coordinate in logical pixels of the top most edge of the keyset.
67      * @type {number}
68      */
69     offsetTop: 0,
70
71     /**
72      * The y-coordinate in logical pixels of the bottom most edge of the keyset.
73      * @type {number}
74      */
75     offsetBottom: 0,
76
77     /**
78      * The ideal width of the keyboard container.
79      * @type {number}
80      */
81     width: 0,
82
83     /**
84      * The ideal height of the keyboard container.
85      * @type {number}
86      */
87     height: 0,
88
89     /**
90      * Recalculates the alignment options for a specific keyset.
91      * @param {Object} keyset The keyset to align.
92      */
93     calculate: function (keyset) {
94       var rows = keyset.querySelectorAll('kb-row').array();
95       // Pick candidate row. This is the row with the most keys.
96       var row = rows[0];
97       var candidateLength = rows[0].childElementCount;
98       for (var i = 1; i < rows.length; i++) {
99         if (rows[i].childElementCount > candidateLength &&
100             rows[i].align == RowAlignment.STRETCH) {
101           row = rows[i];
102           candidateLength = rows[i].childElementCount;
103         }
104       }
105       var allKeys = row.children;
106
107       // Calculates widths first.
108       // Weight of a single interspace.
109       var pitches = keyset.pitch.split();
110       var pitchWeightX;
111       var pitchWeightY;
112       pitchWeightX = parseFloat(pitches[0]);
113       pitchWeightY = pitches.length < 2 ? pitchWeightX : parseFloat(pitch[1]);
114
115       // Sum of all keys in the current row.
116       var keyWeightSumX = 0;
117       for (var i = 0; i < allKeys.length; i++) {
118         keyWeightSumX += allKeys[i].weight;
119       }
120
121       var interspaceWeightSumX = (allKeys.length -1) * pitchWeightX;
122       // Total weight of the row in X.
123       var totalWeightX = keyWeightSumX + interspaceWeightSumX +
124           keyset.weightLeft + keyset.weightRight;
125       var keyAspectRatio = getKeyAspectRatio();
126       var totalWeightY = (pitchWeightY * (rows.length - 1)) +
127                          keyset.weightTop +
128                          keyset.weightBottom;
129       for (var i = 0; i < rows.length; i++) {
130         totalWeightY += rows[i].weight / keyAspectRatio;
131       }
132       // Calculate width and height of the window.
133       var bounds = exports.getKeyboardBounds();
134
135       this.width = bounds.width;
136       this.height = bounds.height;
137       var pixelPerWeightX = bounds.width/totalWeightX;
138       var pixelPerWeightY = bounds.height/totalWeightY;
139
140       if (keyset.align == LayoutAlignment.CENTER) {
141         if (totalWeightX/bounds.width < totalWeightY/bounds.height) {
142           pixelPerWeightY = bounds.height/totalWeightY;
143           pixelPerWeightX = pixelPerWeightY;
144           this.width = Math.floor(pixelPerWeightX * totalWeightX)
145         } else {
146           pixelPerWeightX = bounds.width/totalWeightX;
147           pixelPerWeightY = pixelPerWeightX;
148           this.height = Math.floor(pixelPerWeightY * totalWeightY);
149         }
150       }
151       // Calculate pitch.
152       this.pitchX = Math.floor(pitchWeightX * pixelPerWeightX);
153       this.pitchY = Math.floor(pitchWeightY * pixelPerWeightY);
154
155       // Convert weight to pixels on x axis.
156       this.keyWidth = Math.floor(DEFAULT_KEY_WEIGHT * pixelPerWeightX);
157       var offsetLeft = Math.floor(keyset.weightLeft * pixelPerWeightX);
158       var offsetRight = Math.floor(keyset.weightRight * pixelPerWeightX);
159       this.availableWidth = this.width - offsetLeft - offsetRight;
160
161       // Calculates weight to pixels on the y axis.
162       var weightY = Math.floor(DEFAULT_KEY_WEIGHT / keyAspectRatio);
163       this.keyHeight = Math.floor(weightY * pixelPerWeightY);
164       var offsetTop = Math.floor(keyset.weightTop * pixelPerWeightY);
165       var offsetBottom = Math.floor(keyset.weightBottom * pixelPerWeightY);
166       this.availableHeight = this.height - offsetTop - offsetBottom;
167
168       var dX = bounds.width - this.width;
169       this.offsetLeft = offsetLeft + Math.floor(dX/2);
170       this.offsetRight = offsetRight + Math.ceil(dX/2)
171
172       var dY = bounds.height - this.height;
173       this.offsetBottom = offsetBottom + dY;
174       this.offsetTop = offsetTop;
175     },
176   };
177
178   /**
179    * A simple binary search.
180    * @param {Array} array The array to search.
181    * @param {number} start The start index.
182    * @param {number} end The end index.
183    * @param {Function<Object>:number} The test function used for searching.
184    * @private
185    * @return {number} The index of the search, or -1 if it was not found.
186    */
187   function binarySearch_(array, start, end, testFn) {
188       if (start > end) {
189         // No match found.
190         return -1;
191       }
192       var mid = Math.floor((start+end)/2);
193       var result = testFn(mid);
194       if (result == 0)
195         return mid;
196       if (result < 0)
197         return binarySearch_(array, start, mid - 1, testFn);
198       else
199         return binarySearch_(array, mid + 1, end, testFn);
200   }
201
202   /**
203    * Calculate width and height of the window.
204    * @private
205    * @return {Array.<String, number>} The bounds of the keyboard container.
206    */
207   function getKeyboardBounds_() {
208     return {
209       "width": screen.width,
210       "height": screen.height * DEFAULT_KEYBOARD_ASPECT_RATIO
211     };
212   }
213
214   /**
215    * Calculates the desired key aspect ratio based on screen size.
216    * @return {number} The aspect ratio to use.
217    */
218   function getKeyAspectRatio() {
219     return (screen.width > screen.height) ?
220         KEY_ASPECT_RATIO_LANDSCAPE : KEY_ASPECT_RATIO_PORTRAIT;
221   }
222
223   /**
224    * Callback function for when the window is resized.
225    */
226   var onResize = function() {
227     var keyboard = $('keyboard');
228     keyboard.stale = true;
229     var keyset = keyboard.activeKeyset;
230     if (keyset)
231       realignAll();
232   };
233
234   /**
235    * Updates a specific key to the position specified.
236    * @param {Object} key The key to update.
237    * @param {number} width The new width of the key.
238    * @param {number} height The new height of the key.
239    * @param {number} left The left corner of the key.
240    * @param {number} top The top corner of the key.
241    */
242   function updateKey(key, width, height, left, top) {
243     key.style.position = 'absolute';
244     key.style.width = width + 'px';
245     key.style.height = (height - KEY_PADDING_TOP - KEY_PADDING_BOTTOM) + 'px';
246     key.style.left = left + 'px';
247     key.style.top = (top + KEY_PADDING_TOP) + 'px';
248   }
249
250   /**
251    * Returns the key closest to given x-coordinate
252    * @param {Array.<kb-key>} allKeys Sorted array of all possible key
253    *     candidates.
254    * @param {number} x The x-coordinate.
255    * @param {number} pitch The pitch of the row.
256    * @param {boolean} alignLeft whether to search with respect to the left or
257    *   or right edge.
258    * @return {?kb-key}
259    */
260   function findClosestKey(allKeys, x, pitch, alignLeft) {
261     // Test function.
262     var testFn = function(i) {
263       var ERROR_THRESH = 1;
264       var key = allKeys[i];
265       var left = parseFloat(key.style.left);
266       if (!alignLeft)
267         left += parseFloat(key.style.width);
268       var deltaRight = 0.5*(parseFloat(key.style.width) + pitch)
269       deltaLeft = 0.5 * pitch;
270       if (i > 0)
271         deltaLeft += 0.5*parseFloat(allKeys[i-1].style.width);
272       var high = Math.ceil(left + deltaRight) + ERROR_THRESH;
273       var low = Math.floor(left - deltaLeft) - ERROR_THRESH;
274       if (x <= high && x >= low)
275         return 0;
276       return x >= high? 1 : -1;
277     }
278     var index = exports.binarySearch(allKeys, 0, allKeys.length -1, testFn);
279     return index > 0 ? allKeys[index] : null;
280   }
281
282   /**
283    * Redistributes the total width amongst the keys in the range provided.
284    * @param {Array.<kb-key>} allKeys Ordered list of keys to stretch.
285    * @param {AlignmentOptions} params Options for aligning the keyset.
286    * @param {number} xOffset The x-coordinate of the key who's index is start.
287    * @param {number} width The total extraneous width to distribute.
288    * @param {number} keyHeight The height of each key.
289    * @param {number} yOffset The y-coordinate of the top edge of the row.
290    */
291   function redistribute(allKeys, params, xOffset, width, keyHeight, yOffset) {
292     var availableWidth = width - (allKeys.length - 1) * params.pitchX;
293     var stretchWeight = 0;
294     var nStretch = 0;
295     for (var i = 0; i < allKeys.length; i++) {
296       var key = allKeys[i];
297       if (key.stretch) {
298         stretchWeight += key.weight;
299         nStretch++;
300       } else if (key.weight == DEFAULT_KEY_WEIGHT) {
301         availableWidth -= params.keyWidth;
302       } else {
303         availableWidth -=
304             Math.floor(key.weight/DEFAULT_KEY_WEIGHT * params.keyWidth);
305       }
306     }
307     if (stretchWeight <= 0)
308       console.error("Cannot stretch row without a stretchable key");
309     // Rounding error to distribute.
310     var pixelsPerWeight = availableWidth / stretchWeight;
311     for (var i = 0; i < allKeys.length; i++) {
312       var key = allKeys[i];
313       var keyWidth = params.keyWidth;
314       if (key.weight != DEFAULT_KEY_WEIGHT) {
315         keyWidth =
316             Math.floor(key.weight/DEFAULT_KEY_WEIGHT * params.keyWidth);
317       }
318       if (key.stretch) {
319         nStretch--;
320         if (nStretch > 0) {
321           keyWidth = Math.floor(key.weight * pixelsPerWeight);
322           availableWidth -= keyWidth;
323         } else {
324           keyWidth = availableWidth;
325         }
326       }
327       updateKey(key, keyWidth, keyHeight, xOffset, yOffset)
328       xOffset += keyWidth + params.pitchX;
329     }
330   }
331
332   /**
333    * Aligns a row such that the spacebar is perfectly aligned with the row above
334    * it. A precondition is that all keys in this row can be stretched as needed.
335    * @param {!kb-row} row The current row to be aligned.
336    * @param {!kb-row} prevRow The row above the current row.
337    * @param {!AlignmentOptions} params Options for aligning the keyset.
338    * @param {number} keyHeight The height of the keys in this row.
339    * @param {number} heightOffset The height offset caused by the rows above.
340    */
341   function realignSpacebarRow(row, prevRow, params, keyHeight, heightOffset) {
342     var allKeys = row.children;
343     var stretchWeightBeforeSpace = 0;
344     var stretchBefore = 0;
345     var stretchWeightAfterSpace = 0;
346     var stretchAfter = 0;
347     var spaceIndex = -1;
348
349     for (var i=0; i< allKeys.length; i++) {
350       if (spaceIndex == -1) {
351         if (allKeys[i].classList.contains('space')) {
352           spaceIndex = i;
353           continue;
354         } else {
355           stretchWeightBeforeSpace += allKeys[i].weight;
356           stretchBefore++;
357         }
358       } else {
359         stretchWeightAfterSpace += allKeys[i].weight;
360         stretchAfter++;
361       }
362     }
363     if (spaceIndex == -1) {
364       console.error("No spacebar found in this row.");
365       return;
366     }
367     var totalWeight = stretchWeightBeforeSpace +
368                       stretchWeightAfterSpace +
369                       allKeys[spaceIndex].weight;
370     var widthForKeys = params.availableWidth -
371                        (params.pitchX * (allKeys.length - 1 ))
372     // Number of pixels to assign per unit weight.
373     var pixelsPerWeight = widthForKeys/totalWeight;
374     // Predicted left edge of the space bar.
375     var spacePredictedLeft = params.offsetLeft +
376                           (spaceIndex * params.pitchX) +
377                           (stretchWeightBeforeSpace * pixelsPerWeight);
378     var prevRowKeys = prevRow.children;
379     // Find closest keys to the spacebar in order to align it to them.
380     var leftKey =
381         findClosestKey(prevRowKeys, spacePredictedLeft, params.pitchX, true);
382
383     var spacePredictedRight = spacePredictedLeft +
384         allKeys[spaceIndex].weight * (params.keyWidth/100);
385
386     var rightKey =
387         findClosestKey(prevRowKeys, spacePredictedRight, params.pitchX, false);
388
389     var yOffset = params.offsetTop + heightOffset;
390     // Fix left side.
391     var leftEdge = parseFloat(leftKey.style.left);
392     var leftWidth = leftEdge - params.offsetLeft - params.pitchX;
393     var leftKeys = allKeys.array().slice(0, spaceIndex);
394     redistribute(leftKeys,
395                  params,
396                  params.offsetLeft,
397                  leftWidth,
398                  keyHeight,
399                  yOffset);
400     // Fix right side.
401     var rightEdge = parseFloat(rightKey.style.left) +
402         parseFloat(rightKey.style.width);
403     var spacebarWidth = rightEdge - leftEdge;
404     updateKey(allKeys[spaceIndex],
405               spacebarWidth,
406               keyHeight,
407               leftEdge,
408               yOffset);
409     var rightWidth =
410         params.availableWidth - (rightEdge - params.offsetLeft + params.pitchX);
411     var rightKeys = allKeys.array().slice(spaceIndex + 1);
412     redistribute(rightKeys,
413                  params,
414                  rightEdge + params.pitchX,//xOffset.
415                  rightWidth,
416                  keyHeight,
417                  yOffset);
418   }
419
420   /**
421    * Realigns a given row based on the parameters provided.
422    * @param {!kb-row} row The row to realign.
423    * @param {!AlignmentOptions} params The parameters used to align the keyset.
424    * @param {number} The height of the keys.
425    * @param {number} heightOffset The offset caused by rows above it.
426    */
427   function realignRow(row, params, keyHeight, heightOffset) {
428     var all = row.children;
429     var nStretch = 0;
430     var stretchWeightSum = 0;
431     var allSum = 0;
432     // Keeps track of where to distribute pixels caused by round off errors.
433     var deltaWidth = [];
434     for (var i = 0; i < all.length; i++) {
435       deltaWidth.push(0)
436       var key = all[i];
437       if (key.weight == DEFAULT_KEY_WEIGHT){
438         allSum += params.keyWidth;
439       } else {
440         var width =
441           Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT) * key.weight);
442         allSum += width;
443       }
444       if (!key.stretch)
445         continue;
446       nStretch++;
447       stretchWeightSum += key.weight;
448     }
449     var nRegular = all.length - nStretch;
450     // Extra space.
451     var extra = params.availableWidth -
452                 allSum -
453                 (params.pitchX * (all.length -1));
454     var xOffset = params.offsetLeft;
455
456     var alignment = row.align;
457     switch (alignment) {
458       case RowAlignment.STRETCH:
459         var extraPerWeight = extra/stretchWeightSum;
460         for (var i = 0; i < all.length; i++) {
461           if (!all[i].stretch)
462             continue;
463           var delta = Math.floor(all[i].weight * extraPerWeight);
464           extra -= delta;
465           deltaWidth[i] = delta;
466           // All left-over pixels assigned to right most stretchable key.
467           nStretch--;
468           if (nStretch == 0)
469             deltaWidth[i] += extra;
470         }
471         break;
472       case RowAlignment.CENTER:
473         xOffset += Math.floor(extra/2)
474         break;
475       case RowAlignment.RIGHT:
476         xOffset += extra;
477         break;
478       default:
479         break;
480     };
481
482     var yOffset = params.offsetTop + heightOffset;
483     var left = xOffset;
484     for (var i = 0; i < all.length; i++) {
485       var key = all[i];
486       var width = params.keyWidth;
487       if (key.weight != DEFAULT_KEY_WEIGHT)
488         width = Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT) * key.weight)
489       width += deltaWidth[i];
490       updateKey(key, width, keyHeight, left, yOffset)
491       left += (width + params.pitchX);
492     }
493   }
494
495   /**
496    * Realigns the keysets in all layouts of the keyboard.
497    */
498   function realignAll() {
499     resizeKeyboardContainer()
500     var keyboard = $('keyboard');
501     var layoutParams = {};
502     var idToLayout = function(id) {
503       var parts = id.split('-');
504       parts.pop();
505       return parts.join('-');
506     }
507
508     var keysets = keyboard.querySelectorAll('kb-keyset').array();
509     for (var i=0; i< keysets.length; i++) {
510       var keyset = keysets[i];
511       var layout = idToLayout(keyset.id);
512       // Caches the layouts size parameters since all keysets in the same layout
513       // will have the same specs.
514       if (!(layout in layoutParams))
515         layoutParams[layout] = new AlignmentOptions(keyset);
516       realignKeyset(keyset, layoutParams[layout]);
517     }
518     exports.recordKeysets();
519   }
520
521   /**
522    * Realigns the keysets in the current layout of the keyboard.
523    */
524   function realign() {
525     var keyboard = $('keyboard');
526     var params = new AlignmentOptions();
527     // Check if current window bounds are accurate.
528     resizeKeyboardContainer(params)
529     var layout = keyboard.layout;
530     var keysets =
531         keyboard.querySelectorAll('kb-keyset[id^=' + layout + ']').array();
532     for (var i = 0; i<keysets.length ; i++) {
533       realignKeyset(keysets[i], params);
534     }
535     keyboard.stale = false;
536     exports.recordKeysets();
537   }
538
539   /*
540    * Realigns a given keyset.
541    * @param {Object} keyset The keyset to realign.
542    * @param {!AlignmentOptions} params The parameters used to align the keyset.
543    */
544   function realignKeyset(keyset, params) {
545     var rows = keyset.querySelectorAll('kb-row').array();
546     keyset.style.fontSize = (params.availableHeight /
547       FONT_SIZE_RATIO / rows.length) + 'px';
548     var heightOffset  = 0;
549     for (var i = 0; i < rows.length; i++) {
550       var row = rows[i];
551       var rowHeight =
552           Math.floor(params.keyHeight * (row.weight / DEFAULT_KEY_WEIGHT));
553       if (row.querySelector('.space') && (i > 1)) {
554         realignSpacebarRow(row, rows[i-1], params, rowHeight, heightOffset)
555       } else {
556         realignRow(row, params, rowHeight, heightOffset);
557       }
558       heightOffset += (rowHeight + params.pitchY);
559     }
560   }
561
562   /**
563    * Resizes the keyboard container if needed.
564    * @params {AlignmentOptions=} opt_params Optional parameters to use. Defaults
565    *   to the parameters of the current active keyset.
566    */
567   function resizeKeyboardContainer(opt_params) {
568     var params = opt_params ? opt_params : new AlignmentOptions();
569     if (Math.abs(window.innerHeight - params.height) > RESIZE_THRESHOLD) {
570       // Cannot resize more than 50% of screen height due to crbug.com/338829.
571       window.resizeTo(params.width, params.height);
572     }
573   }
574
575   addEventListener('resize', onResize);
576   addEventListener('load', onResize);
577
578   exports.getKeyboardBounds = getKeyboardBounds_;
579   exports.binarySearch = binarySearch_;
580   exports.realignAll = realignAll;
581 })(this);
582
583 /**
584  * Recursively replace all kb-key-import elements with imported documents.
585  * @param {!Document} content Document to process.
586  */
587 function importHTML(content) {
588   var dom = content.querySelector('template').createInstance();
589   var keyImports = dom.querySelectorAll('kb-key-import');
590   if (keyImports.length != 0) {
591     keyImports.array().forEach(function(element) {
592       if (element.importDoc(content)) {
593         var generatedDom = importHTML(element.importDoc(content));
594         element.parentNode.replaceChild(generatedDom, element);
595       }
596     });
597   }
598   return dom;
599 }
600
601 /**
602   * Flatten the keysets which represents a keyboard layout.
603   */
604 function flattenKeysets() {
605   var keysets = $('keyboard').querySelectorAll('kb-keyset');
606   if (keysets.length > 0) {
607     keysets.array().forEach(function(element) {
608       element.flattenKeyset();
609     });
610   }
611 }
612
613 function resolveAudio() {
614   var keyboard = $('keyboard');
615   keyboard.addSound(Sound.DEFAULT);
616   var nodes = keyboard.querySelectorAll('[sound]').array();
617   // Get id's of all unique sounds.
618   for (var i = 0; i < nodes.length; i++) {
619     var id = nodes[i].getAttribute('sound');
620     keyboard.addSound(id);
621   }
622 }
623
624 // Prevents all default actions of touch. Keyboard should use its own gesture
625 // recognizer.
626 addEventListener('touchstart', function(e) { e.preventDefault() });
627 addEventListener('touchend', function(e) { e.preventDefault() });
628 addEventListener('touchmove', function(e) { e.preventDefault() });
629 addEventListener('polymer-ready', function(e) {
630   flattenKeysets();
631   resolveAudio();
632 });
633 addEventListener('stateChange', function(e) {
634   if (e.detail.value == $('keyboard').activeKeysetId)
635     realignAll();
636 })