Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
409 lines
12 KiB
JavaScript
409 lines
12 KiB
JavaScript
/* 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();
|