Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
409 lines
12 KiB
JavaScript
Executable file
409 lines
12 KiB
JavaScript
Executable file
/* globals CallHandler, Contacts, fb, KeypadManager, LazyLoader,
|
|
SimplePhoneMatcher, SimSettingsHelper, Utils */
|
|
|
|
// Suggestion_bar.js will be loaded on init of KeypadManager through
|
|
// lazy loader. So we call its init() directly at the end of file.
|
|
|
|
'use strict';
|
|
|
|
var SuggestionBar = {
|
|
MIN_DIGIT_TO_SHOW: 3,
|
|
SKIP_FOR_COUNTRYCODE: 2,
|
|
MAX_ITEMS: 50,
|
|
|
|
_phoneNumber: null,
|
|
_contactList: null,
|
|
_loaded: false,
|
|
|
|
_pendingFbRequest: null,
|
|
_hasMatchingFbContacts: false,
|
|
_hasMatchingLocalContacts: false,
|
|
|
|
// Visual Elements
|
|
bar: null,
|
|
barSuggestionItem: null,
|
|
countTag: null,
|
|
list: null,
|
|
overlay: null,
|
|
overlayCancel: null,
|
|
template: null,
|
|
|
|
init: function sb_init() {
|
|
// When the DOM is absent (in the call screen) we don't need
|
|
// to initialize the module.
|
|
this.overlay = document.getElementById('contact-list-overlay');
|
|
if (!this.overlay) {
|
|
return;
|
|
}
|
|
|
|
this.bar = document.getElementById('suggestion-bar');
|
|
this.barSuggestionItem = this.bar.querySelector('.js-suggestion-item');
|
|
this.countTag = document.getElementById('suggestion-count');
|
|
this.template = document.getElementById('contact-in-overlay-template');
|
|
|
|
this.overlay.addEventListener('click', this);
|
|
this.bar.addEventListener('click', this);
|
|
this.countTag.addEventListener('click', this.showOverlay.bind(this));
|
|
KeypadManager.onValueChanged = this.update.bind(this);
|
|
},
|
|
|
|
handleEvent: function sb_handleEvent(event) {
|
|
var node = event.target;
|
|
if (!node.classList.contains('js-suggestion-item')) {
|
|
return;
|
|
}
|
|
|
|
event.stopPropagation();
|
|
var telTag = node.querySelector('.js-tel');
|
|
KeypadManager.updatePhoneNumber(telTag.textContent, 'begin', false);
|
|
// In the multi-SIM case, we just autocomplete the phone number without
|
|
// making the call. The call button is responsible for SIM selection
|
|
// behavior.
|
|
if (!navigator.mozIccManager ||
|
|
navigator.mozIccManager.iccIds.length < 2) {
|
|
LazyLoader.load('/shared/js/sim_settings_helper.js', function() {
|
|
SimSettingsHelper.getCardIndexFrom('outgoingCall', function(ci) {
|
|
CallHandler.call(KeypadManager.phoneNumber(), ci);
|
|
});
|
|
});
|
|
} else {
|
|
this.hideOverlay();
|
|
}
|
|
},
|
|
|
|
update: function sb_update(number) {
|
|
this._phoneNumber = number;
|
|
|
|
// when first letter of number is "+", we'll change minimum search criteria
|
|
// from MIN_DIGIT_TO_SHOW to MIN_DIGIT_TO_SHOW + SKIP_FOR_COUNTRYCODE.
|
|
var min = this.MIN_DIGIT_TO_SHOW +
|
|
((number.charAt(0) === '+') ? this.SKIP_FOR_COUNTRYCODE : 0);
|
|
|
|
if (number.length < min) {
|
|
this.clear(true);
|
|
return;
|
|
}
|
|
if (this._loaded) {
|
|
this._updateByContacts();
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
LazyLoader.load(['/shared/js/async_storage.js',
|
|
'/shared/js/dialer/contacts.js',
|
|
'/shared/js/simple_phone_matcher.js',
|
|
this.barSuggestionItem,
|
|
this.template],
|
|
function callback() {
|
|
self._loaded = true;
|
|
self._updateByContacts();
|
|
});
|
|
},
|
|
|
|
_renderBar: function sb_renderBar() {
|
|
var self = this;
|
|
|
|
self.bar.classList.remove('hide');
|
|
|
|
// Create matching index table for reference
|
|
self._allMatched = self._getAllMatched(self._contactList);
|
|
var totalMatchNum = self._allMatched.totalMatchNum;
|
|
|
|
var contact = self._contactList[0];
|
|
var firstMatch = self._allMatched.allMatches[0][0];
|
|
|
|
var shouldHideSuggestionBar = false;
|
|
|
|
// In a multi-SIM setup, tapping on a suggestion in the settings bar doesn't
|
|
// place a call, it just fills in the phone number. In this case, we should
|
|
// hide the suggestions bar to not confuse the user into thinking that
|
|
// tapping it again will place the call.
|
|
if (totalMatchNum === 1 &&
|
|
contact.tel[firstMatch].value == self._phoneNumber &&
|
|
navigator.mozIccManager &&
|
|
navigator.mozIccManager.iccIds.length > 1) {
|
|
shouldHideSuggestionBar = true;
|
|
}
|
|
|
|
// Don't show any suggestions if we have too many. The user should narrow it
|
|
// down further by entering more digits.
|
|
if (totalMatchNum > self.MAX_ITEMS) {
|
|
shouldHideSuggestionBar = true;
|
|
}
|
|
|
|
if (shouldHideSuggestionBar) {
|
|
self.clear();
|
|
return 0;
|
|
}
|
|
|
|
self.countTag.textContent = totalMatchNum;
|
|
|
|
var hasMoreThanOneMatch = (totalMatchNum > 1);
|
|
self.countTag.hidden = !hasMoreThanOneMatch;
|
|
self.countTag.classList.toggle('more', hasMoreThanOneMatch);
|
|
|
|
var node = self.barSuggestionItem;
|
|
self._fillContacts(contact, firstMatch, node);
|
|
self.bar.dataset.lastId = contact.id || contact.uid;
|
|
|
|
return totalMatchNum;
|
|
},
|
|
|
|
_checkIfCleared: function sb_checkIfCleared() {
|
|
var out = false;
|
|
|
|
if (!this._phoneNumber) {
|
|
this.bar.dataset.lastId = '';
|
|
this.clear(true);
|
|
out = true;
|
|
}
|
|
return out;
|
|
},
|
|
|
|
_searchCallback: function sb_searchCallback(contacts) {
|
|
var self = this;
|
|
|
|
self._pendingFbRequest = null;
|
|
|
|
if (self._checkIfCleared()) {
|
|
return;
|
|
}
|
|
|
|
var totalMatchNum = 0;
|
|
|
|
if (Array.isArray(contacts) && contacts.length > 0) {
|
|
self._contactList = contacts;
|
|
totalMatchNum = self._renderBar();
|
|
self._hasMatchingLocalContacts = true;
|
|
}
|
|
else {
|
|
// Avoid to clear the list if we have previous FB Contacts
|
|
// This will avoid continous reflows while refreshing
|
|
if (!self._hasMatchingFbContacts ||
|
|
(self._hasMatchingFbContacts && self._hasMatchingLocalContacts)) {
|
|
self.clear(false);
|
|
}
|
|
self._hasMatchingLocalContacts = false;
|
|
self._contactList = null;
|
|
}
|
|
|
|
if (totalMatchNum > self.MAX_ITEMS) {
|
|
self._hasMatchingFbContacts = false;
|
|
return;
|
|
}
|
|
|
|
var req = fb.contacts.search('phone', self._phoneNumber);
|
|
self._pendingFbRequest = req;
|
|
|
|
req.onsuccess = function() {
|
|
// Avoid to overlap FB requests
|
|
if (self._pendingFbRequest === req) {
|
|
self._pendingFbRequest = null;
|
|
self._searchCallbackFb(req.result);
|
|
}
|
|
};
|
|
req.onerror = function() {
|
|
window.console.error('Error while searching FB Data: ', req.error.name);
|
|
self._hasMatchingFbContacts = false;
|
|
};
|
|
},
|
|
|
|
_searchCallbackFb: function sb_searchCallbackFb(contacts) {
|
|
if (this._checkIfCleared()) {
|
|
return;
|
|
}
|
|
|
|
if (!Array.isArray(contacts) || contacts.length === 0) {
|
|
this._hasMatchingFbContacts = false;
|
|
if (!this._hasMatchingLocalContacts) {
|
|
this.clear(true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this._hasMatchingFbContacts = true;
|
|
this._contactList = (this._contactList || []).concat(contacts);
|
|
this._renderBar();
|
|
},
|
|
|
|
_updateByContacts: function sb_updateByContacts() {
|
|
// A search is both launched on mozContacts and on Facebook DS
|
|
Contacts.findListByNumber(this._phoneNumber, this.MAX_ITEMS + 1,
|
|
this._searchCallback.bind(this));
|
|
},
|
|
|
|
_fillContacts: function sb_fillContacts(contact, matchLocal, node) {
|
|
|
|
// if first letter of query is '+' and first letter of matchedTel isn't '+'
|
|
// we use query without country code instead of original query for
|
|
// markedNumber.
|
|
var matchedTel = contact.tel[matchLocal];
|
|
|
|
var markedNumber = this._markMatched(matchedTel.value, this._phoneNumber);
|
|
this._setItem(node, markedNumber, matchedTel.type,
|
|
contact.name[0]);
|
|
},
|
|
|
|
_createItem: function sb_createItem() {
|
|
var itemElm = this.template.cloneNode(true);
|
|
itemElm.removeAttribute('id');
|
|
itemElm.hidden = false;
|
|
itemElm.classList.add('ci--action-menu');
|
|
this.list.insertBefore(itemElm, this.overlayCancel);
|
|
return itemElm;
|
|
},
|
|
|
|
_setItem: function sb_setItem(node, tel, type, name) {
|
|
var typeTag = node.querySelector('.js-tel-type');
|
|
var telTag = node.querySelector('.js-tel');
|
|
var nameTag = node.querySelector('.js-name');
|
|
|
|
// Set the contact name
|
|
nameTag.textContent = name ? name : null;
|
|
|
|
// Set the number with the highlighted match
|
|
while (telTag.firstChild) {
|
|
telTag.removeChild(telTag.firstChild);
|
|
}
|
|
|
|
if (tel) {
|
|
telTag.appendChild(tel);
|
|
}
|
|
|
|
// Set the phone type
|
|
if (type) {
|
|
if (Utils.isPhoneType(type)) {
|
|
navigator.mozL10n.setAttributes(typeTag, type);
|
|
} else {
|
|
// No localization found, use the type string as-is
|
|
typeTag.removeAttribute('data-l10n-id');
|
|
typeTag.textContent = type;
|
|
}
|
|
} else {
|
|
typeTag.textContent = null;
|
|
}
|
|
},
|
|
|
|
clear: function sb_clear(isHardClear) {
|
|
if (!this._loaded) {
|
|
return;
|
|
}
|
|
this.countTag.textContent = '';
|
|
this.countTag.classList.remove('more');
|
|
// Clear contents
|
|
var node = this.barSuggestionItem;
|
|
this._setItem(node);
|
|
this._contactList = null;
|
|
this.bar.classList.add('hide');
|
|
|
|
if (isHardClear) {
|
|
this._phoneNumber = null;
|
|
delete this.bar.dataset.lastId;
|
|
}
|
|
},
|
|
|
|
_markMatched: function sb_markMatched(str, substr) {
|
|
var fragment = document.createDocumentFragment();
|
|
var sanitized = SimplePhoneMatcher.sanitizedNumber(str);
|
|
var digitBeforeMatch = sanitized.indexOf(substr);
|
|
if (digitBeforeMatch == -1) {
|
|
fragment.appendChild(document.createTextNode(str));
|
|
return fragment;
|
|
}
|
|
|
|
// Highlight matched number. We need to count formatting character in.
|
|
// we already have sanitized match position above. Then we search from
|
|
// head of str, count only valid digits to find corresponding start and
|
|
// end position in origin str.
|
|
var start = NaN;
|
|
var end = NaN;
|
|
var validDigits = 0;
|
|
for (var i = 0; i < str.length; i++) {
|
|
if (/^[\d\+*#]$/.test(str[i])) {
|
|
validDigits++;
|
|
}
|
|
if ((validDigits > digitBeforeMatch) && isNaN(start)) {
|
|
start = i;
|
|
} else if (validDigits >= (digitBeforeMatch + substr.length) &&
|
|
isNaN(end)) {
|
|
end = i;
|
|
}
|
|
}
|
|
var startStr = str.substr(0, start);
|
|
var middleStr = str.substr(start, end - start + 1);
|
|
var endStr = str.substr(end + 1);
|
|
|
|
var startNode = document.createTextNode(startStr);
|
|
var middleNode = document.createElement('mark');
|
|
middleNode.textContent = middleStr;
|
|
middleNode.classList.add('ci__mark');
|
|
var endNode = document.createTextNode(endStr);
|
|
|
|
fragment.appendChild(startNode);
|
|
fragment.appendChild(middleNode);
|
|
fragment.appendChild(endNode);
|
|
|
|
return fragment;
|
|
},
|
|
|
|
_initOverlay: function() {
|
|
if (this.list) {
|
|
return;
|
|
}
|
|
|
|
this.list = document.getElementById('contact-list');
|
|
this.overlayCancel = document.getElementById('contact-list-overlay-cancel');
|
|
this.overlayCancel.addEventListener('click', this.hideOverlay.bind(this));
|
|
},
|
|
|
|
_clearOverlay: function() {
|
|
while (this.list.firstElementChild != this.overlayCancel) {
|
|
this.list.firstElementChild.remove();
|
|
}
|
|
},
|
|
|
|
showOverlay: function sb_showOverlay() {
|
|
var self = this;
|
|
LazyLoader.load(this.overlay, function() {
|
|
self._initOverlay();
|
|
self._clearOverlay();
|
|
var title = self.overlay.querySelector('header');
|
|
navigator.mozL10n.setAttributes(
|
|
title,
|
|
'suggestionMatches',
|
|
{
|
|
n: +self.countTag.textContent,
|
|
matchNumber: self._phoneNumber
|
|
}
|
|
);
|
|
for (var i = 0; i < self._contactList.length; i++) {
|
|
for (var j = 0; j < self._allMatched.allMatches[i].length; j++) {
|
|
var node = self._createItem();
|
|
self._fillContacts(self._contactList[i],
|
|
self._allMatched.allMatches[i][j], node);
|
|
}
|
|
}
|
|
self.overlay.setAttribute('aria-hidden', false);
|
|
self.overlay.classList.add('display');
|
|
});
|
|
},
|
|
|
|
_getAllMatched: function sb_getAllMatched(contacts) {
|
|
var variants = SimplePhoneMatcher.generateVariants(this._phoneNumber);
|
|
|
|
var contactTels = contacts.map(function getTels(contact) {
|
|
return contact.tel.map(function getNumber(tel) {
|
|
return tel.value;
|
|
});
|
|
});
|
|
|
|
return SimplePhoneMatcher.bestMatch(variants, contactTels);
|
|
},
|
|
|
|
hideOverlay: function sb_hideOverlay() {
|
|
this.overlay.setAttribute('aria-hidden', true);
|
|
this.overlay.classList.remove('display');
|
|
}
|
|
};
|
|
|
|
SuggestionBar.init();
|