// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. (function(exports) { /** * Alignment options for a keyset. * @param {Object=} opt_keyset The keyset to calculate the dimensions for. * Defaults to the current active keyset. */ var AlignmentOptions = function(opt_keyset) { var keyboard = document.getElementById('keyboard'); var keyset = opt_keyset || keyboard.activeKeyset; this.calculate(keyset); } AlignmentOptions.prototype = { /** * The width of a regular key in logical pixels. * @type {number} */ keyWidth: 0, /** * The horizontal space between two keys in logical pixels. * @type {number} */ pitchX: 0, /** * The vertical space between two keys in logical pixels. * @type {number} */ pitchY: 0, /** * The width in logical pixels the row should expand within. * @type {number} */ availableWidth: 0, /** * The x-coordinate in logical pixels of the left most edge of the keyset. * @type {number} */ offsetLeft: 0, /** * The x-coordinate of the right most edge in logical pixels of the keyset. * @type {number} */ offsetRight: 0, /** * The height in logical pixels of all keys. * @type {number} */ keyHeight: 0, /** * The height in logical pixels the keyset should stretch to fit. * @type {number} */ availableHeight: 0, /** * The y-coordinate in logical pixels of the top most edge of the keyset. * @type {number} */ offsetTop: 0, /** * The y-coordinate in logical pixels of the bottom most edge of the keyset. * @type {number} */ offsetBottom: 0, /** * The ideal width of the keyboard container. * @type {number} */ width: 0, /** * The ideal height of the keyboard container. * @type {number} */ height: 0, /** * Recalculates the alignment options for a specific keyset. * @param {Object} keyset The keyset to align. */ calculate: function (keyset) { var rows = keyset.querySelectorAll('kb-row').array(); // Pick candidate row. This is the row with the most keys. var row = rows[0]; var candidateLength = rows[0].childElementCount; for (var i = 1; i < rows.length; i++) { if (rows[i].childElementCount > candidateLength && rows[i].align == RowAlignment.STRETCH) { row = rows[i]; candidateLength = rows[i].childElementCount; } } var allKeys = row.children; // Calculates widths first. // Weight of a single interspace. var pitches = keyset.pitch.split(); var pitchWeightX; var pitchWeightY; pitchWeightX = parseFloat(pitches[0]); pitchWeightY = pitches.length < 2 ? pitchWeightX : parseFloat(pitch[1]); // Sum of all keys in the current row. var keyWeightSumX = 0; for (var i = 0; i < allKeys.length; i++) { keyWeightSumX += allKeys[i].weight; } var interspaceWeightSumX = (allKeys.length -1) * pitchWeightX; // Total weight of the row in X. var totalWeightX = keyWeightSumX + interspaceWeightSumX + keyset.weightLeft + keyset.weightRight; var keyAspectRatio = getKeyAspectRatio(); var totalWeightY = (pitchWeightY * (rows.length - 1)) + keyset.weightTop + keyset.weightBottom; for (var i = 0; i < rows.length; i++) { totalWeightY += rows[i].weight / keyAspectRatio; } // Calculate width and height of the window. var bounds = exports.getKeyboardBounds(); this.width = bounds.width; this.height = bounds.height; var pixelPerWeightX = bounds.width/totalWeightX; var pixelPerWeightY = bounds.height/totalWeightY; if (keyset.align == LayoutAlignment.CENTER) { if (totalWeightX/bounds.width < totalWeightY/bounds.height) { pixelPerWeightY = bounds.height/totalWeightY; pixelPerWeightX = pixelPerWeightY; this.width = Math.floor(pixelPerWeightX * totalWeightX) } else { pixelPerWeightX = bounds.width/totalWeightX; pixelPerWeightY = pixelPerWeightX; this.height = Math.floor(pixelPerWeightY * totalWeightY); } } // Calculate pitch. this.pitchX = Math.floor(pitchWeightX * pixelPerWeightX); this.pitchY = Math.floor(pitchWeightY * pixelPerWeightY); // Convert weight to pixels on x axis. this.keyWidth = Math.floor(DEFAULT_KEY_WEIGHT * pixelPerWeightX); var offsetLeft = Math.floor(keyset.weightLeft * pixelPerWeightX); var offsetRight = Math.floor(keyset.weightRight * pixelPerWeightX); this.availableWidth = this.width - offsetLeft - offsetRight; // Calculates weight to pixels on the y axis. var weightY = Math.floor(DEFAULT_KEY_WEIGHT / keyAspectRatio); this.keyHeight = Math.floor(weightY * pixelPerWeightY); var offsetTop = Math.floor(keyset.weightTop * pixelPerWeightY); var offsetBottom = Math.floor(keyset.weightBottom * pixelPerWeightY); this.availableHeight = this.height - offsetTop - offsetBottom; var dX = bounds.width - this.width; this.offsetLeft = offsetLeft + Math.floor(dX/2); this.offsetRight = offsetRight + Math.ceil(dX/2) var dY = bounds.height - this.height; this.offsetBottom = offsetBottom + dY; this.offsetTop = offsetTop; }, }; /** * A simple binary search. * @param {Array} array The array to search. * @param {number} start The start index. * @param {number} end The end index. * @param {Function<Object>:number} The test function used for searching. * @private * @return {number} The index of the search, or -1 if it was not found. */ function binarySearch_(array, start, end, testFn) { if (start > end) { // No match found. return -1; } var mid = Math.floor((start+end)/2); var result = testFn(mid); if (result == 0) return mid; if (result < 0) return binarySearch_(array, start, mid - 1, testFn); else return binarySearch_(array, mid + 1, end, testFn); } /** * Calculate width and height of the window. * @private * @return {Array.<String, number>} The bounds of the keyboard container. */ function getKeyboardBounds_() { return { "width": screen.width, "height": screen.height * DEFAULT_KEYBOARD_ASPECT_RATIO }; } /** * Calculates the desired key aspect ratio based on screen size. * @return {number} The aspect ratio to use. */ function getKeyAspectRatio() { return (screen.width > screen.height) ? KEY_ASPECT_RATIO_LANDSCAPE : KEY_ASPECT_RATIO_PORTRAIT; } /** * Callback function for when the window is resized. */ var onResize = function() { var keyboard = $('keyboard'); keyboard.stale = true; var keyset = keyboard.activeKeyset; if (keyset) realignAll(); }; /** * Updates a specific key to the position specified. * @param {Object} key The key to update. * @param {number} width The new width of the key. * @param {number} height The new height of the key. * @param {number} left The left corner of the key. * @param {number} top The top corner of the key. */ function updateKey(key, width, height, left, top) { key.style.position = 'absolute'; key.style.width = width + 'px'; key.style.height = (height - KEY_PADDING_TOP - KEY_PADDING_BOTTOM) + 'px'; key.style.left = left + 'px'; key.style.top = (top + KEY_PADDING_TOP) + 'px'; } /** * Returns the key closest to given x-coordinate * @param {Array.<kb-key>} allKeys Sorted array of all possible key * candidates. * @param {number} x The x-coordinate. * @param {number} pitch The pitch of the row. * @param {boolean} alignLeft whether to search with respect to the left or * or right edge. * @return {?kb-key} */ function findClosestKey(allKeys, x, pitch, alignLeft) { // Test function. var testFn = function(i) { var ERROR_THRESH = 1; var key = allKeys[i]; var left = parseFloat(key.style.left); if (!alignLeft) left += parseFloat(key.style.width); var deltaRight = 0.5*(parseFloat(key.style.width) + pitch) deltaLeft = 0.5 * pitch; if (i > 0) deltaLeft += 0.5*parseFloat(allKeys[i-1].style.width); var high = Math.ceil(left + deltaRight) + ERROR_THRESH; var low = Math.floor(left - deltaLeft) - ERROR_THRESH; if (x <= high && x >= low) return 0; return x >= high? 1 : -1; } var index = exports.binarySearch(allKeys, 0, allKeys.length -1, testFn); return index > 0 ? allKeys[index] : null; } /** * Redistributes the total width amongst the keys in the range provided. * @param {Array.<kb-key>} allKeys Ordered list of keys to stretch. * @param {AlignmentOptions} params Options for aligning the keyset. * @param {number} xOffset The x-coordinate of the key who's index is start. * @param {number} width The total extraneous width to distribute. * @param {number} keyHeight The height of each key. * @param {number} yOffset The y-coordinate of the top edge of the row. */ function redistribute(allKeys, params, xOffset, width, keyHeight, yOffset) { var availableWidth = width - (allKeys.length - 1) * params.pitchX; var stretchWeight = 0; var nStretch = 0; for (var i = 0; i < allKeys.length; i++) { var key = allKeys[i]; if (key.stretch) { stretchWeight += key.weight; nStretch++; } else if (key.weight == DEFAULT_KEY_WEIGHT) { availableWidth -= params.keyWidth; } else { availableWidth -= Math.floor(key.weight/DEFAULT_KEY_WEIGHT * params.keyWidth); } } if (stretchWeight <= 0) console.error("Cannot stretch row without a stretchable key"); // Rounding error to distribute. var pixelsPerWeight = availableWidth / stretchWeight; for (var i = 0; i < allKeys.length; i++) { var key = allKeys[i]; var keyWidth = params.keyWidth; if (key.weight != DEFAULT_KEY_WEIGHT) { keyWidth = Math.floor(key.weight/DEFAULT_KEY_WEIGHT * params.keyWidth); } if (key.stretch) { nStretch--; if (nStretch > 0) { keyWidth = Math.floor(key.weight * pixelsPerWeight); availableWidth -= keyWidth; } else { keyWidth = availableWidth; } } updateKey(key, keyWidth, keyHeight, xOffset, yOffset) xOffset += keyWidth + params.pitchX; } } /** * Aligns a row such that the spacebar is perfectly aligned with the row above * it. A precondition is that all keys in this row can be stretched as needed. * @param {!kb-row} row The current row to be aligned. * @param {!kb-row} prevRow The row above the current row. * @param {!AlignmentOptions} params Options for aligning the keyset. * @param {number} keyHeight The height of the keys in this row. * @param {number} heightOffset The height offset caused by the rows above. */ function realignSpacebarRow(row, prevRow, params, keyHeight, heightOffset) { var allKeys = row.children; var stretchWeightBeforeSpace = 0; var stretchBefore = 0; var stretchWeightAfterSpace = 0; var stretchAfter = 0; var spaceIndex = -1; for (var i=0; i< allKeys.length; i++) { if (spaceIndex == -1) { if (allKeys[i].classList.contains('space')) { spaceIndex = i; continue; } else { stretchWeightBeforeSpace += allKeys[i].weight; stretchBefore++; } } else { stretchWeightAfterSpace += allKeys[i].weight; stretchAfter++; } } if (spaceIndex == -1) { console.error("No spacebar found in this row."); return; } var totalWeight = stretchWeightBeforeSpace + stretchWeightAfterSpace + allKeys[spaceIndex].weight; var widthForKeys = params.availableWidth - (params.pitchX * (allKeys.length - 1 )) // Number of pixels to assign per unit weight. var pixelsPerWeight = widthForKeys/totalWeight; // Predicted left edge of the space bar. var spacePredictedLeft = params.offsetLeft + (spaceIndex * params.pitchX) + (stretchWeightBeforeSpace * pixelsPerWeight); var prevRowKeys = prevRow.children; // Find closest keys to the spacebar in order to align it to them. var leftKey = findClosestKey(prevRowKeys, spacePredictedLeft, params.pitchX, true); var spacePredictedRight = spacePredictedLeft + allKeys[spaceIndex].weight * (params.keyWidth/100); var rightKey = findClosestKey(prevRowKeys, spacePredictedRight, params.pitchX, false); var yOffset = params.offsetTop + heightOffset; // Fix left side. var leftEdge = parseFloat(leftKey.style.left); var leftWidth = leftEdge - params.offsetLeft - params.pitchX; var leftKeys = allKeys.array().slice(0, spaceIndex); redistribute(leftKeys, params, params.offsetLeft, leftWidth, keyHeight, yOffset); // Fix right side. var rightEdge = parseFloat(rightKey.style.left) + parseFloat(rightKey.style.width); var spacebarWidth = rightEdge - leftEdge; updateKey(allKeys[spaceIndex], spacebarWidth, keyHeight, leftEdge, yOffset); var rightWidth = params.availableWidth - (rightEdge - params.offsetLeft + params.pitchX); var rightKeys = allKeys.array().slice(spaceIndex + 1); redistribute(rightKeys, params, rightEdge + params.pitchX,//xOffset. rightWidth, keyHeight, yOffset); } /** * Realigns a given row based on the parameters provided. * @param {!kb-row} row The row to realign. * @param {!AlignmentOptions} params The parameters used to align the keyset. * @param {number} keyHeight The height of the keys. * @param {number} heightOffset The offset caused by rows above it. */ function realignRow(row, params, keyHeight, heightOffset) { var all = row.children; var nStretch = 0; var stretchWeightSum = 0; var allSum = 0; // Keeps track of where to distribute pixels caused by round off errors. var deltaWidth = []; for (var i = 0; i < all.length; i++) { deltaWidth.push(0) var key = all[i]; if (key.weight == DEFAULT_KEY_WEIGHT){ allSum += params.keyWidth; } else { var width = Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT) * key.weight); allSum += width; } if (!key.stretch) continue; nStretch++; stretchWeightSum += key.weight; } var nRegular = all.length - nStretch; // Extra space. var extra = params.availableWidth - allSum - (params.pitchX * (all.length -1)); var xOffset = params.offsetLeft; var alignment = row.align; switch (alignment) { case RowAlignment.STRETCH: var extraPerWeight = extra/stretchWeightSum; for (var i = 0; i < all.length; i++) { if (!all[i].stretch) continue; var delta = Math.floor(all[i].weight * extraPerWeight); extra -= delta; deltaWidth[i] = delta; // All left-over pixels assigned to right most stretchable key. nStretch--; if (nStretch == 0) deltaWidth[i] += extra; } break; case RowAlignment.CENTER: xOffset += Math.floor(extra/2) break; case RowAlignment.RIGHT: xOffset += extra; break; default: break; }; var yOffset = params.offsetTop + heightOffset; var left = xOffset; for (var i = 0; i < all.length; i++) { var key = all[i]; var width = params.keyWidth; if (key.weight != DEFAULT_KEY_WEIGHT) width = Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT) * key.weight) width += deltaWidth[i]; updateKey(key, width, keyHeight, left, yOffset) left += (width + params.pitchX); } } /** * Realigns the keysets in all layouts of the keyboard. */ function realignAll() { resizeKeyboardContainer() var keyboard = $('keyboard'); var layoutParams = {}; var idToLayout = function(id) { var parts = id.split('-'); parts.pop(); return parts.join('-'); } var keysets = keyboard.querySelectorAll('kb-keyset').array(); for (var i=0; i< keysets.length; i++) { var keyset = keysets[i]; var layout = idToLayout(keyset.id); // Caches the layouts size parameters since all keysets in the same layout // will have the same specs. if (!(layout in layoutParams)) layoutParams[layout] = new AlignmentOptions(keyset); realignKeyset(keyset, layoutParams[layout]); } exports.recordKeysets(); } /** * Realigns the keysets in the current layout of the keyboard. */ function realign() { var keyboard = $('keyboard'); var params = new AlignmentOptions(); // Check if current window bounds are accurate. resizeKeyboardContainer(params) var layout = keyboard.layout; var keysets = keyboard.querySelectorAll('kb-keyset[id^=' + layout + ']').array(); for (var i = 0; i<keysets.length ; i++) { realignKeyset(keysets[i], params); } keyboard.stale = false; exports.recordKeysets(); } /** * Realigns a given keyset. * @param {Object} keyset The keyset to realign. * @param {!AlignmentOptions} params The parameters used to align the keyset. */ function realignKeyset(keyset, params) { var rows = keyset.querySelectorAll('kb-row').array(); keyset.style.fontSize = (params.availableHeight / FONT_SIZE_RATIO / rows.length) + 'px'; var heightOffset = 0; for (var i = 0; i < rows.length; i++) { var row = rows[i]; var rowHeight = Math.floor(params.keyHeight * (row.weight / DEFAULT_KEY_WEIGHT)); if (row.querySelector('.space') && (i > 1)) { realignSpacebarRow(row, rows[i-1], params, rowHeight, heightOffset) } else { realignRow(row, params, rowHeight, heightOffset); } heightOffset += (rowHeight + params.pitchY); } } /** * Resizes the keyboard container if needed. * @params {AlignmentOptions=} opt_params Optional parameters to use. Defaults * to the parameters of the current active keyset. */ function resizeKeyboardContainer(opt_params) { var params = opt_params ? opt_params : new AlignmentOptions(); if (Math.abs(window.innerHeight - params.height) > RESIZE_THRESHOLD) { // Cannot resize more than 50% of screen height due to crbug.com/338829. window.resizeTo(params.width, params.height); } } addEventListener('resize', onResize); addEventListener('load', onResize); exports.getKeyboardBounds = getKeyboardBounds_; exports.binarySearch = binarySearch_; exports.realignAll = realignAll; })(this); /** * Recursively replace all kb-key-import elements with imported documents. * @param {!Document} content Document to process. */ function importHTML(content) { var dom = content.querySelector('template').createInstance(); var keyImports = dom.querySelectorAll('kb-key-import'); if (keyImports.length != 0) { keyImports.array().forEach(function(element) { if (element.importDoc(content)) { var generatedDom = importHTML(element.importDoc(content)); element.parentNode.replaceChild(generatedDom, element); } }); } return dom; } /** * Flatten the keysets which represents a keyboard layout. */ function flattenKeysets() { var keysets = $('keyboard').querySelectorAll('kb-keyset'); if (keysets.length > 0) { keysets.array().forEach(function(element) { element.flattenKeyset(); }); } } function resolveAudio() { var keyboard = $('keyboard'); keyboard.addSound(Sound.DEFAULT); var nodes = keyboard.querySelectorAll('[sound]').array(); // Get id's of all unique sounds. for (var i = 0; i < nodes.length; i++) { var id = nodes[i].getAttribute('sound'); keyboard.addSound(id); } } // Prevents all default actions of touch. Keyboard should use its own gesture // recognizer. addEventListener('touchstart', function(e) { e.preventDefault() }); addEventListener('touchend', function(e) { e.preventDefault() }); addEventListener('touchmove', function(e) { e.preventDefault() }); addEventListener('polymer-ready', function(e) { flattenKeysets(); resolveAudio(); }); addEventListener('stateChange', function(e) { if (e.detail.value == $('keyboard').activeKeysetId) realignAll(); })