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.
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.
10 var AlignmentOptions = function(opt_keyset
) {
11 var keyboard
= document
.getElementById('keyboard');
12 var keyset
= opt_keyset
|| keyboard
.activeKeyset
;
13 this.calculate(keyset
);
16 AlignmentOptions
.prototype = {
18 * The width of a regular key in logical pixels.
24 * The horizontal space between two keys in logical pixels.
30 * The vertical space between two keys in logical pixels.
36 * The width in logical pixels the row should expand within.
42 * The x-coordinate in logical pixels of the left most edge of the keyset.
48 * The x-coordinate of the right most edge in logical pixels of the keyset.
54 * The height in logical pixels of all keys.
60 * The height in logical pixels the keyset should stretch to fit.
66 * The y-coordinate in logical pixels of the top most edge of the keyset.
72 * The y-coordinate in logical pixels of the bottom most edge of the keyset.
78 * The ideal width of the keyboard container.
84 * The ideal height of the keyboard container.
90 * Recalculates the alignment options for a specific keyset.
91 * @param {Object} keyset The keyset to align.
93 calculate: function (keyset
) {
94 var rows
= keyset
.querySelectorAll('kb-row').array();
95 // Pick candidate row. This is the row with the most keys.
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
) {
102 candidateLength
= rows
[i
].childElementCount
;
105 var allKeys
= row
.children
;
107 // Calculates widths first.
108 // Weight of a single interspace.
109 var pitches
= keyset
.pitch
.split();
112 pitchWeightX
= parseFloat(pitches
[0]);
113 pitchWeightY
= pitches
.length
< 2 ? pitchWeightX
: parseFloat(pitch
[1]);
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
;
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)) +
129 for (var i
= 0; i
< rows
.length
; i
++) {
130 totalWeightY
+= rows
[i
].weight
/ keyAspectRatio
;
132 // Calculate width and height of the window.
133 var bounds
= exports
.getKeyboardBounds();
135 this.width
= bounds
.width
;
136 this.height
= bounds
.height
;
137 var pixelPerWeightX
= bounds
.width
/totalWeightX
;
138 var pixelPerWeightY
= bounds
.height
/totalWeightY
;
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
)
146 pixelPerWeightX
= bounds
.width
/totalWeightX
;
147 pixelPerWeightY
= pixelPerWeightX
;
148 this.height
= Math
.floor(pixelPerWeightY
* totalWeightY
);
152 this.pitchX
= Math
.floor(pitchWeightX
* pixelPerWeightX
);
153 this.pitchY
= Math
.floor(pitchWeightY
* pixelPerWeightY
);
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
;
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
;
168 var dX
= bounds
.width
- this.width
;
169 this.offsetLeft
= offsetLeft
+ Math
.floor(dX
/2);
170 this.offsetRight
= offsetRight
+ Math
.ceil(dX
/2)
172 var dY
= bounds
.height
- this.height
;
173 this.offsetBottom
= offsetBottom
+ dY
;
174 this.offsetTop
= offsetTop
;
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.
185 * @return {number} The index of the search, or -1 if it was not found.
187 function binarySearch_(array
, start
, end
, testFn
) {
192 var mid
= Math
.floor((start
+end
)/2);
193 var result
= testFn(mid
);
197 return binarySearch_(array
, start
, mid
- 1, testFn
);
199 return binarySearch_(array
, mid
+ 1, end
, testFn
);
203 * Calculate width and height of the window.
205 * @return {Array.<String, number>} The bounds of the keyboard container.
207 function getKeyboardBounds_() {
209 "width": screen
.width
,
210 "height": screen
.height
* DEFAULT_KEYBOARD_ASPECT_RATIO
215 * Calculates the desired key aspect ratio based on screen size.
216 * @return {number} The aspect ratio to use.
218 function getKeyAspectRatio() {
219 return (screen
.width
> screen
.height
) ?
220 KEY_ASPECT_RATIO_LANDSCAPE
: KEY_ASPECT_RATIO_PORTRAIT
;
224 * Callback function for when the window is resized.
226 var onResize = function() {
227 var keyboard
= $('keyboard');
228 keyboard
.stale
= true;
229 var keyset
= keyboard
.activeKeyset
;
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.
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';
251 * Returns the key closest to given x-coordinate
252 * @param {Array.<kb-key>} allKeys Sorted array of all possible key
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
260 function findClosestKey(allKeys
, x
, pitch
, alignLeft
) {
262 var testFn = function(i
) {
263 var ERROR_THRESH
= 1;
264 var key
= allKeys
[i
];
265 var left
= parseFloat(key
.style
.left
);
267 left
+= parseFloat(key
.style
.width
);
268 var deltaRight
= 0.5*(parseFloat(key
.style
.width
) + pitch
)
269 deltaLeft
= 0.5 * pitch
;
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
)
276 return x
>= high
? 1 : -1;
278 var index
= exports
.binarySearch(allKeys
, 0, allKeys
.length
-1, testFn
);
279 return index
> 0 ? allKeys
[index
] : null;
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.
291 function redistribute(allKeys
, params
, xOffset
, width
, keyHeight
, yOffset
) {
292 var availableWidth
= width
- (allKeys
.length
- 1) * params
.pitchX
;
293 var stretchWeight
= 0;
295 for (var i
= 0; i
< allKeys
.length
; i
++) {
296 var key
= allKeys
[i
];
298 stretchWeight
+= key
.weight
;
300 } else if (key
.weight
== DEFAULT_KEY_WEIGHT
) {
301 availableWidth
-= params
.keyWidth
;
304 Math
.floor(key
.weight
/DEFAULT_KEY_WEIGHT
* params
.keyWidth
);
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
) {
316 Math
.floor(key
.weight
/DEFAULT_KEY_WEIGHT
* params
.keyWidth
);
321 keyWidth
= Math
.floor(key
.weight
* pixelsPerWeight
);
322 availableWidth
-= keyWidth
;
324 keyWidth
= availableWidth
;
327 updateKey(key
, keyWidth
, keyHeight
, xOffset
, yOffset
)
328 xOffset
+= keyWidth
+ params
.pitchX
;
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.
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;
349 for (var i
=0; i
< allKeys
.length
; i
++) {
350 if (spaceIndex
== -1) {
351 if (allKeys
[i
].classList
.contains('space')) {
355 stretchWeightBeforeSpace
+= allKeys
[i
].weight
;
359 stretchWeightAfterSpace
+= allKeys
[i
].weight
;
363 if (spaceIndex
== -1) {
364 console
.error("No spacebar found in this row.");
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.
381 findClosestKey(prevRowKeys
, spacePredictedLeft
, params
.pitchX
, true);
383 var spacePredictedRight
= spacePredictedLeft
+
384 allKeys
[spaceIndex
].weight
* (params
.keyWidth
/100);
387 findClosestKey(prevRowKeys
, spacePredictedRight
, params
.pitchX
, false);
389 var yOffset
= params
.offsetTop
+ heightOffset
;
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
,
401 var rightEdge
= parseFloat(rightKey
.style
.left
) +
402 parseFloat(rightKey
.style
.width
);
403 var spacebarWidth
= rightEdge
- leftEdge
;
404 updateKey(allKeys
[spaceIndex
],
410 params
.availableWidth
- (rightEdge
- params
.offsetLeft
+ params
.pitchX
);
411 var rightKeys
= allKeys
.array().slice(spaceIndex
+ 1);
412 redistribute(rightKeys
,
414 rightEdge
+ params
.pitchX
,//xOffset.
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.
427 function realignRow(row
, params
, keyHeight
, heightOffset
) {
428 var all
= row
.children
;
430 var stretchWeightSum
= 0;
432 // Keeps track of where to distribute pixels caused by round off errors.
434 for (var i
= 0; i
< all
.length
; i
++) {
437 if (key
.weight
== DEFAULT_KEY_WEIGHT
){
438 allSum
+= params
.keyWidth
;
441 Math
.floor((params
.keyWidth
/DEFAULT_KEY_WEIGHT
) * key
.weight
);
447 stretchWeightSum
+= key
.weight
;
449 var nRegular
= all
.length
- nStretch
;
451 var extra
= params
.availableWidth
-
453 (params
.pitchX
* (all
.length
-1));
454 var xOffset
= params
.offsetLeft
;
456 var alignment
= row
.align
;
458 case RowAlignment
.STRETCH
:
459 var extraPerWeight
= extra
/stretchWeightSum
;
460 for (var i
= 0; i
< all
.length
; i
++) {
463 var delta
= Math
.floor(all
[i
].weight
* extraPerWeight
);
465 deltaWidth
[i
] = delta
;
466 // All left-over pixels assigned to right most stretchable key.
469 deltaWidth
[i
] += extra
;
472 case RowAlignment
.CENTER
:
473 xOffset
+= Math
.floor(extra
/2)
475 case RowAlignment
.RIGHT
:
482 var yOffset
= params
.offsetTop
+ heightOffset
;
484 for (var i
= 0; i
< all
.length
; 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
);
496 * Realigns the keysets in all layouts of the keyboard.
498 function realignAll() {
499 resizeKeyboardContainer()
500 var keyboard
= $('keyboard');
501 var layoutParams
= {};
502 var idToLayout = function(id
) {
503 var parts
= id
.split('-');
505 return parts
.join('-');
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
]);
518 exports
.recordKeysets();
522 * Realigns the keysets in the current layout of the keyboard.
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
;
531 keyboard
.querySelectorAll('kb-keyset[id^=' + layout
+ ']').array();
532 for (var i
= 0; i
<keysets
.length
; i
++) {
533 realignKeyset(keysets
[i
], params
);
535 keyboard
.stale
= false;
536 exports
.recordKeysets();
540 * Realigns a given keyset.
541 * @param {Object} keyset The keyset to realign.
542 * @param {!AlignmentOptions} params The parameters used to align the keyset.
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
++) {
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
)
556 realignRow(row
, params
, rowHeight
, heightOffset
);
558 heightOffset
+= (rowHeight
+ params
.pitchY
);
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.
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
);
575 addEventListener('resize', onResize
);
576 addEventListener('load', onResize
);
578 exports
.getKeyboardBounds
= getKeyboardBounds_
;
579 exports
.binarySearch
= binarySearch_
;
580 exports
.realignAll
= realignAll
;
584 * Recursively replace all kb-key-import elements with imported documents.
585 * @param {!Document} content Document to process.
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
);
602 * Flatten the keysets which represents a keyboard layout.
604 function flattenKeysets() {
605 var keysets
= $('keyboard').querySelectorAll('kb-keyset');
606 if (keysets
.length
> 0) {
607 keysets
.array().forEach(function(element
) {
608 element
.flattenKeyset();
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
);
624 // Prevents all default actions of touch. Keyboard should use its own gesture
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
) {
633 addEventListener('stateChange', function(e
) {
634 if (e
.detail
.value
== $('keyboard').activeKeysetId
)