| // 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} 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(); |
| }) |