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:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
206
gaia/apps/keyboard/js/views/alternatives_char_menu_view.js
Executable file
206
gaia/apps/keyboard/js/views/alternatives_char_menu_view.js
Executable 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);
|
||||
35
gaia/apps/keyboard/js/views/base_view.js
Executable file
35
gaia/apps/keyboard/js/views/base_view.js
Executable 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);
|
||||
257
gaia/apps/keyboard/js/views/candidate_panel_view.js
Executable file
257
gaia/apps/keyboard/js/views/candidate_panel_view.js
Executable 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);
|
||||
32
gaia/apps/keyboard/js/views/emoji_key_view.js
Executable file
32
gaia/apps/keyboard/js/views/emoji_key_view.js
Executable 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);
|
||||
92
gaia/apps/keyboard/js/views/handwriting_pad_view.js
Executable file
92
gaia/apps/keyboard/js/views/handwriting_pad_view.js
Executable 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);
|
||||
186
gaia/apps/keyboard/js/views/key_view.js
Executable file
186
gaia/apps/keyboard/js/views/key_view.js
Executable 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',
|
||||
' ': '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 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);
|
||||
142
gaia/apps/keyboard/js/views/latin_candidate_panel_view.js
Executable file
142
gaia/apps/keyboard/js/views/latin_candidate_panel_view.js
Executable 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);
|
||||
364
gaia/apps/keyboard/js/views/layout_page_view.js
Executable file
364
gaia/apps/keyboard/js/views/layout_page_view.js
Executable 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);
|
||||
89
gaia/apps/keyboard/js/views/swipeable_page_view.js
Executable file
89
gaia/apps/keyboard/js/views/swipeable_page_view.js
Executable 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);
|
||||
299
gaia/apps/keyboard/js/views/swipeable_panel_view.js
Executable file
299
gaia/apps/keyboard/js/views/swipeable_panel_view.js
Executable 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);
|
||||
160
gaia/apps/keyboard/js/views/swiping_detector.js
Executable file
160
gaia/apps/keyboard/js/views/swiping_detector.js
Executable 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);
|
||||
93
gaia/apps/keyboard/js/views/view_utils.js
Executable file
93
gaia/apps/keyboard/js/views/view_utils.js
Executable 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));
|
||||
Loading…
Reference in a new issue