Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,206 @@
'use strict';
/* globals KeyView */
(function(exports) {
/**
* AlternativesCharMenuView handles the rendering of the alternatives char menu.
* The interaction is handled by AlternativesCharMenuManager, so this module
* could focus on UI rendering.
*/
function AlternativesCharMenuView(rootElement, altChars, options, viewManager) {
this.rootElement = rootElement;
this.altChars = altChars;
// XXX: need a better to load the dependencies
this.keyWidth = options.keyWidth;
this.screenInPortraitMode = options.screenInPortraitMode;
this.menu = null;
this._rowCount = 0;
this.altKeyTargets = [];
this.viewManager = viewManager;
}
exports.AlternativesCharMenuView = AlternativesCharMenuView;
AlternativesCharMenuView.prototype.MENU_LINE_HEIGHT = 6; // in rem
AlternativesCharMenuView.prototype.show = function(keyElem) {
var content = document.createDocumentFragment();
// XXX: should not cause reflow by ref. innerWidth
var cachedWindowWidth = window.innerWidth;
var left = (cachedWindowWidth / 2 > keyElem.offsetLeft);
this._direction = left ? 'left' : 'right';
var menu = document.createElement('div');
menu.id = 'keyboard-accent-char-menu';
// Make the menu extend to the left hand side.
if (!left) {
menu.classList.add('kbr-menu-left');
}
// Decide the height of the menu
var widthRatio = 0;
if (this.altChars.length > 5) {
widthRatio = Math.ceil(this.altChars.length / 2);
menu.style.top = (keyElem.offsetTop - this.getLineHeight() * 2) + 'px';
this._columnCount = widthRatio;
this._rowCount = 2;
// Specify the width so that it will be folded into 2 rows.
menu.style.width = this.keyWidth * widthRatio + 'px';
menu.classList.add('multi-row');
} else {
// menu height - 4 (top margin of the visual wrapper)
menu.style.top = (keyElem.offsetTop - this.getLineHeight() + 4) + 'px';
this._rowCount = 1;
this._columnCount = this.altChars.length;
}
// Determine the horizontal positioning of the menu
if (left) {
var keyRight = keyElem.offsetLeft + keyElem.offsetWidth;
var posLeft = keyRight - this.keyWidth;
menu.style.left = posLeft + 'px';
if (posLeft === 0) {
menu.classList.add('left-edge');
}
} else {
var menuRight = keyElem.offsetLeft + this.keyWidth;
var posRight = cachedWindowWidth - menuRight;
menu.style.right = posRight + 'px';
if (posRight === 0) {
menu.classList.add('right-edge');
}
}
// Build a key for each alternative
this.altChars.forEach(function(alt, index) {
// TODO: the renderer should not be creating a business logic object,
// let's move it to somewhere else, and/or allow normalization directly
// to take place at LayoutNormalizer
var altKeyTarget = alt.length == 1 ? {
value: alt,
keyCode: alt.charCodeAt(0),
keyCodeUpper: alt.toUpperCase().charCodeAt(0)
} : { 'compositeKey': alt,
value: alt };
// Make each of these alternative keys as wide as the key that
// it is an alternative for, but adjust for the relative number of
// characters in the original and the alternative.
var width = this.keyWidth;
// Only adjust the width when there is one row, since the key width
// would be fixed in 2-row case.
if (alt.length > 1 && this._rowCount === 1) {
// Add some padding to the composite key.
width = this._getCharWidth(alt) + 10;
width = Math.max(width, this.keyWidth);
}
var altKey = new KeyView(altKeyTarget,
{keyWidth: width},
this.viewManager);
altKey.render();
content.appendChild(altKey.element);
this.altKeyTargets.push(altKeyTarget);
}.bind(this));
menu.appendChild(content);
// Adjust menu style
// Put menu in keyboard container
this.rootElement.appendChild(menu);
this.menu = menu;
};
AlternativesCharMenuView.prototype.hide = function() {
this.rootElement.removeChild(this.menu);
};
AlternativesCharMenuView.prototype.getRowCount = function() {
return this._rowCount;
};
AlternativesCharMenuView.prototype.getMenuContainer = function() {
return this.menu;
};
AlternativesCharMenuView.prototype.getBoundingClientRect = function() {
return this.menu.getBoundingClientRect();
};
AlternativesCharMenuView.prototype.getLineHeight = function() {
var scale = parseFloat(document.documentElement.style.fontSize);
return this.MENU_LINE_HEIGHT * scale;
};
AlternativesCharMenuView.prototype._getCharWidth = function(textContent) {
var scaleContext = document.createElement('canvas')
.getContext('2d', { willReadFrequently: true });
var fontSize = this.screenInPortraitMode() ? '2.4rem' : '2.8rem';
scaleContext.font = fontSize + ' sans-serif';
return scaleContext.measureText(textContent).width;
};
AlternativesCharMenuView.prototype.getMenuTarget = function(x, y) {
var menuRect = this.getBoundingClientRect();
// Limit the x,y in the menu
if (x <= menuRect.left) {
x = menuRect.left + 1;
} else if (x >= menuRect.right) {
// Cannot find element if x hit the edge, so -1 here.
x = menuRect.right - 1;
}
var xOffset = 0;
if (this._direction === 'left') {
// menu extends to left
xOffset = x - menuRect.left;
} else {
// menu extends to right
xOffset = menuRect.right - x;
}
// Redirect to upper row
y = y - this.getLineHeight();
if (y <= menuRect.top) {
// Cannot find element if y hit the edge, so +1 here.
y = menuRect.top + 1;
}
var columnCount = this._columnCount;
var targetIndex = Math.floor(xOffset / menuRect.width * columnCount);
if ((menuRect.bottom - y) > this.getLineHeight()) {
targetIndex += columnCount;
}
// UX spec: if the targetIndex is out of the length of the alt chars,
// we want to highlight the last alt char.
if (targetIndex >= this.altKeyTargets.length) {
targetIndex = this.altKeyTargets.length - 1;
}
return this.altKeyTargets[targetIndex];
};
AlternativesCharMenuView.prototype.isMenuTarget = function(target) {
return (this.altKeyTargets.indexOf(target) !== -1);
};
})(window);

View file

@ -0,0 +1,35 @@
'use strict';
(function(exports) {
/**
* BaseView is the base of all simple views in the keyboard app.
* It provides the common interface like, highlight() and hide().
*/
function BaseView(target, options, viewManager) {
this.target = target;
this.options = options;
this.viewManager = viewManager;
}
// element property is the DOM element created by this View instance.
BaseView.prototype.element = null;
BaseView.prototype.highlight = function() {
this.element.classList.add('highlighted');
};
BaseView.prototype.unHighlight = function() {
this.element.classList.remove('highlighted');
};
BaseView.prototype.show = function() {
this.element.classList.remove('hide');
};
BaseView.prototype.hide = function() {
this.element.classList.add('hide');
};
exports.BaseView = BaseView;
})(window);

View file

@ -0,0 +1,257 @@
'use strict';
/* global BaseView */
(function(exports) {
/**
* CandidateView handles the rendering of each candidate, i.e.
* the word suggestion.
*/
function CandidateView() {
BaseView.apply(this, arguments);
}
CandidateView.prototype = Object.create(BaseView.prototype);
CandidateView.prototype.render = function render() {
var span = document.createElement('span');
span.textContent = this.target.text;
span.style.flex = this.options.unit;
// ui/integration test needs this
span.dataset.data = this.target.data;
this.element = span;
this.viewManager.registerView(this.target, this);
};
/**
* CandidateToggleButtonView represents the toggle button to show/hide
* the full candidate panel.
*/
function CandidateToggleButtonView() {
BaseView.apply(this, arguments);
}
CandidateToggleButtonView.prototype = Object.create(BaseView.prototype);
CandidateToggleButtonView.prototype.render = function() {
var toggleButton = document.createElement('div');
toggleButton.classList.add('keyboard-candidate-panel-toggle-button');
toggleButton.classList.add('hide');
this.element = toggleButton;
this.viewManager.registerView(this.target, this);
};
CandidateToggleButtonView.prototype.show = function() {
this.element.classList.remove('hide');
};
CandidateToggleButtonView.prototype.hide = function() {
this.element.classList.add('hide');
};
/**
* CandidatePanelView handles the rendering of candidate panel, used for showing
* word suggestions.
*/
function CandidatePanelView(target, options, viewManager) {
BaseView.apply(this, arguments);
}
// Default value for how many candidates shown in a row.
CandidatePanelView.prototype.COUNT_PER_ROW = 8;
CandidatePanelView.prototype.HEIGHT_IN_REM = 3.2;
CandidatePanelView.prototype.render = function render() {
var candidatePanel = document.createElement('div');
candidatePanel.setAttribute('role', 'group');
candidatePanel.dataset.l10nId = 'wordSuggestions2';
candidatePanel.classList.add('keyboard-candidate-panel');
if (this.options.className) {
candidatePanel.classList.add(this.options.className);
}
var suggestionsContainer = document.createElement('div');
suggestionsContainer.classList.add('suggestions-container');
suggestionsContainer.setAttribute('role', 'listbox');
candidatePanel.appendChild(suggestionsContainer);
this.suggestionsContainer = suggestionsContainer;
// Create toggle button
// we're not getting reference of LayoutManager, so define this manually
var KEYCODE_TOGGLE_CANDIDATE_PANEL = -4;
// TODO: we should not create a business logic object in the view,
// let's move it to somewhere else.
this.toggleButton = new CandidateToggleButtonView({
keyCode: KEYCODE_TOGGLE_CANDIDATE_PANEL },
null,
this.viewManager);
this.toggleButton.render();
candidatePanel.appendChild(this.toggleButton.element);
this.element = candidatePanel;
};
//
// Show candidates
// Each candidate is a string or an array of two strings
//
CandidatePanelView.prototype.showCandidates = function(candidates) {
if (!this.element) {
console.error('CandidatePanelView: CandidatePanel is not ready yet!');
return;
}
var candidatePanel = this.element;
candidatePanel.dataset.candidateIndicator = 0;
this.suggestionsContainer.innerHTML = '';
this.toggleButton.hide();
var docFragment = this.renderCandidatePanel(1, candidates, true);
this.suggestionsContainer.appendChild(docFragment);
};
CandidatePanelView.prototype.renderCandidatePanel = function(rowLimit,
candidates,
indentFirstRow) {
var candidatePanel = this.element;
var docFragment = document.createDocumentFragment();
if (candidates.length === 0) {
candidatePanel.dataset.rowCount = 0;
return docFragment;
}
var rowDiv = document.createElement('div');
rowDiv.classList.add('candidate-row');
if (indentFirstRow) {
rowDiv.classList.add('candidate-row-first');
}
var nowUnit = 0;
var rowCount = 0;
if (rowLimit < 0) {
rowLimit = Number.Infinity;
}
var candidatesLength = candidates.length;
var dummyEl = null;
var i = 0;
candidates.some(function(candidate, index) {
var cand, data;
if (typeof candidate == 'string') {
cand = data = candidate;
} else {
cand = candidate[0];
data = candidate[1];
}
var unit = this.options.widthUnit ? this.options.widthUnit :
(cand.length >> 1) + 1;
// TODO: we should not create a business logic object in the view,
// let's move it to somewhere else.
var candidateView = new CandidateView({ suggestion: true,
selection: true,
text: cand,
data: data },
{ unit: unit },
this.viewManager);
candidateView.render();
nowUnit += unit;
i++;
var needBreak = false;
if (rowCount === 0 && indentFirstRow &&
nowUnit >= this.COUNT_PER_ROW && index != candidatesLength - 1) {
needBreak = true;
}
if (nowUnit > this.COUNT_PER_ROW || needBreak) {
if (rowCount === 0 && needBreak) {
this.toggleButton.show();
// Create a dummy element to represent the toggle button to occupy
// the space that should be taken by it.
dummyEl = document.createElement('span');
dummyEl.style.flex = this.COUNT_PER_ROW - (nowUnit - unit);
rowDiv.appendChild(dummyEl);
}
if (rowCount >= rowLimit - 1) {
return true;
}
docFragment.appendChild(rowDiv);
rowCount++;
// Create the next row
rowDiv = document.createElement('div');
rowDiv.classList.add('candidate-row');
nowUnit = unit;
}
rowDiv.appendChild(candidateView.element);
}, this);
// Insert a dummy element to fill up the flex box to make it look
// like a grid.
if (nowUnit < this.COUNT_PER_ROW &&
typeof this.options.widthUnit === 'undefined') {
dummyEl = document.createElement('span');
dummyEl.style.flex = this.COUNT_PER_ROW - nowUnit;
rowDiv.appendChild(dummyEl);
}
if (i != candidatesLength) {
candidatePanel.dataset.truncated = true;
} else {
delete candidatePanel.dataset.truncated;
}
candidatePanel.dataset.rowCount = rowCount + 1;
candidatePanel.dataset.candidateIndicator =
parseInt(candidatePanel.dataset.candidateIndicator) + i;
docFragment.appendChild(rowDiv);
rowDiv = null;
return docFragment;
};
CandidatePanelView.prototype.showMoreCandidates = function(rowLimit,
candidates) {
if (!candidates) {
return;
}
if (!rowLimit) {
rowLimit = -1;
}
this.suggestionsContainer.appendChild(
this.renderCandidatePanel(rowLimit, candidates));
};
CandidatePanelView.prototype.resetScroll = function() {
this.suggestionsContainer.scrollTop =
this.suggestionsContainer.scrollLeft = 0;
};
CandidatePanelView.prototype.getHeight = function() {
return this.HEIGHT_IN_REM * this.viewManager.getRemToPx();
};
exports.CandidatePanelView = CandidatePanelView;
})(window);

View file

@ -0,0 +1,32 @@
'use strict';
/* global BaseView */
(function(exports) {
function EmojiKeyView() {
BaseView.apply(this, arguments);
}
EmojiKeyView.prototype = Object.create(BaseView.prototype);
EmojiKeyView.prototype.render = function() {
var contentNode = document.createElement('button');
if (this.options.classNames) {
contentNode.classList.add.apply(contentNode.classList,
this.options.classNames);
}
contentNode.classList.add('emoji');
contentNode.innerHTML = this.options.outputChar ||
this.target.value ||
this.target.compositeKey;
this.element = contentNode;
this.viewManager.registerView(this.target, this);
};
exports.EmojiKeyView = EmojiKeyView;
})(window);

View file

@ -0,0 +1,92 @@
'use strict';
(function(exports) {
/**
* HandwritingPadView handles the rendering of handwriting pad and
* interactions on it. Handwriting pad is a canvas element in DOM.
*/
var HandwritingPadView = function(target, options, viewManager) {
this.target = target;
// Not used for now, added to comply with the ViewClass interface.
this.options = options;
this.viewManager = viewManager;
this.element = null;
this.lastX = this.lastY = 0;
};
HandwritingPadView.prototype.render = function render() {
this.element = document.createElement('canvas');
this.element.className = 'handwriting-pad';
this.viewManager.registerView(this.target, this);
};
HandwritingPadView.prototype.drawHandwritingPad =
function drawHandwritingPad(press, start, strokeWidth) {
var ctx = this.element.getContext('2d');
ctx.strokeStyle = '#df4b26';
ctx.lineJoin = 'round';
ctx.lineWidth = strokeWidth;
var point = this._getPressPoint(press);
ctx.beginPath();
if (start) {
this.lastX = point.posX - 1;
this.lastY = point.posY;
}
ctx.moveTo(this.lastX, this.lastY);
ctx.lineTo(point.posX, point.posY);
ctx.closePath();
ctx.stroke();
this.lastX = point.posX;
this.lastY = point.posY;
return point;
};
HandwritingPadView.prototype.clearHandwritingPad =
function clearHandwritingPad() {
var ctx = this.element.getContext('2d');
var width = this.element.width;
var height = this.element.height;
ctx.clearRect(0, 0, width, height);
};
// Get user press point relative position in canvas.
HandwritingPadView.prototype._getPressPoint =
function getPressPoint(press) {
var canvasRect = this.element.getBoundingClientRect();
// Get canvas rectangle's relative top left positon
// in document body.
var posTop = canvasRect.top - document.body.clientTop;
var posLeft = canvasRect.left - document.body.clientLeft;
// Get user press's relative position in canvas
var posX = press.clientX - posLeft;
var posY = press.clientY - posTop;
// Get position in canvas context.
posX *= window.devicePixelRatio;
posY *= window.devicePixelRatio;
return {
posX: posX,
posY: posY
};
};
HandwritingPadView.prototype.resize = function resize(width, height) {
var canvas = this.element;
canvas.width = width * window.devicePixelRatio;
canvas.style.width = width + 'px';
canvas.height = height * window.devicePixelRatio;
canvas.style.height = height + 'px';
};
exports.HandwritingPadView = HandwritingPadView;
})(window);

View file

@ -0,0 +1,186 @@
'use strict';
(function(exports) {
/**
* KeyView handles the rendering of each key on the keyboard.
*/
function KeyView(target, options, viewManager) {
this.target = target;
this.attributeList = options.attributeList || [];
this.classNames = ['keyboard-key'];
// The 'key' role tells an assistive technology that these buttons
// are used for composing text or numbers, and should be easier to
// activate than usual buttons. We keep some keys as buttons so that
// their activation is not performed by mistake.
if (!target.isButton) {
this.attributeList.push({
key: 'role',
value: 'key'
});
}
if (target.isSpecialKey) {
this.classNames.push('special-key');
} else {
if (options.classNames) {
this.classNames = this.classNames.concat(options.classNames);
}
}
if (target.className) {
this.classNames = this.classNames.concat(target.className.split(' '));
}
if (target.disabled) {
this.attributeList.push({
key: 'disabled',
value: 'true'
});
}
var ARIA_LABELS = this.ARIA_LABELS || {};
if (target.ariaLabel || ARIA_LABELS[target.value]) {
this.attributeList.push({
key: 'data-l10n-id',
value: target.ariaLabel || ARIA_LABELS[target.value]
});
} else {
this.attributeList.push({
key: 'aria-label',
value: target.ariaLabel || target.value
});
}
// If this layout requires different rendering for uppercase/lowercase
// buttons, we will also read the another outputChar , and KeyView
// would be smart enough to put two label <span>s in the DOM.
this.outputChar = (options.outputChar) || target.value;
this.altOutputChar = options.altOutputChar;
this.innerRatio = options.innerRatio;
this.outerRatio = options.outerRatio;
this.width = options.keyWidth;
this.viewManager = viewManager;
}
KeyView.prototype.ARIA_LABELS = {
'⇪': 'upperCaseKey2',
'⌫': 'backSpaceKey2',
'&nbsp': 'spaceKey2',
'↵': 'returnKey2',
'.': 'periodKey2',
',': 'commaKey2',
':': 'colonKey2',
';': 'semicolonKey2',
'?': 'questionMarkKey2',
'!': 'exclamationPointKey2',
'(': 'leftBracketKey2',
')': 'rightBracketKey2',
'"': 'doubleQuoteKey2',
'«': 'leftDoubleAngleQuoteKey2',
'»': 'rightDoubleAngleQuoteKey2'
};
KeyView.prototype.render = function render() {
// Create the DOM element for the key.
var contentNode = document.createElement('button');
if (this.classNames) {
contentNode.classList.add.apply(contentNode.classList, this.classNames);
}
if (this.outerRatio && this.outerRatio != 1) {
contentNode.style.flex = this.outerRatio;
}
// Still need this for alternative char menu.
if (this.width) {
contentNode.style.width = this.width + 'px';
}
if (this.attributeList) {
this.attributeList.forEach(function(attribute) {
contentNode.setAttribute(attribute.key, attribute.value);
});
}
var vWrapperNode = document.createElement('span');
vWrapperNode.className = 'visual-wrapper';
if (this.outerRatio !== this.innerRatio) {
vWrapperNode.style.width = 100 * this.innerRatio / this.outerRatio + '%';
}
var labelNode = document.createElement('span');
// Using innerHTML here because some labels (so far only the &nbsp; in the
// space key) can be HTML entities.
labelNode.innerHTML = this.outputChar;
labelNode.className = 'key-element';
labelNode.dataset.label = this.outputChar;
vWrapperNode.appendChild(labelNode);
// We are creating two <span class="key-element"> regardless, for styling
// reason, but for the lowercase one the label is set with altOutputChar,
// if exists.
labelNode = document.createElement('span');
labelNode.innerHTML = this.altOutputChar || this.outputChar;
labelNode.className = 'key-element lowercase';
labelNode.dataset.label = this.altOutputChar || this.outputChar;
vWrapperNode.appendChild(labelNode);
// Add uppercase and lowercase pop-up for highlighted key
labelNode = document.createElement('span');
labelNode.innerHTML = this.outputChar;
labelNode.className = 'uppercase popup';
vWrapperNode.appendChild(labelNode);
labelNode = document.createElement('span');
labelNode.innerHTML = this.target.lowercaseValue;
labelNode.className = 'lowercase popup';
vWrapperNode.appendChild(labelNode);
if (this.target.longPressValue) {
var altNoteNode = document.createElement('div');
altNoteNode.className = 'alt-note';
altNoteNode.textContent = this.target.longPressValue;
vWrapperNode.appendChild(altNoteNode);
}
contentNode.appendChild(vWrapperNode);
// a few dataset properties are retained in bug 1044525 because some css
// and ui/integration tests rely on them.
// Also to not break them we spell keycode instead of keyCode in dataset
contentNode.dataset.keycode = this.target.keyCode;
contentNode.dataset.keycodeUpper = this.target.keyCodeUpper;
if ('targetPage' in this.target) {
contentNode.dataset.targetPage = this.target.targetPage;
}
this.element = contentNode;
this.viewManager.registerView(this.target, this);
};
KeyView.prototype.highlight = function highlight(options) {
options = options || {};
if (options.upperCase) {
this.element.classList.add('uppercase-popup');
} else {
this.element.classList.add('lowercase-popup');
}
this.element.classList.add('highlighted');
};
KeyView.prototype.unHighlight = function unHighlight() {
this.element.classList.remove('highlighted');
this.element.classList.remove('uppercase-popup');
this.element.classList.remove('lowercase-popup');
};
exports.KeyView = KeyView;
})(window);

View file

@ -0,0 +1,142 @@
'use strict';
/* global CandidatePanelView, BaseView, ViewUtils */
(function(exports) {
function LatinCandidateView() {
BaseView.apply(this, arguments);
}
LatinCandidateView.prototype = Object.create(BaseView.prototype);
LatinCandidateView.prototype.render = function render() {
var div = document.createElement('div');
div.setAttribute('role', 'option');
var span = ViewUtils.fitText(div,
this.target.text,
this.options.candidatesLength,
this.options.totalWidth);
span.setAttribute('role', 'presentation');
span.dataset.data = this.target.data;
if (this.options.correction) {
div.classList.add('autocorrect');
}
div.appendChild(span);
this.element = div;
this.viewManager.registerView(this.target, this);
};
function DismissButtonView() {
BaseView.apply(this, arguments);
}
DismissButtonView.prototype = Object.create(BaseView.prototype);
DismissButtonView.prototype.render = function render() {
var dismissButton = document.createElement('div');
dismissButton.classList.add('dismiss-suggestions-button');
dismissButton.classList.add('hide');
dismissButton.setAttribute('role', 'button');
dismissButton.dataset.l10nId = 'dismiss2';
this.element = dismissButton;
this.viewManager.registerView(this.target, this);
};
/**
* LatinCandidatePanelView handles the rendering of latin candidate panel, used
* for showing word suggestions.
*/
function LatinCandidatePanelView() {
CandidatePanelView.apply(this, arguments);
}
LatinCandidatePanelView.prototype = Object.create(CandidatePanelView.prototype);
LatinCandidatePanelView.prototype.HEIGHT_IN_REM = 3.1;
LatinCandidatePanelView.prototype.render = function render() {
var candidatePanel = document.createElement('div');
candidatePanel.setAttribute('role', 'group');
candidatePanel.dataset.l10nId = 'wordSuggestions2';
candidatePanel.classList.add('keyboard-candidate-panel');
if (this.options.className) {
candidatePanel.classList.add(this.options.className);
}
this.dismissButton = new DismissButtonView(
{ isDismissSuggestionsButton: true },
null,
this.viewManager);
this.dismissButton.render();
candidatePanel.appendChild(this.dismissButton.element);
var suggestionsContainer = document.createElement('div');
suggestionsContainer.classList.add('suggestions-container');
suggestionsContainer.setAttribute('role', 'listbox');
candidatePanel.appendChild(suggestionsContainer);
this.suggestionsContainer = suggestionsContainer;
this.element = candidatePanel;
};
LatinCandidatePanelView.prototype.showCandidates = function(candidates) {
if (!this.element) {
console.error('LatinCandidatePanelView: CandidatePanel is not ready yet!');
return;
}
if (candidates.length === 0) {
this.dismissButton.hide();
} else {
this.dismissButton.show();
}
this.suggestionsContainer.innerHTML = '';
candidates.forEach(function buildCandidateEntry(candidate) {
// Make sure all of the candidates are defined
if (!candidate) {
return;
}
var text, data, correction = false;
if (typeof candidate === 'string') {
if (candidate[0] === '*') { // it is an autocorrection candidate
candidate = candidate.substring(1);
correction = true;
}
data = text = candidate;
} else {
text = candidate[0];
data = candidate[1];
}
// TODO: we should not create a business logic object in the view,
// let's move it to somewhere else.
var candidateView = new LatinCandidateView(
{ suggestion: true,
text: text,
data: data },
{ candidatesLength: candidates.length,
correction: correction,
totalWidth: this.options.totalWidth },
this.viewManager);
candidateView.render();
this.suggestionsContainer.appendChild(candidateView.element);
}, this);
};
exports.LatinCandidatePanelView = LatinCandidatePanelView;
})(window);

View file

@ -0,0 +1,364 @@
'use strict';
/* global HandwritingPadView, KeyView, KeyboardEvent,
LatinCandidatePanelView, CandidatePanelView */
(function(exports) {
/**
* Each keyboard layout may have multiple pages, one for default and another
* for symbol input, etc.
* LayoutPageView represents each page in the keyboard.
*/
function LayoutPageView(layout, options, viewManager) {
this.layout = layout;
this.options = options;
this.viewManager = viewManager;
this.isUpperCase = undefined;
this.rows = new Map();
// Each row would contain the following info:
//{ element: , // DOM element for this row
// keys: , // A map to store all the keys.
//}
this.candidatePanel = null;
// Cache the visual data here
this.keyArrays = new Map();
}
LayoutPageView.prototype.render = function render() {
var layout = this.layout;
var content = document.createDocumentFragment();
var container = document.createElement('div');
if (this.options.classNames) {
container.classList.add.apply(container.classList, this.options.classNames);
}
if (layout.specificCssRule) {
container.classList.add(layout.layoutName);
}
// Render candidate panel
if (this.options.showCandidatePanel) {
var candidatePanel = this.createCandidatePanel(this.layout.imEngine);
candidatePanel.render();
container.classList.add('candidate-panel');
container.appendChild(candidatePanel.element);
this.candidatePanel = candidatePanel;
}
// Create canvas for handwriting.
if ('handwritingPadOptions' in layout) {
var target = {
isHandwritingPad: true
};
var handwritingPadView = new HandwritingPadView(target,
null,
this.viewManager);
handwritingPadView.render();
content.appendChild(handwritingPadView.element);
this.handwritingPadView = handwritingPadView;
}
container.classList.add('rows-' + layout.keys.length);
if (layout.keys.length !== 4 &&
layout.keys.length !== 5) {
console.warn('LayoutPageView: Layout to render is neither 4 or 5 rows; ' +
'might not be render with expected height.', layout.keys.length);
}
layout.keys.forEach((function buildKeyboardRow(row, nrow) {
var kbRow = document.createElement('div');
var rowLayoutWidth = 0;
kbRow.classList.add('keyboard-row');
kbRow.classList.add('row' + nrow);
if (nrow === layout.keys.length - 1) {
kbRow.classList.add('keyboard-last-row');
}
var keyCount = 0;
var keyMap = new Map();
// Calculate the layout width for each row first.
row.forEach(function calcRowRatio(key, keyIndex) {
var ratio = key.ratio || 1;
rowLayoutWidth += ratio;
});
if ('handwritingPadOptions' in layout &&
nrow < layout.handwritingPadOptions.rowspan) {
rowLayoutWidth += layout.handwritingPadOptions.ratio;
}
row.forEach((function buildKeyboardColumns(key, keyIndex) {
var ratio = key.ratio || 1;
// One key in layout may be used to create multiple keyViews in
// different pages, so create a unique instance here.
var target = Object.freeze(Object.create(key));
var options = {
classNames: [],
outputChar: key.uppercaseValue,
outerRatio: ratio,
innerRatio: ratio
};
if (layout.keyClassName) {
options.classNames = options.classNames.concat(
layout.keyClassName.split(' '));
}
var layoutWidth = layout.width || 10;
// Adjust the width of the first and the last key if there are less keys
// in this row.
if (layoutWidth != rowLayoutWidth &&
(keyIndex === 0 || keyIndex === row.length - 1)) {
options.outerRatio = ratio + ((layoutWidth - rowLayoutWidth) / 2);
}
options.altOutputChar = key.value;
var keyView = new KeyView(target, options, this.viewManager);
keyView.render();
kbRow.appendChild(keyView.element);
keyMap.set(keyCount, keyView);
keyCount++;
}.bind(this)));
this.rows.set(nrow, {
element: kbRow,
keys: keyMap
});
content.appendChild(kbRow);
}).bind(this));
container.appendChild(content);
container.setAttribute('lang', layout.lang);
this.element = container;
};
// Accepts a state object with two properties.
// Set isUpperCaseLocked to true if locked
// Set isUpperCase to true when uppercase is enabled
// Use false on both of these properties when uppercase is disabled
LayoutPageView.prototype.setUpperCaseLock = function setUpperCaseLock(state) {
this.isUpperCase = (state.isUpperCase || state.isUpperCaseLocked);
// Toggle the entire container to trigger different rendering
// for upper case state.
var container = this.element;
container.classList.toggle('lowercase', !this.isUpperCase);
//XXX: this should be changed to accessing the KeyView directly.
var capsLockKey = container.querySelector(
'button:not([disabled])' +
'[data-keycode="' + KeyboardEvent.DOM_VK_CAPS_LOCK + '"]'
);
if (!capsLockKey) {
return;
}
if (state.isUpperCaseLocked) {
capsLockKey.classList.remove('kbr-key-active');
capsLockKey.classList.add('kbr-key-hold');
} else if (state.isUpperCase) {
capsLockKey.classList.add('kbr-key-active');
capsLockKey.classList.remove('kbr-key-hold');
} else {
capsLockKey.classList.remove('kbr-key-active');
capsLockKey.classList.remove('kbr-key-hold');
}
capsLockKey.setAttribute('aria-pressed',
state.isUpperCaseLocked || state.isUpperCase);
};
LayoutPageView.prototype.hide = function hide() {
delete this.element.dataset.active;
};
LayoutPageView.prototype.show = function show() {
// For automated testing to locate the active pageView
this.element.dataset.active = true;
};
LayoutPageView.prototype.highlightKey = function highlightKey(target) {
var keyView = this.viewManager.getView(target);
keyView.highlight({upperCase: this.isUpperCase});
};
LayoutPageView.prototype.unHighlightKey = function unHighlightKey(target) {
var keyView = this.viewManager.getView(target);
keyView.unHighlight();
};
LayoutPageView.prototype.resize = function resize(totalWidth) {
this.options.totalWidth = totalWidth;
// Set width and height for handwriting pad.
if (this.handwritingPadView) {
var placeHolderWidth = totalWidth / (this.layout.width || 10);
var width = Math.floor(placeHolderWidth *
this.layout.handwritingPadOptions.ratio);
// Get row height
var height = this.rows.get(0).element.clientHeight *
this.layout.handwritingPadOptions.rowspan;
this.handwritingPadView.resize(width, height);
}
};
LayoutPageView.prototype.getVisualData = function getVisualData() {
var totalWidth = this.options.totalWidth;
var keyArray = this.keyArrays.get(totalWidth);
if (keyArray) {
return keyArray;
}
keyArray = [];
// Now that key sizes have been set and adjusted for the row,
// loop again and record the size and position of each. If we
// do this as part of the loop above, we get bad position data.
// We do this in a seperate loop to avoid reflowing
this.rows.forEach(function (row) {
row.keys.forEach(function(keyView) {
var visualKey = keyView.element.querySelector('.visual-wrapper');
keyArray.push({
code: keyView.target.keyCode,
x: visualKey.offsetLeft,
y: visualKey.offsetTop,
width: visualKey.clientWidth,
height: visualKey.clientHeight
});
});
});
this.keyArrays.set(totalWidth, keyArray);
return keyArray;
};
// A factory method to create different instances of candidate panel.
// Right now, it depends on the input method name to show different types
// of candidate panels.
LayoutPageView.prototype.createCandidatePanel = function(inputMethodName) {
var candidatePanel;
var target = {};
var options = {
className: inputMethodName,
totalWidth: this.options.totalWidth
};
switch (inputMethodName) {
case 'latin':
candidatePanel =
new LatinCandidatePanelView(target, options, this.viewManager);
break;
case 'vietnamese':
options.widthUnit = 1;
candidatePanel =
new CandidatePanelView(target, options, this.viewManager);
break;
default:
candidatePanel =
new CandidatePanelView(target, options, this.viewManager);
break;
}
return candidatePanel;
};
LayoutPageView.prototype.resetCandidatePanel = function() {
if (!this.candidatePanel) {
return;
}
this.candidatePanel.resetScroll();
};
LayoutPageView.prototype.showCandidates = function(candidates) {
if (!this.candidatePanel) {
return;
}
this.candidatePanel.showCandidates(candidates);
};
LayoutPageView.prototype.showMoreCandidates = function(rowLimit, candidates) {
if (!this.candidatePanel) {
return;
}
this.candidatePanel.showMoreCandidates(rowLimit, candidates);
};
LayoutPageView.prototype.getNumberOfCandidatesPerRow = function() {
if (!this.candidatePanel) {
return;
}
return this.candidatePanel.COUNT_PER_ROW;
};
LayoutPageView.prototype.getHeight = function() {
var height = this.rows.size * this.getKeyHeight();
if (this.candidatePanel) {
height += this.candidatePanel.getHeight();
}
return height;
};
LayoutPageView.prototype.getKeyHeight = function() {
// Start with top & bottom margin height
var keyHeightInRem = 0.8;
/**
* There are 4 different heights we are using here.
* See CSS variable '--key-height' in keyboard.css.
*/
if (this.viewManager.screenInPortraitMode()) {
if (this.layout.keys.length === 5) {
keyHeightInRem += 3.4;
} else {
keyHeightInRem += 4.5;
}
} else {
if (this.layout.keys.length === 5) {
keyHeightInRem += 2.85;
} else {
keyHeightInRem += 3.8;
}
}
return keyHeightInRem * this.viewManager.getRemToPx();
};
LayoutPageView.prototype.destroy = function() {
if (this.element) {
// Remove DOM from the tree it belongs to, then main document retain
// no reference to it. Unreachable object will be garbage collected.
this.element.remove();
}
};
exports.LayoutPageView = LayoutPageView;
})(window);

View file

@ -0,0 +1,89 @@
'use strict';
/* global LayoutPageView, KeyView, SwipeablePanelView */
(function(exports) {
/**
* SwipeablePageView is used to render a swipeable page,
* like the pages in emoji layout.
*/
function SwipeablePageView() {
LayoutPageView.apply(this, arguments);
}
SwipeablePageView.prototype = Object.create(LayoutPageView.prototype);
SwipeablePageView.prototype.render = function() {
var layout = this.layout;
var container = document.createElement('div');
if (this.options.classNames) {
container.classList.add.apply(container.classList, this.options.classNames);
}
if (layout.specificCssRule) {
container.classList.add(layout.layoutName);
}
var swipeablePanel = new SwipeablePanelView(this.layout,
{ totalWidth: this.options.totalWidth },
this.viewManager);
swipeablePanel.render();
this.swipeablePanel = swipeablePanel;
container.appendChild(swipeablePanel.element);
// Render the bottom row for switching different type of emojis
layout.keys.forEach(function buildKeyboardRow(row) {
var kbRow = document.createElement('div');
kbRow.classList.add('keyboard-row', 'swipe-switching-buttons');
row.forEach(function buildKeyboardColumns(key, keyIndex) {
var ratio = key.ratio || 1;
// One key in layout may be used to create multiple keyViews in
// different pages, so create a unique instance here.
var target = Object.freeze(Object.create(key));
var options = {
classNames: [],
outputChar: key.uppercaseValue,
outerRatio: ratio,
innerRatio: ratio
};
if (layout.keyClassName) {
options.classNames = options.classNames.concat(
layout.keyClassName.split(' '));
}
var keyView = new KeyView(target, options, this.viewManager);
keyView.render();
kbRow.appendChild(keyView.element);
}, this);
container.appendChild(kbRow);
}, this);
// XXX: need this so that the switching keys would be visible.
if (!layout.secondLayout) {
container.classList.add('uppercase-only');
}
this.element = container;
};
SwipeablePageView.prototype.resize = function resize(totalWidth) {
this.options.totalWidth = totalWidth;
this.swipeablePanel.resize(totalWidth);
};
SwipeablePageView.prototype.getHeight = function() {
return this.element.clientHeight;
};
exports.SwipeablePageView = SwipeablePageView;
})(window);

View file

@ -0,0 +1,299 @@
'use strict';
/* global EmojiKeyView, KeyView, SwipingDetector */
(function(exports) {
/**
* SwipeablePanelView is used to render a panel that is swipeable to go through
* several sections.
*/
function SwipeablePanelView(layout, options, viewManager) {
this.layout = layout;
this.options = options;
this.viewManager = viewManager;
this.sections = [];
this.currentSectionIndex = 0;
this.indicators = [];
this.startX = 0;
this.swipingDetector = null;
}
SwipeablePanelView.prototype.COLUMN_COUNT = 6;
SwipeablePanelView.prototype.KEY_COUNT_PER_PANEL = 18;
SwipeablePanelView.prototype.SWIPE_THRESHOLD = 20;
SwipeablePanelView.prototype.SWIPE_FRACTION = 0.25;
SwipeablePanelView.prototype.SWIPE_SPEED = 0.5;
SwipeablePanelView.prototype.render = function() {
var panel = document.createElement('div');
panel.classList.add('swipe-panel');
var i = 0;
while (true) {
var section = this._renderSection(i);
if (!section) {
break;
}
this.sections.push(section);
section.classList.add('swipe-section');
panel.appendChild(section);
i++;
}
// SwipingDetector and SectionIndicator should be initialized
// only when there are more than one sections in panel.
if (this.sections.length > 1) {
this._createSwipingDetector(panel);
this._renderSectionIndicator(panel);
}
this.element = panel;
};
SwipeablePanelView.prototype.gotoSection = function(index) {
// Stay on the current section.
if (index === this.currentSectionIndex) {
var currentPos = this.sections[this.currentSectionIndex].
getBoundingClientRect();
if (currentPos.left !== 0) {
// Move the previous section backwards
if (index > 0) {
this._moveSection(index - 1, -this.options.totalWidth);
}
this._moveSection(index, 0);
// Move the next section forwards
if (index < this.sections.length - 1) {
this._moveSection(index + 1, this.options.totalWidth);
}
}
return;
}
this._moveSection(index, 0);
var forward = (index >= this.currentSectionIndex) ? 1 : -1;
// Move the original section
this._moveSection(this.currentSectionIndex,
-forward * this.options.totalWidth);
this.currentSectionIndex = index;
this._updateIndicator();
};
SwipeablePanelView.prototype.resize = function resize(totalWidth) {
this.options.totalWidth = totalWidth;
this.swipeDetectingPaused = true;
// Reposition sections when resize;
this.sections.forEach(function(section, index) {
var translateX;
if (index === this.currentSectionIndex) {
translateX = 0;
} else if (this.currentSectionIndex > index) {
translateX = -totalWidth;
} else {
translateX = totalWidth;
}
section.style.transform = 'translateX(' + translateX + 'px)';
}, this);
};
SwipeablePanelView.prototype._moveSection = function(index, distance) {
var style = this.sections[index].style;
style.transform = 'translateX(' + distance + 'px)';
style.transition = 'transform ' + 300 + 'ms ease';
};
SwipeablePanelView.prototype._updateIndicator = function() {
// Change the section indicator.
this.indicators.forEach(function(indicator, index) {
indicator.classList.toggle('active',
(index == this.currentSectionIndex));
}, this);
};
SwipeablePanelView.prototype._handleTouchStart = function(evt) {
this.startX = evt.position.clientX;
this.deltaX = 0;
this.swipeDetectingPaused = false;
};
SwipeablePanelView.prototype._handlePan = function(evt) {
if (this.swipeDetectingPaused) {
return;
}
var totalWidth = this.options.totalWidth;
// Clear all transition styles
this.sections.forEach(function(section) {
section.style.transition = '';
});
var currentX = evt.position.clientX;
this.deltaX = currentX - this.startX;
var previous;
var next;
var current;
var forward = this.deltaX < 0;
if (this.currentSectionIndex === 0) {
if (forward) {
this.sections[1].style.transform =
'translateX(' + (totalWidth + this.deltaX) + 'px)';
this.sections[0].style.transform =
'translateX(' + this.deltaX + 'px)';
} else {
this.startX = currentX;
}
} else if (this.currentSectionIndex === this.sections.length - 1) {
previous =
this.sections[this.currentSectionIndex - 1].style;
if (this.deltaX >= 0) {
previous.transform =
'translateX(' + (-totalWidth + this.deltaX) + 'px)';
this.sections[this.currentSectionIndex].style.transform =
'translateX(' + this.deltaX + 'px)';
} else {
this.startX = currentX;
}
} else {
previous = this.sections[this.currentSectionIndex - 1].style;
next = this.sections[this.currentSectionIndex + 1].style;
if (this.deltaX >= 0) {
previous.transform =
'translateX(' + (-totalWidth + this.deltaX) + 'px)';
// If we change direction make sure there isn't any part
// of the page on the other side that stays visible.
next.transform = 'translateX(' + totalWidth + 'px)';
} else {
next.transform = 'translateX(' + (totalWidth + this.deltaX) + 'px)';
// If we change direction make sure there isn't any part
// of the page on the other side that stays visible.
previous.transform =
'translateX(-' + totalWidth + 'px)';
}
current = this.sections[this.currentSectionIndex].style;
current.transform = 'translateX(' + this.deltaX + 'px)';
}
};
SwipeablePanelView.prototype._handleSwipe = function(evt) {
if (this.swipeDetectingPaused) {
return;
}
var totalWidth = this.options.totalWidth;
var targetIndex = this.currentSectionIndex;
var forward = evt.direction === 'left';
var isFarEnough = Math.abs(this.deltaX) > totalWidth * this.SWIPE_FRACTION;
var velocity = evt.vx;
var isFastEnough = Math.abs(velocity) > this.SWIPE_SPEED;
var isSameDirection = velocity === 0 || this.deltaX / velocity >= 0;
if ((isFarEnough || isFastEnough) && isSameDirection) {
if (forward && this.currentSectionIndex < this.sections.length - 1) {
targetIndex = this.currentSectionIndex + 1;
} else if (!forward && this.currentSectionIndex > 0) {
targetIndex = this.currentSectionIndex - 1;
}
}
this.gotoSection(targetIndex);
};
SwipeablePanelView.prototype._renderSection = function(index) {
var panelKeys = this.layout.panelKeys;
var startIndex = this.KEY_COUNT_PER_PANEL * index;
if (startIndex >= panelKeys.length) {
return null;
}
var endIndex = Math.min(startIndex + this.KEY_COUNT_PER_PANEL,
panelKeys.length);
var section = document.createElement('div');
for (var i = startIndex; i < endIndex; i++) {
var keyView = this._createKeyView(panelKeys[i]);
keyView.render();
section.appendChild(keyView.element);
}
// Create a dummy element to make the UI look like a grid.
var itemCount = i - startIndex;
if (itemCount % this.COLUMN_COUNT !== 0) {
var dummyEl = document.createElement('span');
dummyEl.style.flex = this.COLUMN_COUNT - (itemCount % this.COLUMN_COUNT);
section.appendChild(dummyEl);
}
this.element = section;
return section;
};
SwipeablePanelView.prototype._renderSectionIndicator = function(panel) {
var indicatorContainer = document.createElement('div');
indicatorContainer.classList.add('section-indicator-container');
for (var j = 0; j < this.sections.length; j++) {
var dot = document.createElement('span');
dot.classList.add('section-indicator');
this.indicators.push(dot);
indicatorContainer.appendChild(dot);
}
this._updateIndicator();
panel.appendChild(indicatorContainer);
};
SwipeablePanelView.prototype._createSwipingDetector = function(panel) {
this.swipingDetector = new SwipingDetector(panel);
this.swipingDetector.start();
this.swipingDetector.ontouchstart = this._handleTouchStart.bind(this);
this.swipingDetector.onpan = this._handlePan.bind(this);
this.swipingDetector.onswipe = this._handleSwipe.bind(this);
};
SwipeablePanelView.prototype._createKeyView = function(keyObject) {
var target = Object.create(keyObject);
// Set selection property as true, so it won't trigger send key when
// swiping.
target.selection = true;
var keyView;
switch (target.type) {
case 'emoji':
keyView = new EmojiKeyView(target, {}, this.viewManager);
break;
default:
keyView = new KeyView(target, {}, this.viewManager);
break;
}
return keyView;
};
exports.SwipeablePanelView = SwipeablePanelView;
})(window);

View file

@ -0,0 +1,160 @@
'use strict';
(function(exports) {
function SwipingDetector(element) {
this.element = element;
this.state = this.STATE_INIT;
}
SwipingDetector.prototype.EVENT_TYPES = [
'touchstart',
'touchmove',
'touchend'
];
SwipingDetector.prototype.VELOCITY_SMOOTHING = 0.5;
SwipingDetector.prototype.STATE_INIT = 0;
SwipingDetector.prototype.STATE_PANNING = 1;
SwipingDetector.prototype.onpan = null;
SwipingDetector.prototype.onswipe = null;
SwipingDetector.prototype.start = function() {
this.EVENT_TYPES.forEach(function(type) {
this.element.addEventListener(type, this);
}, this);
};
SwipingDetector.prototype.stop = function() {
this.EVENT_TYPES.forEach(function(type) {
this.element.removeEventListener(type, this);
}, this);
};
SwipingDetector.prototype.handleEvent = function(evt) {
var current;
var touch;
switch (evt.type) {
case 'touchstart':
if (this.state === this.STATE_PANNING) {
return;
}
touch = evt.touches[0];
this.touchStartTimestamp = evt.timeStamp;
this.startX = touch.pageX;
this.startCoords = this.getCoordinates(evt, touch);
this.last = this.startCoords;
this.deltaX = 0;
this.touchID = touch.identifier;
this.vx = this.vy = null;
if (typeof this.ontouchstart === 'function') {
this.ontouchstart({
position: this.getCoordinates(evt, touch)
});
}
break;
case 'touchmove':
if (evt.timeStamp == this.last.timeStamp) {
return;
}
touch = evt.touches[0];
if (touch.identifier != this.touchID) {
return;
}
this.state = this.STATE_PANNING;
current = this.getCoordinates(evt, touch);
if (typeof this.onpan === 'function') {
this.onpan({
dx: current.screenX - this.startX,
position: this.getCoordinates(evt, touch)
});
}
var dt = current.timeStamp - this.last.timeStamp;
var vx = (current.screenX - this.last.screenX) / dt;
var vy = (current.screenY - this.last.screenY) / dt;
if (this.vx == null) { // first time; no average
this.vx = vx;
this.vy = vy;
} else {
this.vx = this.vx * this.VELOCITY_SMOOTHING +
vx * (1 - this.VELOCITY_SMOOTHING);
this.vy = this.vy * this.VELOCITY_SMOOTHING +
vy * (1 - this.VELOCITY_SMOOTHING);
}
this.last = current;
break;
case 'touchend':
for (var i = 0; i < evt.changedTouches.length; i++) {
if (evt.changedTouches[i].identifier !== this.touchID) {
continue;
}
// Emit a swipe event when the finger goes up.
// Report start and end point, dx, dy, dt, velocity and direction
current = this.getCoordinates(evt, evt.changedTouches[i]);
var dx = current.screenX - this.startCoords.screenX;
var dy = current.screenY - this.startCoords.screenY;
// angle is a positive number of degrees, starting at 0 on the
// positive x axis and increasing clockwise.
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
if (angle < 0) {
angle += 360;
}
var direction;
if (angle >= 315 || angle < 45) {
direction = 'right';
} else if (angle >= 45 && angle < 135) {
direction = 'down';
} else if (angle >= 135 && angle < 225) {
direction = 'left';
} else if (angle >= 225 && angle < 315) {
direction = 'up';
}
if (typeof this.onswipe === 'function') {
this.onswipe({
start: this.startCoords,
end: current,
dx: dx,
dy: dy,
dt: evt.timeStamp - this.startCoords.timeStamp,
vx: this.vx,
vy: this.vy,
direction: direction,
angle: angle
});
}
this.state = this.STATE_INIT;
}
break;
}
};
SwipingDetector.prototype.getCoordinates = function(event, touch) {
return Object.freeze({
screenX: touch.screenX,
screenY: touch.screenY,
clientX: touch.clientX,
clientY: touch.clientY,
timeStamp: event.timeStamp
});
};
exports.SwipingDetector = SwipingDetector;
})(window);

View file

@ -0,0 +1,93 @@
'use strict';
(function(exports) {
var ViewUtils = {
};
var scaleContext;
ViewUtils.getScale = function(text, textWidth) {
if (!scaleContext) {
scaleContext = document.createElement('canvas')
.getContext('2d', { willReadFrequently: true });
scaleContext.font = '2rem sans-serif';
}
var elementWidth = scaleContext.measureText(text).width;
var s = textWidth / elementWidth;
if (s >= 1) {
return 1; // 10pt font "Body Large"
}
if (s >= 0.8) {
return 0.8; // 8pt font "Body"
}
if (s >= 0.7) {
return 0.7; // 7pt font "Body Medium"
}
if (s >= 0.65) {
return 0.65; // 6.5pt font "Body Small"
}
if (s >= 0.6) {
return 0.6; // 6pt font "Body Mini"
}
return s; // Something smaller than 6pt.
};
// Put the text in a span and make it fit in the container
ViewUtils.fitText = function(container, text, length, totalWidth) {
container.textContent = '';
if (!text) {
return null;
}
var span = document.createElement('span');
span.textContent = text;
container.appendChild(span);
var limit = 0.6; // Dont use a scale smaller than this
// container width is window width - 36 (for the dismiss button) and then
// depending on the number of suggestions there are.
var textWidth = (totalWidth - 36) / length | 0;
textWidth -= 6; // 6 pixels margin on both sides
var scale = ViewUtils.getScale(text, textWidth);
// If the text does not fit within the scaling limit,
// reduce the length of the text by replacing characters in
// the middle with ...
if (scale < limit) {
var charactersReplaced = text.length % 2;
while (scale < limit && charactersReplaced < text.length - 2) {
charactersReplaced += 2;
var halflen = (text.length - charactersReplaced) / 2;
span.textContent = text.substring(0, halflen) +
'…' +
text.substring(text.length - halflen);
scale = ViewUtils.getScale(text, textWidth);
}
}
// The scaling and centering we do only works if the span
// is display:block or inline-block
span.style.display = 'inline-block';
if (scale < 1) {
span.style.width = (100 / scale) + '%';
span.style.transformOrigin = 'left';
span.style.transform = 'scale(' + scale + ')';
} else {
span.style.width = '100%';
}
return span;
};
exports.ViewUtils = ViewUtils;
}(window));