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.
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 * Recalculates the alignment options for a specific keyset.
79 * @param {Object} keyset The keyset to align.
81 calculate: function (keyset) {
82 var rows = keyset.querySelectorAll('kb-row').array();
83 // Pick candidate row. This is the row with the most keys.
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) {
90 candidateLength = rows[i].childElementCount;
93 var allKeys = row.children;
95 // Calculates widths first.
96 // Weight of a single interspace.
97 var pitches = keyset.pitch.split();
100 pitchWeightX = parseFloat(pitches[0]);
101 pitchWeightY = pitches.length < 2 ? pitchWeightX : parseFloat(pitch[1]);
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;
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;
114 var totalWeightY = (pitchWeightY * (rows.length - 1)) +
117 for (var i = 0; i < rows.length; i++) {
118 totalWeightY += rows[i].weight;
120 // Calculate width and height of the window.
121 var bounds = exports.getKeyboardBounds();
123 var width = bounds.width;
124 var height = bounds.height;
125 var pixelPerWeightX = bounds.width/totalWeightX;
126 var pixelPerWeightY = bounds.height/totalWeightY;
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)
134 pixelPerWeightX = bounds.width/totalWeightX;
135 pixelPerWeightY = pixelPerWeightX;
136 height = Math.floor(pixelPerWeightY * totalWeightY);
140 this.pitchX = Math.floor(pitchWeightX * pixelPerWeightX);
141 this.pitchY = Math.floor(pitchWeightY * pixelPerWeightY);
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;
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;
155 var dX = bounds.width - width;
156 this.offsetLeft = offsetLeft + Math.floor(dX/2);
157 this.offsetRight = offsetRight + Math.ceil(dX/2)
159 var dY = bounds.height - height;
160 this.offsetBottom = offsetBottom + dY;
161 this.offsetTop = offsetTop;
166 * Calculate width and height of the window.
168 * @return {Array.<String, number>} The bounds of the keyboard container.
170 function getKeyboardBounds_() {
172 "width": window.innerWidth,
173 "height": window.innerHeight,
177 * Callback function for when the window is resized.
179 var onResize = function() {
180 var keyboard = $('keyboard');
181 keyboard.stale = true;
182 var keyset = keyboard.activeKeyset;
188 * Keeps track of number of loaded keysets.
189 * @param {number} n The number of keysets.
190 * @param {function()} fn Callback function on completion.
192 var Counter = function(n, fn) {
198 Counter.prototype = {
201 if (this.count == this.nKeysets)
207 * Keeps track of keysets loaded and triggers a realign when all are ready.
210 var alignmentCounter = undefined;
213 * Request realignment for a new keyset that was just loaded.
215 function requestRealign () {
216 var keyboard = $('keyboard');
219 if (!alignmentCounter) {
220 var layout = keyboard.layout;
222 keyboard.querySelectorAll('kb-keyset[id^=' + layout + ']').length;
223 alignmentCounter = new Counter(length, function(){
225 alignmentCounter = undefined;
228 alignmentCounter.tick();
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.
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';
248 * Returns the key closest to given x-coordinate
249 * @param {Array.<kb-key>} allKeys Sorted array of all possible key
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
256 function findClosestKey(allKeys, x, pitch, alignLeft) {
257 var n = allKeys.length;
258 // Simple binary search.
259 var binarySearch = function (start, end, testFn) {
261 console.error("Unable to find key.");
264 var mid = Math.floor((start+end)/2);
265 var result = testFn(mid);
269 return binarySearch(start, mid, testFn);
271 return binarySearch(mid + 1, end, testFn);
274 var testFn = function(i) {
275 var ERROR_THRESH = 1;
276 var key = allKeys[i];
277 var left = parseFloat(key.style.left);
279 left += parseFloat(key.style.width);
280 var deltaRight = 0.5*(parseFloat(key.style.width) + pitch)
281 deltaLeft = 0.5 * pitch;
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)
288 return x >= high? 1 : -1;
291 return binarySearch(0, allKeys.length -1, testFn);
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.
303 function redistribute(allKeys, params, xOffset, width, keyHeight, yOffset) {
304 var availableWidth = width - (allKeys.length - 1) * params.pitchX;
305 var stretchWeight = 0;
307 for (var i = 0; i < allKeys.length; i++) {
308 var key = allKeys[i];
310 stretchWeight += key.weight;
312 } else if (key.weight == DEFAULT_KEY_WEIGHT_X) {
313 availableWidth -= params.keyWidth;
316 Math.floor(key.weight/DEFAULT_KEY_WEIGHT_X * params.keyWidth);
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) {
328 Math.floor(key.weight/DEFAULT_KEY_WEIGHT_X * params.keyWidth);
333 keyWidth = Math.floor(key.weight * pixelsPerWeight);
334 availableWidth -= keyWidth;
336 keyWidth = availableWidth;
339 updateKey(key, keyWidth, keyHeight, xOffset, yOffset)
340 xOffset += keyWidth + params.pitchX;
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.
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;
361 for (var i=0; i< allKeys.length; i++) {
362 if (spaceIndex == -1) {
363 if (allKeys[i].classList.contains('space')) {
367 stretchWeightBeforeSpace += allKeys[i].weight;
371 stretchWeightAfterSpace += allKeys[i].weight;
375 if (spaceIndex == -1) {
376 console.error("No spacebar found in this row.");
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.
393 findClosestKey(prevRowKeys, spacePredictedLeft, params.pitchX, true);
395 var spacePredictedRight = spacePredictedLeft +
396 allKeys[spaceIndex].weight * (params.keyWidth/100);
399 findClosestKey(prevRowKeys, spacePredictedRight, params.pitchX, false);
401 var yOffset = params.offsetTop + heightOffset;
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,
413 var rightEdge = parseFloat(rightKey.style.left) +
414 parseFloat(rightKey.style.width);
415 var spacebarWidth = rightEdge - leftEdge;
416 updateKey(allKeys[spaceIndex],
422 params.availableWidth - (rightEdge - params.offsetLeft + params.pitchX);
423 var rightKeys = allKeys.array().slice(spaceIndex + 1);
424 redistribute(rightKeys,
426 rightEdge + params.pitchX,//xOffset.
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.
439 function realignRow(row, params, keyHeight, heightOffset) {
440 var all = row.children;
442 var stretchWeightSum = 0;
444 // Keeps track of where to distribute pixels caused by round off errors.
446 for (var i = 0; i < all.length; i++) {
449 if (key.weight == DEFAULT_KEY_WEIGHT_X){
450 allSum += params.keyWidth;
453 Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT_X) * key.weight);
459 stretchWeightSum += key.weight;
461 var nRegular = all.length - nStretch;
463 var extra = params.availableWidth -
465 (params.pitchX * (all.length -1));
466 var xOffset = params.offsetLeft;
468 var alignment = row.align;
470 case RowAlignment.STRETCH:
471 var extraPerWeight = extra/stretchWeightSum;
472 for (var i = 0; i < all.length; i++) {
475 var delta = Math.floor(all[i].weight * extraPerWeight);
477 deltaWidth[i] = delta;
478 // All left-over pixels assigned to right most stretchable key.
481 deltaWidth[i] += extra;
484 case RowAlignment.CENTER:
485 xOffset += Math.floor(extra/2)
487 case RowAlignment.RIGHT:
494 var yOffset = params.offsetTop + heightOffset;
496 for (var i = 0; i < all.length; 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);
508 * Realigns the keysets in all layouts of the keyboard.
510 function realignAll() {
511 var keyboard = $('keyboard');
512 var layoutParams = {};
514 var idToLayout = function(id) {
515 var parts = id.split('-');
517 return parts.join('-');
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]);
533 * Realigns the keysets in the current layout of the keyboard.
536 var keyboard = $('keyboard');
537 var params = new AlignmentOptions();
538 var layout = keyboard.layout;
540 keyboard.querySelectorAll('kb-keyset[id^=' + layout + ']').array();
541 for (var i = 0; i<keysets.length ; i++) {
542 realignKeyset(keysets[i], params);
544 keyboard.stale = false;
548 * Realigns a given keyset.
549 * @param {Object} keyset The keyset to realign.
550 * @param {!AlignmentOptions} params The parameters used to align the keyset.
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';
557 var heightOffset = 0;
558 for (var i = 0; i < rows.length; i++) {
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)
565 realignRow(row, params, rowHeight, heightOffset);
567 heightOffset += (rowHeight + params.pitchY);
570 window.addEventListener('realign', requestRealign);
572 addEventListener('resize', onResize);
573 addEventListener('load', onResize);
575 exports.getKeyboardBounds = getKeyboardBounds_;
579 * Recursively replace all kb-key-import elements with imported documents.
580 * @param {!Document} content Document to process.
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);
597 * Replace all kb-key-sequence elements with generated kb-key elements.
598 * @param {!DocumentFragment} importedContent The imported dom structure.
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);
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
614 * 2) Replace all kb-key-sequence elements with generated DOM structures.
615 * @param {!Document} content Document to process.
617 function flattenKeysets(content) {
618 var importedContent = importHTML(content);
619 expandHTML(importedContent);
620 return importedContent;
623 // Prevents all default actions of touch. Keyboard should use its own gesture
625 addEventListener('touchstart', function(e) { e.preventDefault() });
626 addEventListener('touchend', function(e) { e.preventDefault() });
627 addEventListener('touchmove', function(e) { e.preventDefault() });