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)