VulpesOS/assets/webapps/communications.gaiamobile.org/dialer/js/suggestion_bar.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

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