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