Home | History | Annotate | Download | only in resources
      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} keyHeight 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 })
    637